diff --git a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx deleted file mode 100644 index c22002a66..000000000 --- a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import clsx from 'clsx'; -import './style.scss'; -import { Icon } from '../../../../../shared/components/Icon'; -import { RadioIndicator } from '../../../../../shared/components/RadioIndicator/RadioIndicator'; -import { mfaMethodIcon } from '../../../../../shared/consts'; -import { MfaMethod, type MfaMethodValue } from '../../../../../shared/rust-api/types'; -import { - mfaToText, - type OpenIdInstance, - openIdProviderName, -} from '../../../../../shared/utils/mfa'; - -type FactorText = { - title: string; - description: string; -}; - -const factorText = (method: MfaMethodValue, instance?: OpenIdInstance): FactorText => { - switch (method) { - case MfaMethod.Totp: - return { - title: 'Security code from your authenticator app', - description: `Use an authenticator app on your phone, such as Google Authenticator, Microsoft Authenticator, or Authy, to generate a temporary code and verify your identity.`, - }; - case MfaMethod.Email: - return { - title: `Code from email`, - description: `Enter the temporary security code sent to your email to verify your identity and continue.`, - }; - case MfaMethod.Oidc: - return { - title: `Verify with ${openIdProviderName(instance)}`, - description: `Sign in through your identity provider in the browser to verify your identity.`, - }; - case MfaMethod.Biometric: - return { title: '', description: '' }; - case MfaMethod.MobileApprove: - return { title: '', description: '' }; - case MfaMethod.Fido2: - return { - title: mfaToText(MfaMethod.Fido2), - description: `Verify your identity using a registered security key, such as a YubiKey.`, - }; - } -}; - -interface Props { - mfaMethod: MfaMethodValue; - instance?: OpenIdInstance; - selected: boolean; - disabled?: boolean; - onClick: () => void; -} - -export const ConfigureMfaVerificatorFactorSelector = ({ - mfaMethod, - instance, - selected, - disabled = false, - onClick, -}: Props) => { - const { title, description } = factorText(mfaMethod, instance); - - return ( -
{ - if (!disabled) onClick(); - }} - > -
-
- -
-
-

{title}

-

{description}

-
-
- -
-
-
- ); -}; diff --git a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss b/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss deleted file mode 100644 index 729a1cc7f..000000000 --- a/new-ui/src/pages/full/ConfigureMfaPage/components/ConfigureMfaVerificatorFactorSelector/style.scss +++ /dev/null @@ -1,55 +0,0 @@ -.mfa-verification-factor-selector { - --border-color: var(--border-disabled); - - border: 1px solid var(--border-color); - border-radius: 12px; - box-sizing: border-box; - padding: var(--spacing-lg) var(--spacing-md); - transition: border-color 160ms ease-out; - cursor: pointer; - user-select: none; - - &.selected { - --border-color: var(--fg-white-100); - } - - &.disabled { - cursor: not-allowed; - opacity: 0.5; - } - - > .inner-grid { - display: grid; - grid-template-rows: 1fr; - grid-template-columns: 20px var(--spacing-lg) minmax(0, 1fr) var(--spacing-4xl) 24px; - - > .icon-col { - grid-row: 1; - grid-column: 1; - } - - > .content-col { - grid-row: 1; - grid-column: 3; - - > .title { - font: var(--t-body-sm-500); - color: var(--fg-white-100); - padding-bottom: var(--spacing-xs); - } - - > .description { - font: var(--t-body-xs-400); - color: var(--fg-white-70); - } - } - - > .indicator-col { - grid-row: 1; - grid-column: 5; - display: flex; - align-items: center; - justify-content: center; - } - } -} diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx index b102767d3..7b2532e48 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/ConfigureSelectVerificationStep.tsx @@ -1,20 +1,15 @@ import { useMutation } from '@tanstack/react-query'; import { useNavigate } from '@tanstack/react-router'; -import { useState } from 'react'; import { Button } from '../../../../../shared/components/Button/Button'; import { ButtonVariant } from '../../../../../shared/components/Button/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; -import { ExternalProviderButton } from '../../../../../shared/components/ExternalProviderButton/ExternalProviderButton'; -import { FullPageTitle } from '../../../../../shared/components/FullPageTitle/FullPageTitle'; +import { Icon, IconKind } from '../../../../../shared/components/Icon'; +import { MfaSelector } from '../../../../../shared/components/LocationCard/components/MfaSelector/MfaSelector'; +import { SizedBox } from '../../../../../shared/components/SizedBox/SizedBox'; import { FullPage } from '../../../../../shared/layouts/FullPage/FullPage'; -import { ConfigureFactorsSource, MfaMethod } from '../../../../../shared/rust-api/types'; +import { ConfigureFactorsSource } from '../../../../../shared/rust-api/types'; +import { ThemeSpacing, ThemeVariable } from '../../../../../shared/types'; import { isPresent } from '../../../../../shared/utils/isPresent'; -import { - type ClientAuthorizingMethod, - findOpenIdProvider, - openIdProviderName, -} from '../../../../../shared/utils/mfa'; -import { ConfigureMfaVerificatorFactorSelector } from '../../components/ConfigureMfaVerificatorFactorSelector/ConfigureMfaVerificatorFactorSelector'; import { discardMfaConfiguration, useConfigureMfaStore, @@ -36,10 +31,6 @@ export const ConfigureSelectVerificationStep = ({ onCancel }: Props) => { ); const instance = useConfigureMfaStore((s) => s.instance) ?? undefined; - const [selected, setSelected] = useState( - methods[0], - ); - const { mutate: cancel, isPending: isCancelling } = useMutation({ // the source is read before the discard resets the store mutationFn: async () => { @@ -63,26 +54,34 @@ export const ConfigureSelectVerificationStep = ({ onCancel }: Props) => { hideScrollContainer withControls > - -

