Skip to content

Commit 70b6dc3

Browse files
feat: check whoami before other requests (#1103)
1 parent e343a13 commit 70b6dc3

File tree

16 files changed

+164
-244
lines changed

16 files changed

+164
-244
lines changed

src/components/PDiskInfo/PDiskInfo.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {getPDiskPagePath} from '../../routes';
2+
import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
23
import {useDiskPagesAvailable} from '../../store/reducers/capabilities/hooks';
34
import {valueIsDefined} from '../../utils';
45
import {formatBytes} from '../../utils/bytesParsers';
@@ -194,7 +195,7 @@ export function PDiskInfo<T extends PreparedPDisk>({
194195
withPDiskPageLink,
195196
className,
196197
}: PDiskInfoProps<T>) {
197-
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);
198+
const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
198199
const diskPagesAvailable = useDiskPagesAvailable();
199200

200201
const [generalInfo, statusInfo, spaceInfo, additionalInfo] = getPDiskInfo({

src/containers/App/Content.tsx

+29-34
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
import React from 'react';
22

3-
import {connect, shallowEqual} from 'react-redux';
3+
import {connect} from 'react-redux';
44
import type {RedirectProps} from 'react-router-dom';
55
import {Redirect, Route, Switch} from 'react-router-dom';
66

7+
import {PageError} from '../../components/Errors/PageError/PageError';
8+
import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
79
import {useSlots} from '../../components/slots';
810
import type {SlotMap} from '../../components/slots/SlotMap';
911
import type {SlotComponent} from '../../components/slots/types';
1012
import routes from '../../routes';
1113
import type {RootState} from '../../store';
12-
import {getUser} from '../../store/reducers/authentication/authentication';
14+
import {authenticationApi} from '../../store/reducers/authentication/authentication';
1315
import {capabilitiesApi} from '../../store/reducers/capabilities/capabilities';
1416
import {nodesListApi} from '../../store/reducers/nodesList';
1517
import {cn} from '../../utils/cn';
16-
import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
1718
import {lazyComponent} from '../../utils/lazyComponent';
1819
import Authentication from '../Authentication/Authentication';
1920
import {getClusterPath} from '../Cluster/utils';
@@ -143,43 +144,37 @@ export function Content(props: ContentProps) {
143144
{additionalRoutes?.rendered}
144145
{/* Single cluster routes */}
145146
<Route key="single-cluster">
146-
<GetUser />
147-
<GetNodesList />
148-
<GetCapabilities />
149-
<Header mainPage={mainPage} />
150-
<Switch>
151-
{routesSlots.map((route) => {
152-
return renderRouteSlot(slots, route);
153-
})}
154-
<Route
155-
path={redirectProps.from || redirectProps.path}
156-
exact={redirectProps.exact}
157-
strict={redirectProps.strict}
158-
render={() => <Redirect to={redirectProps.to} push={redirectProps.push} />}
159-
/>
160-
</Switch>
147+
<GetUser>
148+
<GetNodesList />
149+
<GetCapabilities />
150+
<Header mainPage={mainPage} />
151+
<Switch>
152+
{routesSlots.map((route) => {
153+
return renderRouteSlot(slots, route);
154+
})}
155+
<Route
156+
path={redirectProps.from || redirectProps.path}
157+
exact={redirectProps.exact}
158+
strict={redirectProps.strict}
159+
render={() => (
160+
<Redirect to={redirectProps.to} push={redirectProps.push} />
161+
)}
162+
/>
163+
</Switch>
164+
</GetUser>
161165
</Route>
162166
</Switch>
163167
);
164168
}
165169

166-
function GetUser() {
167-
const dispatch = useTypedDispatch();
168-
const {isAuthenticated, isInternalUser} = useTypedSelector(
169-
(state) => ({
170-
isAuthenticated: state.authentication.isAuthenticated,
171-
isInternalUser: Boolean(state.authentication.user),
172-
}),
173-
shallowEqual,
174-
);
175-
176-
React.useEffect(() => {
177-
if (isAuthenticated && !isInternalUser) {
178-
dispatch(getUser());
179-
}
180-
}, [dispatch, isAuthenticated, isInternalUser]);
170+
function GetUser({children}: {children: React.ReactNode}) {
171+
const {isLoading, error} = authenticationApi.useWhoamiQuery(undefined);
181172

182-
return null;
173+
return (
174+
<LoaderWrapper loading={isLoading} size="l">
175+
<PageError error={error}>{children}</PageError>
176+
</LoaderWrapper>
177+
);
183178
}
184179

185180
function GetNodesList() {

src/containers/AsideNavigation/AsideNavigation.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {MenuItem} from '@gravity-ui/navigation';
55
import {AsideHeader, FooterItem} from '@gravity-ui/navigation';
66
import {useHistory} from 'react-router-dom';
77

8+
import {selectUser} from '../../store/reducers/authentication/authentication';
89
import {cn} from '../../utils/cn';
910
import {ASIDE_HEADER_COMPACT_KEY} from '../../utils/constants';
1011
import {useSetting, useTypedSelector} from '../../utils/hooks';
@@ -65,7 +66,7 @@ export function AsideNavigation(props: AsideNavigationProps) {
6566

6667
const [visiblePanel, setVisiblePanel] = React.useState<Panel>();
6768

68-
const {user: ydbUser} = useTypedSelector((state) => state.authentication);
69+
const ydbUser = useTypedSelector(selectUser);
6970
const [compact, setIsCompact] = useSetting<boolean>(ASIDE_HEADER_COMPACT_KEY);
7071

7172
return (

src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx

+5-4
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import {Button, Icon} from '@gravity-ui/uikit';
33
import {useHistory} from 'react-router-dom';
44

55
import routes, {createHref} from '../../../routes';
6-
import {logout} from '../../../store/reducers/authentication/authentication';
6+
import {authenticationApi} from '../../../store/reducers/authentication/authentication';
77
import {cn} from '../../../utils/cn';
8-
import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
8+
import {useTypedSelector} from '../../../utils/hooks';
99
import i18n from '../i18n';
1010

1111
import './YdbInternalUser.scss';
@@ -15,16 +15,17 @@ const b = cn('kv-ydb-internal-user');
1515
export function YdbInternalUser() {
1616
const {user: ydbUser} = useTypedSelector((state) => state.authentication);
1717

18+
const [logout] = authenticationApi.useLogoutMutation();
19+
1820
const history = useHistory();
1921
const handleLoginClick = () => {
2022
history.push(
2123
createHref(routes.auth, undefined, {returnUrl: encodeURIComponent(location.href)}),
2224
);
2325
};
2426

25-
const dispatch = useTypedDispatch();
2627
const handleLogout = () => {
27-
dispatch(logout);
28+
logout(undefined);
2829
};
2930

3031
return (

src/containers/Authentication/Authentication.tsx

+11-10
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import {Button, Link as ExternalLink, Icon, TextInput} from '@gravity-ui/uikit';
55
import {useHistory, useLocation} from 'react-router-dom';
66

77
import {parseQuery} from '../../routes';
8-
import {authenticate} from '../../store/reducers/authentication/authentication';
8+
import {authenticationApi} from '../../store/reducers/authentication/authentication';
99
import {cn} from '../../utils/cn';
10-
import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
10+
11+
import {isPasswordError, isUserError} from './utils';
1112

1213
import ydbLogoIcon from '../../assets/icons/ydb.svg';
1314

@@ -20,25 +21,24 @@ interface AuthenticationProps {
2021
}
2122

2223
function Authentication({closable = false}: AuthenticationProps) {
23-
const dispatch = useTypedDispatch();
2424
const history = useHistory();
2525
const location = useLocation();
2626

27-
const {returnUrl} = parseQuery(location);
27+
const [authenticate, {error, isLoading}] = authenticationApi.useAuthenticateMutation(undefined);
2828

29-
const {error} = useTypedSelector((state) => state.authentication);
29+
const {returnUrl} = parseQuery(location);
3030

3131
const [login, setLogin] = React.useState('');
32-
const [pass, setPass] = React.useState('');
32+
const [password, setPass] = React.useState('');
3333
const [loginError, setLoginError] = React.useState('');
3434
const [passwordError, setPasswordError] = React.useState('');
3535
const [showPassword, setShowPassword] = React.useState(false);
3636

3737
React.useEffect(() => {
38-
if (error?.data?.error?.includes('user')) {
38+
if (isUserError(error)) {
3939
setLoginError(error.data.error);
4040
}
41-
if (error?.data?.error?.includes('password')) {
41+
if (isPasswordError(error)) {
4242
setPasswordError(error.data.error);
4343
}
4444
}, [error]);
@@ -54,7 +54,7 @@ function Authentication({closable = false}: AuthenticationProps) {
5454
};
5555

5656
const onLoginClick = () => {
57-
dispatch(authenticate(login, pass)).then(() => {
57+
authenticate({user: login, password}).then(() => {
5858
if (returnUrl) {
5959
const decodedUrl = decodeURIComponent(returnUrl.toString());
6060

@@ -108,7 +108,7 @@ function Authentication({closable = false}: AuthenticationProps) {
108108
</div>
109109
<div className={b('field-wrapper')}>
110110
<TextInput
111-
value={pass}
111+
value={password}
112112
onUpdate={onPassUpdate}
113113
type={showPassword ? 'text' : 'password'}
114114
placeholder={'Password'}
@@ -130,6 +130,7 @@ function Authentication({closable = false}: AuthenticationProps) {
130130
width="max"
131131
size="l"
132132
disabled={Boolean(!login || loginError || passwordError)}
133+
loading={isLoading}
133134
className={b('button-sign-in')}
134135
>
135136
Sign in
+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
interface AuthError {
2+
data: {
3+
error: string;
4+
};
5+
}
6+
7+
function isAuthError(error: unknown): error is AuthError {
8+
return Boolean(
9+
error &&
10+
typeof error === 'object' &&
11+
'data' in error &&
12+
error.data &&
13+
typeof error.data === 'object' &&
14+
'error' in error.data &&
15+
typeof error.data.error === 'string',
16+
);
17+
}
18+
19+
export function isUserError(error: unknown): error is AuthError {
20+
return isAuthError(error) && error.data.error.includes('user');
21+
}
22+
export function isPasswordError(error: unknown): error is AuthError {
23+
return isAuthError(error) && error.data.error.includes('password');
24+
}

src/containers/PDiskPage/PDiskPage.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {PDiskInfo} from '../../components/PDiskInfo/PDiskInfo';
1717
import {PageMeta} from '../../components/PageMeta/PageMeta';
1818
import {getPDiskPagePath} from '../../routes';
1919
import {api} from '../../store/reducers/api';
20+
import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
2021
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
2122
import {pDiskApi} from '../../store/reducers/pdisk/pdisk';
2223
import {valueIsDefined} from '../../utils';
@@ -55,7 +56,7 @@ const pDiskTabSchema = z.nativeEnum(PDISK_TABS_IDS).catch(PDISK_TABS_IDS.diskDis
5556
export function PDiskPage() {
5657
const dispatch = useTypedDispatch();
5758

58-
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);
59+
const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
5960

6061
const [{nodeId, pDiskId, activeTab}] = useQueryParams({
6162
activeTab: StringParam,

src/containers/Tablet/TabletControls/TabletControls.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22

33
import {ButtonWithConfirmDialog} from '../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
4+
import {selectIsUserAllowedToMakeChanges} from '../../../store/reducers/authentication/authentication';
45
import {ETabletState} from '../../../types/api/tablet';
56
import type {TTabletStateInfo} from '../../../types/api/tablet';
67
import {useTypedSelector} from '../../../utils/hooks';
@@ -15,7 +16,7 @@ interface TabletControlsProps {
1516
export const TabletControls = ({tablet, fetchData}: TabletControlsProps) => {
1617
const {TabletId, HiveId} = tablet;
1718

18-
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);
19+
const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
1920

2021
const _onKillClick = () => {
2122
return window.api.killTablet(TabletId);

src/containers/Tablets/Tablets.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {InternalLink} from '../../components/InternalLink';
1111
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
1212
import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
1313
import routes, {createHref} from '../../routes';
14+
import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
1415
import {selectTabletsWithFqdn, tabletsApi} from '../../store/reducers/tablets';
1516
import {ETabletState} from '../../types/api/tablet';
1617
import type {TTabletStateInfo} from '../../types/api/tablet';
@@ -136,7 +137,7 @@ const columns: DataTableColumn<TTabletStateInfo & {fqdn?: string}>[] = [
136137
function TabletActions(tablet: TTabletStateInfo) {
137138
const isDisabledRestart = tablet.State === ETabletState.Stopped;
138139
const dispatch = useTypedDispatch();
139-
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);
140+
const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
140141

141142
return (
142143
<ButtonWithConfirmDialog

src/containers/VDiskPage/VDiskPage.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewer
1515
import {PageMeta} from '../../components/PageMeta/PageMeta';
1616
import {VDiskWithDonorsStack} from '../../components/VDisk/VDiskWithDonorsStack';
1717
import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo';
18+
import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
1819
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
1920
import {selectNodesMap} from '../../store/reducers/nodesList';
2021
import {vDiskApi} from '../../store/reducers/vdisk/vdisk';
@@ -34,7 +35,7 @@ export function VDiskPage() {
3435
const dispatch = useTypedDispatch();
3536

3637
const nodesMap = useTypedSelector(selectNodesMap);
37-
const {isUserAllowedToMakeChanges} = useTypedSelector((state) => state.authentication);
38+
const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
3839

3940
const [{nodeId, pDiskId, vDiskSlotId}] = useQueryParams({
4041
nodeId: StringParam,

src/services/api.ts

+3-10
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
7878
// authUrl - external auth service link, after successful auth additional cookies will be appended
7979
// that will allow access to clusters where OIDC proxy is a balancer
8080
if (response && response.status === 401 && response.data?.authUrl) {
81-
return window.location.assign(response.data.authUrl);
81+
window.location.assign(response.data.authUrl);
8282
}
8383

8484
return Promise.reject(error);
@@ -611,15 +611,8 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
611611
},
612612
);
613613
}
614-
authenticate(user: string, password: string) {
615-
return this.post(
616-
this.getPath('/login'),
617-
{
618-
user,
619-
password,
620-
},
621-
{},
622-
);
614+
authenticate(params: {user: string; password: string}) {
615+
return this.post(this.getPath('/login'), params, {});
623616
}
624617
logout() {
625618
return this.post(this.getPath('/logout'), {}, {});

src/store/reducers/api.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const api = createApi({
99
*/
1010
endpoints: () => ({}),
1111
invalidationBehavior: 'immediately',
12-
tagTypes: ['All', 'PDiskData'],
12+
tagTypes: ['All', 'PDiskData', 'UserData'],
1313
});
1414

1515
export const _NEVER = Symbol();

0 commit comments

Comments
 (0)