Skip to content

Commit

Permalink
feat(experience): implement totp experience flow (#4589)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun authored Oct 8, 2023
1 parent 3e45c38 commit f537788
Show file tree
Hide file tree
Showing 44 changed files with 988 additions and 78 deletions.
6 changes: 5 additions & 1 deletion packages/experience/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@testing-library/react": "^14.0.0",
"@types/color": "^3.0.3",
"@types/jest": "^29.4.0",
"@types/qrcode": "^1.5.2",
"@types/react": "^18.0.31",
"@types/react-dom": "^18.0.0",
"@types/react-helmet": "^6.1.6",
Expand Down Expand Up @@ -117,5 +118,8 @@
"stylelint": {
"extends": "@silverhand/eslint-config-react/.stylelintrc"
},
"prettier": "@silverhand/eslint-config/.prettierrc"
"prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": {
"qrcode": "^1.5.3"
}
}
25 changes: 25 additions & 0 deletions packages/experience/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { AppInsightsBoundary } from '@logto/app-insights/react';
import { MfaFactor } from '@logto/schemas';
import { Route, Routes, BrowserRouter } from 'react-router-dom';

import AppLayout from './Layout/AppLayout';
import AppBoundary from './Providers/AppBoundary';
import LoadingLayerProvider from './Providers/LoadingLayerProvider';
import PageContextProvider from './Providers/PageContextProvider';
import SettingsProvider from './Providers/SettingsProvider';
import { isDevelopmentFeaturesEnabled } from './constants/env';
import Callback from './pages/Callback';
import Consent from './pages/Consent';
import Continue from './pages/Continue';
import ErrorPage from './pages/ErrorPage';
import ForgotPassword from './pages/ForgotPassword';
import MfaBinding from './pages/MfaBinding';
import TotpBinding from './pages/MfaBinding/TotpBinding';
import MfaVerification from './pages/MfaVerification';
import TotpVerification from './pages/MfaVerification/TotpVerification';
import Register from './pages/Register';
import RegisterPassword from './pages/RegisterPassword';
import ResetPassword from './pages/ResetPassword';
Expand All @@ -21,6 +27,7 @@ import SocialLinkAccount from './pages/SocialLinkAccount';
import SocialSignIn from './pages/SocialSignInCallback';
import Springboard from './pages/Springboard';
import VerificationCode from './pages/VerificationCode';
import { UserMfaFlow } from './types';
import { handleSearchParametersData } from './utils/search-parameters';

import './scss/normalized.scss';
Expand Down Expand Up @@ -66,6 +73,24 @@ const App = () => {
{/* Passwordless verification code */}
<Route path=":flow/verification-code" element={<VerificationCode />} />

{isDevelopmentFeaturesEnabled && (
<>
{/* Mfa binding */}
{/* Todo @xiaoyijun reorg these routes when factors are all implemented */}
<Route path={UserMfaFlow.MfaBinding}>
<Route index element={<MfaBinding />} />
<Route path={MfaFactor.TOTP} element={<TotpBinding />} />
</Route>

{/* Mfa verification */}
{/* Todo @xiaoyijun reorg these routes when factors are all implemented */}
<Route path={UserMfaFlow.MfaVerification}>
<Route index element={<MfaVerification />} />
<Route path={MfaFactor.TOTP} element={<TotpVerification />} />
</Route>
</>
)}

{/* Continue set up missing profile */}
<Route path="continue">
<Route path=":method" element={<Continue />} />
Expand Down
11 changes: 11 additions & 0 deletions packages/experience/src/Layout/SectionLayout/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@use '@/scss/underscore' as _;

.title {
font: var(--font-title-3);
}

.description {
font: var(--font-body-2);
color: var(--color-type-secondary);
margin-top: _.unit(1);
}
30 changes: 30 additions & 0 deletions packages/experience/src/Layout/SectionLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { type TFuncKey } from 'i18next';
import { type ReactNode } from 'react';

import DynamicT from '@/components/DynamicT';

import * as styles from './index.module.scss';

type Props = {
title: TFuncKey;
description: TFuncKey;
titleProps?: Record<string, unknown>;
descriptionProps?: Record<string, unknown>;
children: ReactNode;
};

const SectionLayout = ({ title, description, titleProps, descriptionProps, children }: Props) => {
return (
<div>
<div className={styles.title}>
<DynamicT forKey={title} interpolation={titleProps} />
</div>
<div className={styles.description}>
<DynamicT forKey={description} interpolation={descriptionProps} />
</div>
{children}
</div>
);
};

export default SectionLayout;
17 changes: 17 additions & 0 deletions packages/experience/src/apis/interaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
SocialConnectorPayload,
SocialEmailPayload,
SocialPhonePayload,
BindMfaPayload,
VerifyMfaPayload,
} from '@logto/schemas';
import { conditional } from '@silverhand/essentials';

Expand Down Expand Up @@ -222,3 +224,18 @@ export const linkWithSocial = async (connectorId: string) => {

return api.post(`${interactionPrefix}/submit`).json<Response>();
};