- {`For security, verify your identity using one of your existing MFA methods. You'll be able to add a new method after verification.`} -

- {isPresent(error) &&

{error}

} -
-

Select an existing method

-
-
- {methods.map((method) => ( - { - setSelected(method); - }} - /> - ))} +
+ + +

Verify your identity

+ +

+ To add a new MFA method, please select one of the available options to verify + your identity. +

+ + {isPresent(error) &&

{error}

} +
+ {methods.map((method) => ( + { + useConfigureMfaStore.getState().selectVerificationMethod(method); + }} + /> + ))} +
); diff --git a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss index 127a27029..9b2234e78 100644 --- a/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss +++ b/new-ui/src/pages/full/ConfigureMfaPage/verify/ConfigureSelectVerificationStep/style.scss @@ -1,18 +1,37 @@ #configure-select-verification-step { - .label { - padding-bottom: var(--spacing-md); + & > .content { + display: flex; + flex: 1 1 auto; + flex-flow: column; + align-items: center; + justify-content: center; - & > p { - font: var(--t-body-xs-500); + & > .title { + text-align: center; + font: var(--t-h4); color: var(--fg-white-100); } - } - .methods { - display: flex; - flex-flow: column; - row-gap: var(--spacing-md); - width: 100%; - padding-bottom: var(--spacing-xl); + & > .subtitle { + max-width: 380px; + text-align: center; + font: var(--t-body-sm-400); + color: var(--fg-white-70); + } + + & > .error { + padding-bottom: var(--spacing-xl); + text-align: center; + font: var(--t-body-sm-400); + color: var(--fg-critical); + } + + & > .methods { + display: flex; + flex-flow: column; + row-gap: var(--spacing-md); + width: 100%; + max-width: 296px; + } } } diff --git a/new-ui/src/shared/components/Icon/Icon.tsx b/new-ui/src/shared/components/Icon/Icon.tsx index 172dafde8..8e63878ab 100644 --- a/new-ui/src/shared/components/Icon/Icon.tsx +++ b/new-ui/src/shared/components/Icon/Icon.tsx @@ -130,6 +130,7 @@ import { IconUbuntu } from './icons/IconUbuntu'; import { IconUpload } from './icons/IconUpload'; import { IconUser } from './icons/IconUser'; import { IconUserActive } from './icons/IconUserActive'; +import { IconUserShield } from './icons/IconUserShield'; import { IconUsers } from './icons/IconUsers'; import { IconWarningFilled } from './icons/IconWarningFilled'; import { IconWarningOutlined } from './icons/IconWarningOutlined'; @@ -430,6 +431,8 @@ export const Icon = ({ return IconUser; case 'user-active': return IconUserActive; + case 'user-shield': + return IconUserShield; case 'users': return IconUsers; case 'webhooks': diff --git a/new-ui/src/shared/components/Icon/icon-types.ts b/new-ui/src/shared/components/Icon/icon-types.ts index d9b22cd2e..29097a15c 100644 --- a/new-ui/src/shared/components/Icon/icon-types.ts +++ b/new-ui/src/shared/components/Icon/icon-types.ts @@ -91,6 +91,7 @@ export const IconKind = { EmptyPoint: 'empty-point', DevicesActive: 'devices-active', UserActive: 'user-active', + UserShield: 'user-shield', Windows: 'windows', AppStore: 'app-store', Apple: 'apple', diff --git a/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx b/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx new file mode 100644 index 000000000..8ff0c20e4 --- /dev/null +++ b/new-ui/src/shared/components/Icon/icons/IconUserShield.tsx @@ -0,0 +1,31 @@ +import type { SVGProps } from 'react'; + +export const IconUserShield = (props: SVGProps) => { + return ( + + + + + + ); +}; diff --git a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx index b7b3bd3c8..aca37bea0 100644 --- a/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx +++ b/new-ui/src/shared/components/LocationCard/components/MfaSelector/MfaSelector.tsx @@ -8,7 +8,7 @@ import { type MfaMethodValue, } from '../../../../rust-api/types'; import { isPresent } from '../../../../utils/isPresent'; -import { findOpenIdProvider, mfaToText } from '../../../../utils/mfa'; +import { findOpenIdProvider, mfaToText, openIdProviderName } from '../../../../utils/mfa'; import { Icon } from '../../../Icon'; import checkboxSrc from './assets/checkbox.svg'; import { openIdProviderIcon } from './consts'; @@ -69,7 +69,11 @@ export const MfaSelector = ({ )}
-

{mfaToText(factor, instance)}

+

+ {factor === MfaMethod.Oidc + ? `Verify with ${openIdProviderName(instance)}` + : mfaToText(factor, instance)} +

{(isMobileOnly || !configured) && (