From a8a812df7b4f6f50006d089b03f83d375b95ccd1 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 1 Aug 2024 16:39:20 +0300 Subject: [PATCH 1/2] feat: check whoami before other requests --- src/components/PDiskInfo/PDiskInfo.tsx | 3 +- src/containers/App/Content.tsx | 65 ++++---- .../AsideNavigation/AsideNavigation.tsx | 3 +- .../YdbInternalUser/YdbInternalUser.tsx | 9 +- .../Authentication/Authentication.tsx | 21 +-- src/containers/Authentication/utils.ts | 24 +++ src/containers/PDiskPage/PDiskPage.tsx | 3 +- .../Tablet/TabletControls/TabletControls.tsx | 3 +- src/containers/Tablets/Tablets.tsx | 3 +- src/containers/VDiskPage/VDiskPage.tsx | 3 +- src/services/api.ts | 13 +- src/store/reducers/api.ts | 2 +- .../reducers/authentication/authentication.ts | 147 ++++++++++-------- src/store/reducers/authentication/types.ts | 15 -- src/store/utils.ts | 6 +- 15 files changed, 169 insertions(+), 151 deletions(-) create mode 100644 src/containers/Authentication/utils.ts diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx index b8432739a..65b1ccc21 100644 --- a/src/components/PDiskInfo/PDiskInfo.tsx +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -1,4 +1,5 @@ import {getPDiskPagePath} from '../../routes'; +import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {useDiskPagesAvailable} from '../../store/reducers/capabilities/hooks'; import {valueIsDefined} from '../../utils'; import {formatBytes} from '../../utils/bytesParsers'; @@ -194,7 +195,7 @@ export function PDiskInfo({ withPDiskPageLink, className, }: PDiskInfoProps) { - const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const diskPagesAvailable = useDiskPagesAvailable(); const [generalInfo, statusInfo, spaceInfo, additionalInfo] = getPDiskInfo({ diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 174157cd6..2c588c907 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -1,19 +1,20 @@ import React from 'react'; -import {connect, shallowEqual} from 'react-redux'; +import {connect} from 'react-redux'; import type {RedirectProps} from 'react-router-dom'; import {Redirect, Route, Switch} from 'react-router-dom'; +import {PageError} from '../../components/Errors/PageError/PageError'; +import {Loader} from '../../components/Loader'; import {useSlots} from '../../components/slots'; import type {SlotMap} from '../../components/slots/SlotMap'; import type {SlotComponent} from '../../components/slots/types'; import routes from '../../routes'; import type {RootState} from '../../store'; -import {getUser} from '../../store/reducers/authentication/authentication'; +import {authenticationApi} from '../../store/reducers/authentication/authentication'; import {capabilitiesApi} from '../../store/reducers/capabilities/capabilities'; import {nodesListApi} from '../../store/reducers/nodesList'; import {cn} from '../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {lazyComponent} from '../../utils/lazyComponent'; import Authentication from '../Authentication/Authentication'; import {getClusterPath} from '../Cluster/utils'; @@ -143,43 +144,41 @@ export function Content(props: ContentProps) { {additionalRoutes?.rendered} {/* Single cluster routes */} - - - -
- - {routesSlots.map((route) => { - return renderRouteSlot(slots, route); - })} - } - /> - + + + +
+ + {routesSlots.map((route) => { + return renderRouteSlot(slots, route); + })} + ( + + )} + /> + + ); } -function GetUser() { - const dispatch = useTypedDispatch(); - const {isAuthenticated, isInternalUser} = useTypedSelector( - (state) => ({ - isAuthenticated: state.authentication.isAuthenticated, - isInternalUser: Boolean(state.authentication.user), - }), - shallowEqual, - ); +function GetUser({children}: {children: React.ReactNode}) { + const {isLoading, error} = authenticationApi.useWhoamiQuery(undefined); - React.useEffect(() => { - if (isAuthenticated && !isInternalUser) { - dispatch(getUser()); - } - }, [dispatch, isAuthenticated, isInternalUser]); + if (isLoading) { + return ; + } - return null; + if (error) { + return ; + } + + return children; } function GetNodesList() { diff --git a/src/containers/AsideNavigation/AsideNavigation.tsx b/src/containers/AsideNavigation/AsideNavigation.tsx index b4d86b76a..9ce69319f 100644 --- a/src/containers/AsideNavigation/AsideNavigation.tsx +++ b/src/containers/AsideNavigation/AsideNavigation.tsx @@ -5,6 +5,7 @@ import type {MenuItem} from '@gravity-ui/navigation'; import {AsideHeader, FooterItem} from '@gravity-ui/navigation'; import {useHistory} from 'react-router-dom'; +import {selectUser} from '../../store/reducers/authentication/authentication'; import {cn} from '../../utils/cn'; import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants'; import {useSetting, useTypedSelector} from '../../utils/hooks'; @@ -65,7 +66,7 @@ export function AsideNavigation(props: AsideNavigationProps) { const [visiblePanel, setVisiblePanel] = React.useState(); - const {user: ydbUser} = useTypedSelector((state) => state.authentication); + const ydbUser = useTypedSelector(selectUser); const [compact, setIsCompact] = useSetting(ASIDE_HEADER_COMPACT_KEY); return ( diff --git a/src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx b/src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx index b3a1eebb6..7ede9635c 100644 --- a/src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx +++ b/src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx @@ -3,9 +3,9 @@ import {Button, Icon} from '@gravity-ui/uikit'; import {useHistory} from 'react-router-dom'; import routes, {createHref} from '../../../routes'; -import {logout} from '../../../store/reducers/authentication/authentication'; +import {authenticationApi} from '../../../store/reducers/authentication/authentication'; import {cn} from '../../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; +import {useTypedSelector} from '../../../utils/hooks'; import i18n from '../i18n'; import './YdbInternalUser.scss'; @@ -15,6 +15,8 @@ const b = cn('kv-ydb-internal-user'); export function YdbInternalUser() { const {user: ydbUser} = useTypedSelector((state) => state.authentication); + const [logout] = authenticationApi.useLogoutMutation(); + const history = useHistory(); const handleLoginClick = () => { history.push( @@ -22,9 +24,8 @@ export function YdbInternalUser() { ); }; - const dispatch = useTypedDispatch(); const handleLogout = () => { - dispatch(logout); + logout(undefined); }; return ( diff --git a/src/containers/Authentication/Authentication.tsx b/src/containers/Authentication/Authentication.tsx index 3e3f66c7a..bcc67fa65 100644 --- a/src/containers/Authentication/Authentication.tsx +++ b/src/containers/Authentication/Authentication.tsx @@ -5,9 +5,10 @@ import {Button, Link as ExternalLink, Icon, TextInput} from '@gravity-ui/uikit'; import {useHistory, useLocation} from 'react-router-dom'; import {parseQuery} from '../../routes'; -import {authenticate} from '../../store/reducers/authentication/authentication'; +import {authenticationApi} from '../../store/reducers/authentication/authentication'; import {cn} from '../../utils/cn'; -import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; + +import {isPasswordError, isUserError} from './utils'; import ydbLogoIcon from '../../assets/icons/ydb.svg'; @@ -20,25 +21,24 @@ interface AuthenticationProps { } function Authentication({closable = false}: AuthenticationProps) { - const dispatch = useTypedDispatch(); const history = useHistory(); const location = useLocation(); - const {returnUrl} = parseQuery(location); + const [authenticate, {error, isLoading}] = authenticationApi.useAuthenticateMutation(undefined); - const {error} = useTypedSelector((state) => state.authentication); + const {returnUrl} = parseQuery(location); const [login, setLogin] = React.useState(''); - const [pass, setPass] = React.useState(''); + const [password, setPass] = React.useState(''); const [loginError, setLoginError] = React.useState(''); const [passwordError, setPasswordError] = React.useState(''); const [showPassword, setShowPassword] = React.useState(false); React.useEffect(() => { - if (error?.data?.error?.includes('user')) { + if (isUserError(error)) { setLoginError(error.data.error); } - if (error?.data?.error?.includes('password')) { + if (isPasswordError(error)) { setPasswordError(error.data.error); } }, [error]); @@ -54,7 +54,7 @@ function Authentication({closable = false}: AuthenticationProps) { }; const onLoginClick = () => { - dispatch(authenticate(login, pass)).then(() => { + authenticate({user: login, password}).then(() => { if (returnUrl) { const decodedUrl = decodeURIComponent(returnUrl.toString()); @@ -108,7 +108,7 @@ function Authentication({closable = false}: AuthenticationProps) {
Sign in diff --git a/src/containers/Authentication/utils.ts b/src/containers/Authentication/utils.ts new file mode 100644 index 000000000..c017df1c4 --- /dev/null +++ b/src/containers/Authentication/utils.ts @@ -0,0 +1,24 @@ +interface AuthError { + data: { + error: string; + }; +} + +function isAuthError(error: unknown): error is AuthError { + return Boolean( + error && + typeof error === 'object' && + 'data' in error && + error.data && + typeof error.data === 'object' && + 'error' in error.data && + typeof error.data.error === 'string', + ); +} + +export function isUserError(error: unknown): error is AuthError { + return isAuthError(error) && error.data.error.includes('user'); +} +export function isPasswordError(error: unknown): error is AuthError { + return isAuthError(error) && error.data.error.includes('password'); +} diff --git a/src/containers/PDiskPage/PDiskPage.tsx b/src/containers/PDiskPage/PDiskPage.tsx index fb4a49721..0273f7372 100644 --- a/src/containers/PDiskPage/PDiskPage.tsx +++ b/src/containers/PDiskPage/PDiskPage.tsx @@ -17,6 +17,7 @@ import {PDiskInfo} from '../../components/PDiskInfo/PDiskInfo'; import {PageMeta} from '../../components/PageMeta/PageMeta'; import {getPDiskPagePath} from '../../routes'; import {api} from '../../store/reducers/api'; +import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {pDiskApi} from '../../store/reducers/pdisk/pdisk'; import {valueIsDefined} from '../../utils'; @@ -55,7 +56,7 @@ const pDiskTabSchema = z.nativeEnum(PDISK_TABS_IDS).catch(PDISK_TABS_IDS.diskDis export function PDiskPage() { const dispatch = useTypedDispatch(); - const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const [{nodeId, pDiskId, activeTab}] = useQueryParams({ activeTab: StringParam, diff --git a/src/containers/Tablet/TabletControls/TabletControls.tsx b/src/containers/Tablet/TabletControls/TabletControls.tsx index ea34e2e5c..20ceb6ab9 100644 --- a/src/containers/Tablet/TabletControls/TabletControls.tsx +++ b/src/containers/Tablet/TabletControls/TabletControls.tsx @@ -1,6 +1,7 @@ import React from 'react'; import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; +import {selectIsUserAllowedToMakeChanges} from '../../../store/reducers/authentication/authentication'; import {ETabletState} from '../../../types/api/tablet'; import type {TTabletStateInfo} from '../../../types/api/tablet'; import {useTypedSelector} from '../../../utils/hooks'; @@ -15,7 +16,7 @@ interface TabletControlsProps { export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => { const {TabletId, HiveId} = tablet; - const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const _onKillClick = () => { return window.api.killTablet(TabletId); diff --git a/src/containers/Tablets/Tablets.tsx b/src/containers/Tablets/Tablets.tsx index 5d294b0d2..f230c907a 100644 --- a/src/containers/Tablets/Tablets.tsx +++ b/src/containers/Tablets/Tablets.tsx @@ -11,6 +11,7 @@ import {InternalLink} from '../../components/InternalLink'; import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable'; import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton'; import routes, {createHref} from '../../routes'; +import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; import {selectTabletsWithFqdn, tabletsApi} from '../../store/reducers/tablets'; import {ETabletState} from '../../types/api/tablet'; import type {TTabletStateInfo} from '../../types/api/tablet'; @@ -136,7 +137,7 @@ const columns: DataTableColumn[] = [ function TabletActions(tablet: TTabletStateInfo) { const isDisabledRestart = tablet.State === ETabletState.Stopped; const dispatch = useTypedDispatch(); - const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); return ( state.authentication); + const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); const [{nodeId, pDiskId, vDiskSlotId}] = useQueryParams({ nodeId: StringParam, diff --git a/src/services/api.ts b/src/services/api.ts index 35e49feb0..b341606b0 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -78,7 +78,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { // authUrl - external auth service link, after successful auth additional cookies will be appended // that will allow access to clusters where OIDC proxy is a balancer if (response && response.status === 401 && response.data?.authUrl) { - return window.location.assign(response.data.authUrl); + window.location.assign(response.data.authUrl); } return Promise.reject(error); @@ -611,15 +611,8 @@ export class YdbEmbeddedAPI extends AxiosWrapper { }, ); } - authenticate(user: string, password: string) { - return this.post( - this.getPath('/login'), - { - user, - password, - }, - {}, - ); + authenticate(params: {user: string; password: string}) { + return this.post(this.getPath('/login'), params, {}); } logout() { return this.post(this.getPath('/logout'), {}, {}); diff --git a/src/store/reducers/api.ts b/src/store/reducers/api.ts index 08079da24..fa2760608 100644 --- a/src/store/reducers/api.ts +++ b/src/store/reducers/api.ts @@ -9,7 +9,7 @@ export const api = createApi({ */ endpoints: () => ({}), invalidationBehavior: 'immediately', - tagTypes: ['All', 'PDiskData'], + tagTypes: ['All', 'PDiskData', 'UserData'], }); export const _NEVER = Symbol(); diff --git a/src/store/reducers/authentication/authentication.ts b/src/store/reducers/authentication/authentication.ts index 4e4250d73..f01a2c0fd 100644 --- a/src/store/reducers/authentication/authentication.ts +++ b/src/store/reducers/authentication/authentication.ts @@ -1,81 +1,92 @@ -import type {Reducer} from '@reduxjs/toolkit'; +import {createSlice} from '@reduxjs/toolkit'; +import type {PayloadAction} from '@reduxjs/toolkit'; -import {createApiRequest, createRequestActionTypes} from '../../utils'; +import type {TUserToken} from '../../../types/api/whoami'; +import {isAxiosResponse} from '../../../utils/response'; +import {api} from '../api'; -import type {AuthenticationAction, AuthenticationState} from './types'; +import type {AuthenticationState} from './types'; -export const SET_UNAUTHENTICATED = createRequestActionTypes( - 'authentication', - 'SET_UNAUTHENTICATED', -); -export const SET_AUTHENTICATED = createRequestActionTypes('authentication', 'SET_AUTHENTICATED'); -export const FETCH_USER = createRequestActionTypes('authentication', 'FETCH_USER'); - -const initialState = { +const initialState: AuthenticationState = { isAuthenticated: true, user: '', - error: undefined, }; -const authentication: Reducer = ( - state = initialState, - action, -) => { - switch (action.type) { - case SET_UNAUTHENTICATED.SUCCESS: { - return {...state, isAuthenticated: false, user: '', error: undefined}; - } - case SET_AUTHENTICATED.SUCCESS: { - return {...state, isAuthenticated: true, error: undefined}; - } - case SET_AUTHENTICATED.FAILURE: { - return {...state, error: action.error}; - } - case FETCH_USER.SUCCESS: { - const {user, isUserAllowedToMakeChanges} = action.data; - - return { - ...state, - user, - isUserAllowedToMakeChanges, - }; - } +export const slice = createSlice({ + name: 'authentication', + initialState, + reducers: { + setIsAuthenticated: (state, action: PayloadAction) => { + const isAuthenticated = action.payload; - default: - return {...state}; - } -}; + state.isAuthenticated = isAuthenticated; -export const authenticate = (user: string, password: string) => { - return createApiRequest({ - request: window.api.authenticate(user, password), - actions: SET_AUTHENTICATED, - }); -}; + if (!isAuthenticated) { + state.user = ''; + } + }, + setUser: (state, action: PayloadAction) => { + const {UserSID, AuthType, IsMonitoringAllowed} = action.payload; -export const logout = () => { - return createApiRequest({ - request: window.api.logout(), - actions: SET_UNAUTHENTICATED, - }); -}; + state.user = AuthType === 'Login' ? UserSID : undefined; -export const getUser = () => { - return createApiRequest({ - request: window.api.whoami(), - actions: FETCH_USER, - dataHandler: (data) => { - const {UserSID, AuthType, IsMonitoringAllowed} = data; - return { - user: AuthType === 'Login' ? UserSID : undefined, - // If ydb version supports this feature, - // There should be explicit flag in whoami response - // Otherwise every user is allowed to make changes - // Anyway there will be guards on backend - isUserAllowedToMakeChanges: IsMonitoringAllowed !== false, - }; + // If ydb version supports this feature, + // There should be explicit flag in whoami response + // Otherwise every user is allowed to make changes + // Anyway there will be guards on backend + state.isUserAllowedToMakeChanges = IsMonitoringAllowed !== false; }, - }); -}; + }, + selectors: { + selectIsUserAllowedToMakeChanges: (state) => state.isUserAllowedToMakeChanges, + selectUser: (state) => state.user, + }, +}); + +export default slice.reducer; +export const {setIsAuthenticated, setUser} = slice.actions; +export const {selectIsUserAllowedToMakeChanges, selectUser} = slice.selectors; -export default authentication; +export const authenticationApi = api.injectEndpoints({ + endpoints: (build) => ({ + whoami: build.query({ + queryFn: async (_, {dispatch}) => { + try { + const data = await window.api.whoami(); + dispatch(setUser(data)); + return {data}; + } catch (error) { + if (isAxiosResponse(error) && error.status === 401) { + dispatch(setIsAuthenticated(false)); + } + return {error}; + } + }, + providesTags: ['UserData'], + }), + authenticate: build.mutation({ + queryFn: async (params: {user: string; password: string}, {dispatch}) => { + try { + const data = await window.api.authenticate(params); + dispatch(setIsAuthenticated(true)); + return {data}; + } catch (error) { + return {error}; + } + }, + invalidatesTags: (_, error) => (error ? [] : ['UserData']), + }), + logout: build.mutation({ + queryFn: async (_, {dispatch}) => { + try { + const data = await window.api.logout(); + dispatch(setIsAuthenticated(false)); + return {data}; + } catch (error) { + return {error}; + } + }, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/store/reducers/authentication/types.ts b/src/store/reducers/authentication/types.ts index df241d67e..bb58438c4 100644 --- a/src/store/reducers/authentication/types.ts +++ b/src/store/reducers/authentication/types.ts @@ -1,20 +1,5 @@ -import type {AuthErrorResponse} from '../../../types/api/error'; -import type {ApiRequestAction} from '../../utils'; - -import type {FETCH_USER, SET_AUTHENTICATED, SET_UNAUTHENTICATED} from './authentication'; - export interface AuthenticationState { isAuthenticated: boolean; isUserAllowedToMakeChanges?: boolean; user: string | undefined; - error: AuthErrorResponse | undefined; } - -export type AuthenticationAction = - | ApiRequestAction - | ApiRequestAction - | ApiRequestAction< - typeof FETCH_USER, - {user: string | undefined; isUserAllowedToMakeChanges: boolean}, - unknown - >; diff --git a/src/store/utils.ts b/src/store/utils.ts index 798eef577..4d893bac4 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,7 +1,7 @@ import createToast from '../utils/createToast'; import {isAxiosResponse} from '../utils/response'; -import {SET_UNAUTHENTICATED} from './reducers/authentication/authentication'; +import {setIsAuthenticated} from './reducers/authentication/authentication'; import type {AppDispatch, GetState} from '.'; @@ -50,9 +50,7 @@ export function createApiRequest< return data; } catch (error) { if (isAxiosResponse(error) && error.status === 401) { - dispatch({ - type: SET_UNAUTHENTICATED.SUCCESS, - }); + dispatch(setIsAuthenticated(false)); } else if (isAxiosResponse(error) && error.status >= 500 && error.statusText) { createToast({ name: 'Request failure', From 551812dd6cf6652c8e5a5f960e03431dc12bed07 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 2 Aug 2024 13:56:49 +0300 Subject: [PATCH 2/2] fix: review --- src/containers/App/Content.tsx | 16 ++---- src/store/reducers/executeQuery.ts | 3 - src/store/utils.ts | 91 ------------------------------ 3 files changed, 6 insertions(+), 104 deletions(-) delete mode 100644 src/store/utils.ts diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 2c588c907..9b783b0e3 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -5,7 +5,7 @@ import type {RedirectProps} from 'react-router-dom'; import {Redirect, Route, Switch} from 'react-router-dom'; import {PageError} from '../../components/Errors/PageError/PageError'; -import {Loader} from '../../components/Loader'; +import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import {useSlots} from '../../components/slots'; import type {SlotMap} from '../../components/slots/SlotMap'; import type {SlotComponent} from '../../components/slots/types'; @@ -170,15 +170,11 @@ export function Content(props: ContentProps) { function GetUser({children}: {children: React.ReactNode}) { const {isLoading, error} = authenticationApi.useWhoamiQuery(undefined); - if (isLoading) { - return ; - } - - if (error) { - return ; - } - - return children; + return ( + + {children} + + ); } function GetNodesList() { diff --git a/src/store/reducers/executeQuery.ts b/src/store/reducers/executeQuery.ts index 6809e7f61..aa3967438 100644 --- a/src/store/reducers/executeQuery.ts +++ b/src/store/reducers/executeQuery.ts @@ -18,14 +18,11 @@ import type { import {QUERIES_HISTORY_KEY} from '../../utils/constants'; import {QUERY_SYNTAX, isQueryErrorResponse, parseQueryAPIExecuteResponse} from '../../utils/query'; import {isNumeric} from '../../utils/utils'; -import {createRequestActionTypes} from '../utils'; import {api} from './api'; const MAXIMUM_QUERIES_IN_HISTORY = 20; -export const SEND_QUERY = createRequestActionTypes('query', 'SEND_QUERY'); - const CHANGE_USER_INPUT = 'query/CHANGE_USER_INPUT'; const SAVE_QUERY_TO_HISTORY = 'query/SAVE_QUERY_TO_HISTORY'; const GO_TO_PREVIOUS_QUERY = 'query/GO_TO_PREVIOUS_QUERY'; diff --git a/src/store/utils.ts b/src/store/utils.ts deleted file mode 100644 index 4d893bac4..000000000 --- a/src/store/utils.ts +++ /dev/null @@ -1,91 +0,0 @@ -import createToast from '../utils/createToast'; -import {isAxiosResponse} from '../utils/response'; - -import {setIsAuthenticated} from './reducers/authentication/authentication'; - -import type {AppDispatch, GetState} from '.'; - -export const nop = (result: any) => result; - -export function createRequestActionTypes( - prefix: Prefix, - type: Type, -) { - return { - REQUEST: `${prefix}/${type}_REQUEST`, - SUCCESS: `${prefix}/${type}_SUCCESS`, - FAILURE: `${prefix}/${type}_FAILURE`, - } as const; -} - -type CreateApiRequestParams = { - actions: Actions; - request: Promise; - dataHandler?: (data: Response, getState: GetState) => HandledResponse; -}; - -export function createApiRequest< - Actions extends ReturnType, - Response, - HandledResponse, ->({ - actions, - request, - dataHandler = nop, -}: CreateApiRequestParams) { - const doRequest = async function (dispatch: AppDispatch, getState: GetState) { - dispatch({ - type: actions.REQUEST, - }); - - try { - const result = await request; - const data = dataHandler(result, getState); - - dispatch({ - type: actions.SUCCESS, - data, - }); - - return data; - } catch (error) { - if (isAxiosResponse(error) && error.status === 401) { - dispatch(setIsAuthenticated(false)); - } else if (isAxiosResponse(error) && error.status >= 500 && error.statusText) { - createToast({ - name: 'Request failure', - title: 'Request failure', - type: 'error', - content: `${error.status} ${error.statusText}`, - }); - } - - dispatch({ - type: actions.FAILURE, - error, - }); - - // TODO should probably throw the received error here, but this change requires a thorough revision of all api calls - return undefined; - } - }; - - return doRequest; -} - -export type ApiRequestAction< - Actions extends ReturnType, - SuccessResponse = unknown, - ErrorResponse = unknown, -> = - | { - type: Actions['REQUEST']; - } - | { - type: Actions['SUCCESS']; - data: SuccessResponse; - } - | { - type: Actions['FAILURE']; - error: ErrorResponse; - };