export const createTotpSecret = async () =>
api.post(`${interactionPrefix}/${verificationPath}/totp`).json<{ secret: string }>();

export const bindMfa = async (payload: BindMfaPayload) => {
await api.put(`${interactionPrefix}/bind-mfa`, { json: payload });

return api.post(`${interactionPrefix}/submit`).json<Response>();
};

export const verifyMfa = async (payload: VerifyMfaPayload) => {
await api.put(`${interactionPrefix}/mfa`, { json: payload });

return api.post(`${interactionPrefix}/submit`).json<Response>();
};
3 changes: 3 additions & 0 deletions packages/experience/src/assets/icons/factor-backup-code.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions packages/experience/src/assets/icons/factor-totp.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions packages/experience/src/assets/icons/factor-webauthn.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@use '@/scss/underscore' as _;

.mfaFactorButton {
padding: _.unit(3) _.unit(4) _.unit(3) _.unit(3);
height: unset;
gap: _.unit(4);
border-radius: 12px;
}

.icon {
width: 20px;
height: 20px;
color: var(--color-type-secondary);
}

.title {
flex: 1;
@include _.flex-column;
align-items: flex-start;

.name {
font: var(--font-body-1);
}

.description {
font: var(--font-body-2);
color: var(--color-type-secondary);
}
}
73 changes: 73 additions & 0 deletions packages/experience/src/components/Button/MfaFactorButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { MfaFactor } from '@logto/schemas';
import classNames from 'classnames';
import { type TFuncKey } from 'i18next';

import ArrowNext from '@/assets/icons/arrow-next.svg';
import FactorBackupCode from '@/assets/icons/factor-backup-code.svg';
import FactorTotp from '@/assets/icons/factor-totp.svg';
import FactorWebAuthn from '@/assets/icons/factor-webauthn.svg';

import DynamicT from '../DynamicT';

import * as mfaFactorButtonStyles from './MfaFactorButton.module.scss';
import * as styles from './index.module.scss';

export type Props = {
factor: MfaFactor;
isBinding: boolean;
onClick?: () => void;
};

const factorIcon: Record<MfaFactor, SvgComponent> = {
[MfaFactor.TOTP]: FactorTotp,
[MfaFactor.WebAuthn]: FactorWebAuthn,
[MfaFactor.BackupCode]: FactorBackupCode,
};

const factorName: Record<MfaFactor, TFuncKey> = {
[MfaFactor.TOTP]: 'mfa.totp',
[MfaFactor.WebAuthn]: 'mfa.webauthn',
[MfaFactor.BackupCode]: 'mfa.backup_code',
};

const factorDescription: Record<MfaFactor, TFuncKey> = {
[MfaFactor.TOTP]: 'mfa.verify_totp_description',
[MfaFactor.WebAuthn]: 'mfa.verify_webauthn_description',
[MfaFactor.BackupCode]: 'mfa.verify_backup_code_description',
};

const linkFactorDescription: Record<MfaFactor, TFuncKey> = {
[MfaFactor.TOTP]: 'mfa.link_totp_description',
[MfaFactor.WebAuthn]: 'mfa.link_webauthn_description',
[MfaFactor.BackupCode]: 'mfa.link_backup_code_description',
};

const MfaFactorButton = ({ factor, isBinding, onClick }: Props) => {
const Icon = factorIcon[factor];

return (
<button
className={classNames(
styles.button,
styles.secondary,
styles.large,
mfaFactorButtonStyles.mfaFactorButton
)}
type="button"
onClick={onClick}
>
<Icon className={mfaFactorButtonStyles.icon} />
<div className={mfaFactorButtonStyles.title}>
<div className={mfaFactorButtonStyles.name}>
<DynamicT forKey={factorName[factor]} />
</div>
<div className={mfaFactorButtonStyles.description}>
<DynamicT forKey={(isBinding ? linkFactorDescription : factorDescription)[factor]} />
</div>
</div>
<ArrowNext className={mfaFactorButtonStyles.icon} />
</button>
);
};

export default MfaFactorButton;
8 changes: 6 additions & 2 deletions packages/experience/src/components/Divider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ const Divider = ({ className, label }: Props) => {
return (
<div className={classNames(styles.divider, className)}>
<i className={styles.line} />
{label && <DynamicT forKey={label} />}
<i className={styles.line} />
{label && (
<>
<DynamicT forKey={label} />
<i className={styles.line} />
</>
)}
</div>
);
};
Expand Down
4 changes: 4 additions & 0 deletions packages/experience/src/constants/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { yes } from '@silverhand/essentials';

export const isDevelopmentFeaturesEnabled =
yes(process.env.DEV_FEATURES_ENABLED) || yes(process.env.INTEGRATION_TEST);
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@use '@/scss/underscore' as _;

.factorList {
@include _.flex-column;
gap: _.unit(3);
}
Loading

0 comments on commit f537788

Please sign in to comment.