Skip to content

Commit d9067b3

Browse files
authoredFeb 24, 2025
feat: add database to authentication process (#1976)
1 parent 249011d commit d9067b3

File tree

9 files changed

+121
-28
lines changed

9 files changed

+121
-28
lines changed
 

‎src/containers/App/Content.tsx

+28-9
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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 {AccessDenied} from '../../components/Errors/403';
78
import {PageError} from '../../components/Errors/PageError/PageError';
89
import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper';
910
import {useSlots} from '../../components/slots';
@@ -12,7 +13,11 @@ import type {SlotComponent} from '../../components/slots/types';
1213
import routes from '../../routes';
1314
import type {RootState} from '../../store';
1415
import {authenticationApi} from '../../store/reducers/authentication/authentication';
15-
import {useCapabilitiesLoaded, useCapabilitiesQuery} from '../../store/reducers/capabilities/hooks';
16+
import {
17+
useCapabilitiesLoaded,
18+
useCapabilitiesQuery,
19+
useClusterWithoutAuthInUI,
20+
} from '../../store/reducers/capabilities/hooks';
1621
import {nodesListApi} from '../../store/reducers/nodesList';
1722
import {cn} from '../../utils/cn';
1823
import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery';
@@ -177,10 +182,14 @@ export function Content(props: ContentProps) {
177182

178183
function DataWrapper({children}: {children: React.ReactNode}) {
179184
return (
180-
<GetUser>
181-
<GetNodesList />
182-
<GetCapabilities>{children}</GetCapabilities>
183-
</GetUser>
185+
// capabilities is going to work without authentication, but not all running systems are supporting this yet
186+
<GetCapabilities>
187+
<GetUser>
188+
<GetNodesList />
189+
{/* this GetCapabilities will be removed */}
190+
<GetCapabilities>{children}</GetCapabilities>
191+
</GetUser>
192+
</GetCapabilities>
184193
);
185194
}
186195

@@ -219,15 +228,25 @@ interface ContentWrapperProps {
219228

220229
function ContentWrapper(props: ContentWrapperProps) {
221230
const {singleClusterMode, isAuthenticated} = props;
231+
const authUnavailable = useClusterWithoutAuthInUI();
232+
233+
const renderNotAuthenticated = () => {
234+
if (authUnavailable) {
235+
return <AccessDenied />;
236+
}
237+
return <Authentication />;
238+
};
222239

223240
return (
224241
<Switch>
225-
<Route path={routes.auth}>
226-
<Authentication closable />
227-
</Route>
242+
{!authUnavailable && (
243+
<Route path={routes.auth}>
244+
<Authentication closable />
245+
</Route>
246+
)}
228247
<Route>
229248
<div className={b({embedded: singleClusterMode})}>
230-
{isAuthenticated ? props.children : <Authentication />}
249+
{isAuthenticated ? props.children : renderNotAuthenticated()}
231250
</div>
232251
</Route>
233252
</Switch>

‎src/containers/AsideNavigation/YdbInternalUser/YdbInternalUser.tsx

+20-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import {useHistory} from 'react-router-dom';
44

55
import routes, {createHref} from '../../../routes';
66
import {authenticationApi} from '../../../store/reducers/authentication/authentication';
7+
import {useClusterWithoutAuthInUI} from '../../../store/reducers/capabilities/hooks';
78
import {cn} from '../../../utils/cn';
9+
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
810
import i18n from '../i18n';
911

1012
import './YdbInternalUser.scss';
@@ -13,18 +15,34 @@ const b = cn('kv-ydb-internal-user');
1315

1416
export function YdbInternalUser({login}: {login?: string}) {
1517
const [logout] = authenticationApi.useLogoutMutation();
18+
const authUnavailable = useClusterWithoutAuthInUI();
19+
const database = useDatabaseFromQuery();
1620

1721
const history = useHistory();
1822
const handleLoginClick = () => {
1923
history.push(
20-
createHref(routes.auth, undefined, {returnUrl: encodeURIComponent(location.href)}),
24+
createHref(routes.auth, undefined, {
25+
returnUrl: encodeURIComponent(location.href),
26+
database,
27+
}),
2128
);
2229
};
2330

2431
const handleLogout = () => {
2532
logout(undefined);
2633
};
2734

35+
const renderLoginButton = () => {
36+
if (authUnavailable) {
37+
return null;
38+
}
39+
return (
40+
<Button view="flat-secondary" title={i18n('account.login')} onClick={handleLoginClick}>
41+
<Icon data={ArrowRightToSquare} />
42+
</Button>
43+
);
44+
};
45+
2846
return (
2947
<div className={b()}>
3048
<div className={b('user-info-wrapper')}>
@@ -36,13 +54,7 @@ export function YdbInternalUser({login}: {login?: string}) {
3654
<Icon data={ArrowRightFromSquare} />
3755
</Button>
3856
) : (
39-
<Button
40-
view="flat-secondary"
41-
title={i18n('account.login')}
42-
onClick={handleLoginClick}
43-
>
44-
<Icon data={ArrowRightToSquare} />
45-
</Button>
57+
renderLoginButton()
4658
)}
4759
</div>
4860
);

‎src/containers/Authentication/Authentication.tsx

+27-3
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ import {useHistory, useLocation} from 'react-router-dom';
66

77
import {parseQuery} from '../../routes';
88
import {authenticationApi} from '../../store/reducers/authentication/authentication';
9+
import {useLoginWithDatabase} from '../../store/reducers/capabilities/hooks';
910
import {cn} from '../../utils/cn';
1011

11-
import {isPasswordError, isUserError} from './utils';
12+
import {isDatabaseError, isPasswordError, isUserError} from './utils';
1213

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

@@ -24,28 +25,36 @@ function Authentication({closable = false}: AuthenticationProps) {
2425
const history = useHistory();
2526
const location = useLocation();
2627

28+
const needDatabase = useLoginWithDatabase();
29+
2730
const [authenticate, {isLoading}] = authenticationApi.useAuthenticateMutation(undefined);
2831

29-
const {returnUrl} = parseQuery(location);
32+
const {returnUrl, database: databaseFromQuery} = parseQuery(location);
3033

3134
const [login, setLogin] = React.useState('');
35+
const [database, setDatabase] = React.useState(databaseFromQuery?.toString() ?? '');
3236
const [password, setPass] = React.useState('');
3337
const [loginError, setLoginError] = React.useState('');
3438
const [passwordError, setPasswordError] = React.useState('');
39+
const [databaseError, setDatabaseError] = React.useState('');
3540
const [showPassword, setShowPassword] = React.useState(false);
3641

3742
const onLoginUpdate = (value: string) => {
3843
setLogin(value);
3944
setLoginError('');
4045
};
46+
const onDatabaseUpdate = (value: string) => {
47+
setDatabase(value);
48+
setDatabaseError('');
49+
};
4150

4251
const onPassUpdate = (value: string) => {
4352
setPass(value);
4453
setPasswordError('');
4554
};
4655

4756
const onLoginClick = () => {
48-
authenticate({user: login, password})
57+
authenticate({user: login, password, database})
4958
.unwrap()
5059
.then(() => {
5160
if (returnUrl) {
@@ -66,6 +75,9 @@ function Authentication({closable = false}: AuthenticationProps) {
6675
if (isPasswordError(error)) {
6776
setPasswordError(error.data.error);
6877
}
78+
if (isDatabaseError(error)) {
79+
setDatabaseError(error.data.error);
80+
}
6981
});
7082
};
7183

@@ -125,6 +137,18 @@ function Authentication({closable = false}: AuthenticationProps) {
125137
<Icon data={showPassword ? EyeSlash : Eye} size={16} />
126138
</Button>
127139
</div>
140+
{needDatabase && (
141+
<div className={b('field-wrapper')}>
142+
<TextInput
143+
value={database}
144+
onUpdate={onDatabaseUpdate}
145+
placeholder={'Database'}
146+
error={databaseError}
147+
onKeyDown={onEnterClick}
148+
size="l"
149+
/>
150+
</div>
151+
)}
128152
<Button
129153
view="action"
130154
onClick={onLoginClick}

‎src/containers/Authentication/utils.ts

+3
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ export function isUserError(error: unknown): error is AuthError {
2222
export function isPasswordError(error: unknown): error is AuthError {
2323
return isAuthError(error) && error.data.error.includes('password');
2424
}
25+
export function isDatabaseError(error: unknown): error is AuthError {
26+
return isAuthError(error) && error.data.error.includes('database');
27+
}

‎src/services/api/auth.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {BaseYdbAPI} from './base';
22

33
export class AuthAPI extends BaseYdbAPI {
4-
authenticate(params: {user: string; password: string}) {
4+
authenticate(params: {user: string; password: string; database?: string}) {
55
return this.post(this.getPath('/login'), params, {});
66
}
77

‎src/store/reducers/authentication/authentication.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ export const authenticationApi = api.injectEndpoints({
6565
providesTags: ['UserData'],
6666
}),
6767
authenticate: build.mutation({
68-
queryFn: async (params: {user: string; password: string}, {dispatch}) => {
68+
queryFn: async (
69+
params: {user: string; password: string; database?: string},
70+
{dispatch},
71+
) => {
6972
try {
7073
const data = await window.api.auth.authenticate(params);
7174
dispatch(setIsAuthenticated(true));

‎src/store/reducers/capabilities/capabilities.ts

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {createSelector} from '@reduxjs/toolkit';
22

3-
import type {Capability} from '../../../types/api/capabilities';
3+
import type {Capability, SecuritySetting} from '../../../types/api/capabilities';
44
import type {AppDispatch, RootState} from '../../defaultStore';
55

66
import {api} from './../api';
@@ -38,8 +38,16 @@ export const selectCapabilityVersion = createSelector(
3838
(state: RootState) => state,
3939
(_state: RootState, capability: Capability) => capability,
4040
(_state: RootState, _capability: Capability, database?: string) => database,
41-
(state, capability, database) =>
42-
selectDatabaseCapabilities(state, database).data?.Capabilities?.[capability],
41+
(state, capability, database) => {
42+
return selectDatabaseCapabilities(state, database).data?.Capabilities?.[capability];
43+
},
44+
);
45+
export const selectSecuritySetting = createSelector(
46+
(state: RootState) => state,
47+
(_state: RootState, setting: SecuritySetting) => setting,
48+
(_state: RootState, _setting: SecuritySetting, database?: string) => database,
49+
(state, setting, database) =>
50+
selectDatabaseCapabilities(state, database).data?.Settings?.Security?.[setting],
4351
);
4452

4553
export async function queryCapability(
@@ -50,5 +58,5 @@ export async function queryCapability(
5058
const thunk = capabilitiesApi.util.getRunningQueryThunk('getClusterCapabilities', {database});
5159
await dispatch(thunk);
5260

53-
return selectCapabilityVersion(getState(), capability) || 0;
61+
return selectCapabilityVersion(getState(), capability, database) || 0;
5462
}

‎src/store/reducers/capabilities/hooks.ts

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
import type {Capability} from '../../../types/api/capabilities';
1+
import type {Capability, SecuritySetting} from '../../../types/api/capabilities';
22
import {useTypedSelector} from '../../../utils/hooks';
33
import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery';
44

5-
import {capabilitiesApi, selectCapabilityVersion, selectDatabaseCapabilities} from './capabilities';
5+
import {
6+
capabilitiesApi,
7+
selectCapabilityVersion,
8+
selectDatabaseCapabilities,
9+
selectSecuritySetting,
10+
} from './capabilities';
611

712
export function useCapabilitiesQuery() {
813
const database = useDatabaseFromQuery();
@@ -68,3 +73,17 @@ export const useClusterDashboardAvailable = () => {
6873
export const useStreamingAvailable = () => {
6974
return useGetFeatureVersion('/viewer/query') >= 7;
7075
};
76+
77+
const useGetSecuritySetting = (feature: SecuritySetting) => {
78+
const database = useDatabaseFromQuery();
79+
80+
return useTypedSelector((state) => selectSecuritySetting(state, feature, database));
81+
};
82+
83+
export const useClusterWithoutAuthInUI = () => {
84+
return useGetSecuritySetting('UseLoginProvider') === false;
85+
};
86+
87+
export const useLoginWithDatabase = () => {
88+
return useGetSecuritySetting('DomainLoginOnly') === false;
89+
};

‎src/types/api/capabilities.ts

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
*/
44
export interface CapabilitiesResponse {
55
Capabilities: Record<Partial<Capability>, number>;
6+
Settings?: {
7+
Security?: Record<Partial<SecuritySetting>, boolean>;
8+
};
69
}
710

811
// Add feature name before using it
@@ -14,3 +17,5 @@ export type Capability =
1417
| '/viewer/feature_flags'
1518
| '/viewer/cluster'
1619
| '/viewer/nodes';
20+
21+
export type SecuritySetting = 'UseLoginProvider' | 'DomainLoginOnly';

0 commit comments

Comments
 (0)