Skip to content

Commit

Permalink
feat(ManageTokens): optional password for token issuing
Browse files Browse the repository at this point in the history
  • Loading branch information
vitshev committed Dec 18, 2024
1 parent b7d8c8f commit e696da8
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {getSettingsCluster} from '../../store/selectors/global';
import {importManageTokens} from '../ManageTokens';

import './AppNavigationComponent.scss';
import {getAllowManageTokens} from '../../store/selectors/manage-tokens';

const block = cn('yt-app-navigation');

Expand Down Expand Up @@ -56,6 +57,7 @@ function AppNavigationComponent({

const [popupVisible, setPopupVisible] = useState(false);
const settingsCluster = useSelector(getSettingsCluster);
const isManageTokensMenuVisible = useSelector(getAllowManageTokens);
const history = useHistory();

let showUserIcon = Boolean(currentUser);
Expand Down Expand Up @@ -158,29 +160,29 @@ function AppNavigationComponent({
<div className={block('settings-ul')}>
<Menu>
{authWay === 'passwd' && (
<>
<Menu.Item
onClick={() =>
history.push(
`/${YT.cluster}/change-password`,
)
}
>
Change password
</Menu.Item>
<Menu.Item
onClick={() =>
importManageTokens().then((actions) => {
dispatch(
actions.openManageTokensModal(),
);
setPopupVisible(false);
})
}
>
Manage tokens
</Menu.Item>
</>
<Menu.Item
onClick={() =>
history.push(
`/${YT.cluster}/change-password`,
)
}
>
Change password
</Menu.Item>
)}
{isManageTokensMenuVisible && (
<Menu.Item
onClick={() =>
importManageTokens().then((actions) => {
dispatch(
actions.openManageTokensModal(),
);
setPopupVisible(false);
})
}
>
Manage tokens
</Menu.Item>
)}
<Menu.Item href={'/api/yt/logout'}>Logout</Menu.Item>
</Menu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@ import {
manageTokensRevokeToken,
} from '../../../store/actions/manage-tokens';
import {useThunkDispatch} from '../../../store/thunkDispatch';
import {useSelector} from 'react-redux';
import {useDispatch, useSelector} from 'react-redux';
import {AuthenticationToken, manageTokensSelector} from '../../../store/selectors/manage-tokens';
import {getCurrentUserName} from '../../../store/selectors/global';
import Icon from '../../../components/Icon/Icon';
import {YTError} from '../../../../@types/types';
import ClipboardButton from '../../../components/ClipboardButton/ClipboardButton';
import DataTableYT from '../../../components/DataTableYT/DataTableYT';
import {sha256} from '../../../utils/sha256';
import {
ManageTokensPasswordModalContextProvider,
useManageTokensPasswordModalContext,
Expand Down Expand Up @@ -138,70 +137,6 @@ const AuthenticationGenerateTokenFormSection: FC<{onClose: () => void}> = ({onCl
);
};

const AuthenticationPasswordSection: FC<{onSuccess: () => void}> = ({onSuccess}) => {
const [error, setError] = useState<YTError>();
type FormData = {password: string};
const dispatch = useThunkDispatch();
const user = useSelector(getCurrentUserName);
const handleSubmit = async (form: FormApi<FormData>) => {
const values = form.getState().values;

setError(undefined);

await sha256(values.password).then((password_sha256) => {
return dispatch(
manageTokensGetList({
user,
password_sha256,
}),
)
.then(() => {
onSuccess();
})
.catch((error) => setError(error?.response?.data || error));
});
};

return (
<YTDFDialog<FormData>
headerProps={{
title: 'Authentication',
}}
pristineSubmittable
modal={false}
visible={true}
initialValues={{}}
onAdd={handleSubmit}
fields={[
{
name: 'error-block',
type: 'block',
extras: {
children: (
<Alert message="To access tokens management, you need enter your password" />
),
},
},
{
name: 'password',
type: 'text',
required: true,
caption: 'Password',
extras: () => ({type: 'password'}),
},
...makeErrorFields([error]),
]}
footerProps={{
propsButtonCancel: {
onClick: () => {
dispatch(closeManageTokensModal());
},
},
}}
/>
);
};

const RevokeToken = (props: {handleClickRemoveToken: (index: number) => void; index: number}) => {
const [visible, setVisible] = useState(false);

Expand Down Expand Up @@ -238,10 +173,11 @@ const RevokeToken = (props: {handleClickRemoveToken: (index: number) => void; in
const AuthenticationTokensSection: FC<{
onClickGenerateTokenButton: () => void;
onSuccessRemove: () => void;
}> = ({onSuccessRemove, onClickGenerateTokenButton}) => {
passwordSha256?: string;
}> = ({onSuccessRemove, onClickGenerateTokenButton, passwordSha256}) => {
const {getPassword} = useManageTokensPasswordModalContext();
const dispatch = useThunkDispatch();
const tokens = useSelector(manageTokensSelector)!;
const tokens = useSelector(manageTokensSelector);
const user = useSelector(getCurrentUserName);

const handleClickRemoveToken = React.useCallback(
Expand All @@ -268,6 +204,15 @@ const AuthenticationTokensSection: FC<{
[getPassword, user, onSuccessRemove, tokens, manageTokensRevokeToken, dispatch],
);

React.useEffect(() => {
dispatch(
manageTokensGetList({
user,
password_sha256: passwordSha256,
}),
);
}, [passwordSha256, getPassword, user, dispatch]);

return (
<div className={block('tokens')}>
<h2>Authentication Tokens</h2>
Expand Down Expand Up @@ -373,26 +318,23 @@ enum ViewSection {
generate,
}

const useViewSectionState = () => {
const [section, setSection] = useState<ViewSection>(ViewSection.default);
const useViewSectionState = (defaultState: ViewSection) => {
const [section, setSection] = useState<ViewSection>(defaultState);

return {section, setSection};
};

export const ManageTokensModalContent = () => {
const {section, setSection} = useViewSectionState();
export const ManageTokensModalSections: React.FC<{passwordSha256?: string}> = ({
passwordSha256,
}) => {
const {section, setSection} = useViewSectionState(ViewSection.tokens);

const content = useMemo(() => {
switch (section) {
case ViewSection.default:
return (
<AuthenticationPasswordSection
onSuccess={() => setSection(ViewSection.tokens)}
/>
);
case ViewSection.tokens:
return (
<AuthenticationTokensSection
passwordSha256={passwordSha256}
onSuccessRemove={() => setSection(ViewSection.tokens)}
onClickGenerateTokenButton={() => setSection(ViewSection.generate)}
/>
Expand All @@ -414,3 +356,35 @@ export const ManageTokensModalContent = () => {
</ManageTokensPasswordModalContextProvider>
);
};

const ManageTokenPasswordGuard = () => {
const {getPassword} = useManageTokensPasswordModalContext();
const dispatch = useDispatch();
const [passwordSha256, setPassword] = React.useState<string | undefined>();
const [open, setOpen] = React.useState(false);

React.useEffect(() => {
getPassword()
.then((passwordSha256) => {
setPassword(passwordSha256);
setOpen(true);
})
.catch(() => {
dispatch(closeManageTokensModal());
});
}, [getPassword]);

if (!open) {
return null;
}

return <ManageTokensModalSections passwordSha256={passwordSha256} />;
};

export const ManageTokensModalContent = () => {
return (
<ManageTokensPasswordModalContextProvider>
<ManageTokenPasswordGuard />
</ManageTokensPasswordModalContextProvider>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import {sha256} from '../../../utils/sha256';
import {YTDFDialog, makeErrorFields} from '../../../components/Dialog';
import {getCurrentUserName, getSettingsCluster} from '../../../store/selectors/global';
import {YTError} from '../../../../@types/types';
import {isManageTokensInOAuthMode} from '../../../store/selectors/manage-tokens';

interface ManageTokensPasswordModalContextValue {
getPassword: () => Promise<string>;
getPassword: () => Promise<string | undefined>;
}
export const ManageTokensPasswordModalContext = React.createContext<
undefined | ManageTokensPasswordModalContextValue
Expand Down Expand Up @@ -110,17 +111,22 @@ class PromiseWaiter<Data> {
}

export function ManageTokensPasswordModalContextProvider({children}: {children: React.ReactChild}) {
const isOAuth = useSelector(isManageTokensInOAuthMode);
const [visible, setVisible] = React.useState(false);
const p = React.useRef(new PromiseWaiter<string>());
const value = React.useMemo(() => {
return {
getPassword: () => {
if (isOAuth) {
return Promise.resolve(undefined);
}

setVisible(true);

return p.current.create();
},
};
}, [setVisible]);
}, [setVisible, isOAuth]);

const handleCancel = () => {
setVisible(false);
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ui/store/actions/manage-tokens/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function getQTApiSetup(): {proxy?: string} {

type Credentials = {
user: string;
password_sha256: string;
password_sha256?: string;
};
export const manageTokensGetList = (
credentials: Credentials,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface SupportedFeaturesState {
erasure_codecs?: Array<string>;
primitive_types?: Array<string>;
operation_statistics_descriptions?: Record<string, OperationStatisticInfo>;
require_password_in_authentication_commands?: boolean;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,8 @@ export const getOperationStatisticsDescription = createSelector(
};
},
);

export const getRequirePasswordInAuthenticationCommands = createSelector(
[getSupportedFeatures],
(features) => features.require_password_in_authentication_commands,
);
19 changes: 19 additions & 0 deletions packages/ui/src/ui/store/selectors/manage-tokens/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {createSelector} from 'reselect';
import {RootState} from '../../reducers';
import {getAuthWay} from '../global';
import {getRequirePasswordInAuthenticationCommands} from '../global/supported-features';

export type AuthenticationToken = {
tokenHash: string;
Expand Down Expand Up @@ -29,3 +31,20 @@ export const manageTokensSelector = createSelector(
);

export const isManageTokensModalOpened = (state: RootState) => state.manageTokens.modal.open;

export const isManageTokensInOAuthMode = createSelector(
[getAuthWay, getRequirePasswordInAuthenticationCommands],
(authWay, requirePasswordInAuthenticationCommands) => {
return authWay === 'oauth' && !requirePasswordInAuthenticationCommands;
},
);

export const getAllowManageTokens = (state: RootState) => {
const authWay = getAuthWay(state);

if (authWay === 'passwd') {
return true;
}

return isManageTokensInOAuthMode(state);
};

0 comments on commit e696da8

Please sign in to comment.