feat: improve 2fa styles based on feedback (#635)

* feat: improve 2fa styles based on feedback

* fix: preferences panes and dialogs electron compatibility

* fix: no horizontal line when opening two factor activation

* feat: improve two factor activation styles

* feat: further 2fa style improvements

* feat: padding 2fa widgets

* feat: add padding between QR code and content

* feat: refresh 2fa after passcode confirmation

* feat: don't autocomplete passwords for DecoratedInput
This commit is contained in:
Gorjan Petrovski
2021-09-17 18:14:53 +02:00
committed by GitHub
parent 9d85fbccc4
commit 8fb34f2e85
25 changed files with 494 additions and 228 deletions

View File

@@ -3,7 +3,7 @@ import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
import { Button } from '@/components/Button';
import { FunctionalComponent } from 'preact';
@@ -16,28 +16,30 @@ import { isEmailValid } from '@/utils';
enum SubmitButtonTitles {
Default = 'Continue',
GeneratingKeys = 'Generating Keys...',
Finish = 'Finish'
Finish = 'Finish',
}
enum Steps {
InitialStep,
FinishStep
FinishStep,
}
type Props = {
onCloseDialog: () => void;
application: WebApplication;
}
};
export const ChangeEmail: FunctionalComponent<Props> = ({
onCloseDialog,
application
application,
}) => {
const [currentPassword, setCurrentPassword] = useState('');
const [newEmail, setNewEmail] = useState('');
const [isContinuing, setIsContinuing] = useState(false);
const [lockContinue, setLockContinue] = useState(false);
const [submitButtonTitle, setSubmitButtonTitle] = useState(SubmitButtonTitles.Default);
const [submitButtonTitle, setSubmitButtonTitle] = useState(
SubmitButtonTitles.Default
);
const [currentStep, setCurrentStep] = useState(Steps.InitialStep);
useBeforeUnload();
@@ -46,9 +48,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
const validateCurrentPassword = async () => {
if (!currentPassword || currentPassword.length === 0) {
applicationAlertService.alert(
'Please enter your current password.'
);
applicationAlertService.alert('Please enter your current password.');
return false;
}
@@ -67,7 +67,9 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
const validateNewEmail = async () => {
if (!isEmailValid(newEmail)) {
applicationAlertService.alert('The email you entered has an invalid format. Please review your input and try again.');
applicationAlertService.alert(
'The email you entered has an invalid format. Please review your input and try again.'
);
return false;
}
@@ -85,10 +87,7 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
setLockContinue(true);
const response = await application.changeEmail(
newEmail,
currentPassword,
);
const response = await application.changeEmail(newEmail, currentPassword);
const success = !response.error;
@@ -121,7 +120,8 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
setIsContinuing(true);
setSubmitButtonTitle(SubmitButtonTitles.GeneratingKeys);
const valid = await validateCurrentPassword() && await validateNewEmail();
const valid =
(await validateCurrentPassword()) && (await validateNewEmail());
if (!valid) {
resetProgressState();
@@ -169,15 +169,15 @@ export const ChangeEmail: FunctionalComponent<Props> = ({
<ModalDialogButtons>
{currentStep === Steps.InitialStep && (
<Button
className='min-w-20'
type='normal'
label='Cancel'
className="min-w-20"
type="normal"
label="Cancel"
onClick={handleDialogClose}
/>
)}
<Button
className='min-w-20'
type='primary'
className="min-w-20"
type="primary"
label={submitButtonTitle}
onClick={handleSubmit}
/>

View File

@@ -1,18 +1,21 @@
import { Icon, IconType } from '@/components/Icon';
import { IconButton } from '@/components/IconButton';
import {
Disclosure,
DisclosureButton,
DisclosurePanel,
} from '@reach/disclosure';
import { FunctionComponent } from 'preact';
import { useState, useRef, useEffect } from 'react';
import { useState, useRef, useEffect, MouseEventHandler } from 'react';
const DisclosureIconButton: FunctionComponent<{
className?: string;
icon: IconType;
}> = ({ className = '', icon }) => (
onMouseEnter?: MouseEventHandler;
onMouseLeave?: MouseEventHandler;
}> = ({ className = '', icon, onMouseEnter, onMouseLeave }) => (
<DisclosureButton
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${
className ?? ''
}`}
@@ -29,11 +32,12 @@ const DisclosureIconButton: FunctionComponent<{
* @returns
*/
export const AuthAppInfoTooltip: FunctionComponent = () => {
const [isShown, setShown] = useState(false);
const [isClicked, setClicked] = useState(false);
const [isHover, setHover] = useState(false);
const ref = useRef(null);
useEffect(() => {
const dismiss = () => setShown(false);
const dismiss = () => setClicked(false);
document.addEventListener('mousedown', dismiss);
return () => {
document.removeEventListener('mousedown', dismiss);
@@ -41,9 +45,17 @@ export const AuthAppInfoTooltip: FunctionComponent = () => {
}, [ref]);
return (
<Disclosure open={isShown} onChange={() => setShown(!isShown)}>
<Disclosure
open={isClicked || isHover}
onChange={() => setClicked(!isClicked)}
>
<div className="relative">
<DisclosureIconButton icon="info" className="mt-1" />
<DisclosureIconButton
icon="info"
className="mt-1"
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
/>
<DisclosurePanel>
<div
className={`bg-black color-white text-center rounded shadow-overlay

View File

@@ -0,0 +1,7 @@
import { FunctionComponent } from 'preact';
export const Bullet: FunctionComponent<{ className?: string }> = ({
className = '',
}) => (
<div className={`min-w-1 min-h-1 rounded-full bg-black ${className} mr-2`} />
);

View File

@@ -0,0 +1,23 @@
import { FunctionComponent } from 'preact';
import { IconButton } from '../../../components/IconButton';
import { useState } from 'preact/hooks';
export const CopyButton: FunctionComponent<{ copyValue: string }> = ({
copyValue: secretKey,
}) => {
const [isCopied, setCopied] = useState(false);
return (
<IconButton
focusable={false}
title="Copy to clipboard"
icon={isCopied ? 'check' : 'copy'}
className={isCopied ? 'success' : undefined}
onClick={() => {
navigator?.clipboard?.writeText(secretKey);
setCopied(() => true);
}}
/>
);
};

View File

@@ -3,13 +3,15 @@ import { DecoratedInput } from '@/components/DecoratedInput';
import { IconButton } from '@/components/IconButton';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { CopyButton } from './CopyButton';
import { Bullet } from './Bullet';
import { downloadSecretKey } from './download-secret-key';
import { TwoFactorActivation } from './TwoFactorActivation';
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
export const SaveSecretKey: FunctionComponent<{
@@ -17,20 +19,14 @@ export const SaveSecretKey: FunctionComponent<{
}> = observer(({ activation: act }) => {
const download = (
<IconButton
focusable={false}
title="Download"
icon="download"
onClick={() => {
downloadSecretKey(act.secretKey);
}}
/>
);
const copy = (
<IconButton
icon="copy"
onClick={() => {
navigator?.clipboard?.writeText(act.secretKey);
}}
/>
);
return (
<ModalDialog>
<ModalDialogLabel
@@ -40,11 +36,13 @@ export const SaveSecretKey: FunctionComponent<{
>
Step 2 of 3 - Save secret key
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex-grow flex flex-col gap-2">
<div className="flex flex-row items-center gap-1">
<ModalDialogDescription className="h-33">
<div className="flex-grow flex flex-col">
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
<b>Save your secret key</b>{' '}
<b>Save your secret key</b>{' '}
<a
target="_blank"
href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key"
@@ -53,21 +51,27 @@ export const SaveSecretKey: FunctionComponent<{
</a>
:
</div>
<div className="min-w-2" />
<DecoratedInput
disabled={true}
right={[copy, download]}
right={[<CopyButton copyValue={act.secretKey} />, download]}
text={act.secretKey}
/>
</div>
<div className="text-sm">
You can use this key to generate codes if you lose access to your
authenticator app.{' '}
<a
target="_blank"
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
>
Learn more
</a>
<div className="h-2" />
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
You can use this key to generate codes if you lose access to your
authenticator app.{' '}
<a
target="_blank"
href="https://standardnotes.com/help/22/what-happens-if-i-lose-my-2fa-device-and-my-secret-key"
>
Learn more
</a>
</div>
</div>
</div>
</ModalDialogDescription>

View File

@@ -4,7 +4,6 @@ import { observer } from 'mobx-react-lite';
import QRCode from 'qrcode.react';
import { DecoratedInput } from '@/components/DecoratedInput';
import { IconButton } from '@/components/IconButton';
import { Button } from '@/components/Button';
import { TwoFactorActivation } from './TwoFactorActivation';
import { AuthAppInfoTooltip } from './AuthAppInfoPopup';
@@ -12,54 +11,49 @@ import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
import { CopyButton } from './CopyButton';
import { Bullet } from './Bullet';
export const ScanQRCode: FunctionComponent<{
activation: TwoFactorActivation;
}> = observer(({ activation: act }) => {
const copy = (
<IconButton
icon="copy"
onClick={() => {
navigator?.clipboard?.writeText(act.secretKey);
}}
/>
);
return (
<ModalDialog>
<ModalDialogLabel
closeDialog={() => {
act.cancelActivation();
}}
>
<ModalDialogLabel closeDialog={act.cancelActivation}>
Step 1 of 3 - Scan QR code
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex flex-row gap-3 items-center">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode value={act.qrCode} size={100} />
</div>
<div className="flex-grow flex flex-col gap-2">
<div className="flex flex-row gap-1 items-center">
<div className="text-sm">
Open your <b>authenticator app</b>.
</div>
<AuthAppInfoTooltip />
<ModalDialogDescription className="h-33">
<div className="w-25 h-25 flex items-center justify-center bg-info">
<QRCode value={act.qrCode} size={100} />
</div>
<div className="min-w-5" />
<div className="flex-grow flex flex-col">
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Open your <b>authenticator app</b>.
</div>
<div className="flex flex-row items-center">
<div className="text-sm flex-grow">
<b>Scan this QR code</b> or <b>add this secret key</b>:
</div>
<div className="w-56">
<DecoratedInput
disabled={true}
text={act.secretKey}
right={[copy]}
/>
</div>
<div className="min-w-2" />
<AuthAppInfoTooltip />
</div>
<div className="min-h-2" />
<div className="flex flex-row items-center">
<Bullet className="self-start mt-2" />
<div className="min-w-1" />
<div className="text-sm flex-grow">
<b>Scan this QR code</b> or <b>add this secret key</b>:
</div>
</div>
<div className="min-h-2" />
<DecoratedInput
className="ml-4 w-92"
disabled={true}
text={act.secretKey}
right={[<CopyButton copyValue={act.secretKey} />]}
/>
</div>
</ModalDialogDescription>
<ModalDialogButtons>

View File

@@ -1,8 +1,16 @@
import { MfaProvider, UserProvider } from '../../providers';
import { MfaProvider } from '../../providers';
import { action, makeAutoObservable, observable } from 'mobx';
type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification';
type VerificationStatus = 'none' | 'invalid' | 'valid';
type ActivationStep =
| 'scan-qr-code'
| 'save-secret-key'
| 'verification'
| 'success';
type VerificationStatus =
| 'none'
| 'invalid-auth-code'
| 'invalid-secret'
| 'valid';
export class TwoFactorActivation {
public readonly type = 'two-factor-activation' as const;
@@ -16,7 +24,7 @@ export class TwoFactorActivation {
constructor(
private mfaProvider: MfaProvider,
private userProvider: UserProvider,
private readonly email: string,
private readonly _secretKey: string,
private _cancelActivation: () => void,
private _enabled2FA: () => void
@@ -29,7 +37,6 @@ export class TwoFactorActivation {
| '_authCode'
| '_step'
| '_enable2FAVerification'
| 'refreshOtp'
| 'inputOtpToken'
| 'inputSecretKey'
>(
@@ -39,7 +46,6 @@ export class TwoFactorActivation {
_authCode: observable,
_step: observable,
_enable2FAVerification: observable,
refreshOtp: action,
inputOtpToken: observable,
inputSecretKey: observable,
},
@@ -60,8 +66,7 @@ export class TwoFactorActivation {
}
get qrCode(): string {
const email = this.userProvider.getUser()!.email;
return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`;
return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${this.email}`;
}
cancelActivation(): void {
@@ -69,8 +74,7 @@ export class TwoFactorActivation {
}
openScanQRCode(): void {
const preconditions: ActivationStep[] = ['save-secret-key'];
if (preconditions.includes(this._activationStep)) {
if (this._activationStep === 'save-secret-key') {
this._activationStep = 'scan-qr-code';
}
}
@@ -85,13 +89,18 @@ export class TwoFactorActivation {
openVerification(): void {
this.inputOtpToken = '';
this.inputSecretKey = '';
const preconditions: ActivationStep[] = ['save-secret-key'];
if (preconditions.includes(this._activationStep)) {
if (this._activationStep === 'save-secret-key') {
this._activationStep = 'verification';
this._2FAVerification = 'none';
}
}
openSuccess(): void {
if (this._activationStep === 'verification') {
this._activationStep = 'success';
}
}
setInputSecretKey(secretKey: string): void {
this.inputSecretKey = secretKey;
}
@@ -101,22 +110,29 @@ export class TwoFactorActivation {
}
enable2FA(): void {
if (this.inputSecretKey === this._secretKey) {
this.mfaProvider
.enableMfa(this.inputSecretKey, this.inputOtpToken)
.then(
action(() => {
this._2FAVerification = 'valid';
this._enabled2FA();
})
)
.catch(
action(() => {
this._2FAVerification = 'invalid';
})
);
} else {
this._2FAVerification = 'invalid';
if (this.inputSecretKey !== this._secretKey) {
this._2FAVerification = 'invalid-secret';
return;
}
this.mfaProvider
.enableMfa(this.inputSecretKey, this.inputOtpToken)
.then(
action(() => {
this._2FAVerification = 'valid';
this.openSuccess();
})
)
.catch(
action(() => {
this._2FAVerification = 'invalid-auth-code';
})
);
}
finishActivation(): void {
if (this._activationStep === 'success') {
this._enabled2FA();
}
}
}

View File

@@ -4,17 +4,19 @@ import { TwoFactorActivation } from './TwoFactorActivation';
import { SaveSecretKey } from './SaveSecretKey';
import { ScanQRCode } from './ScanQRCode';
import { Verification } from './Verification';
import { TwoFactorSuccess } from './TwoFactorSuccess';
export const TwoFactorActivationView: FunctionComponent<{
activation: TwoFactorActivation;
}> = observer(({ activation: act }) => (
<>
{act.activationStep === 'scan-qr-code' && <ScanQRCode activation={act} />}
{act.activationStep === 'save-secret-key' && (
<SaveSecretKey activation={act} />
)}
{act.activationStep === 'verification' && <Verification activation={act} />}
</>
));
}> = observer(({ activation: act }) => {
switch (act.activationStep) {
case 'scan-qr-code':
return <ScanQRCode activation={act} />;
case 'save-secret-key':
return <SaveSecretKey activation={act} />;
case 'verification':
return <Verification activation={act} />;
case 'success':
return <TwoFactorSuccess activation={act} />;
}
});

View File

@@ -7,17 +7,21 @@ type TwoFactorStatus =
| TwoFactorActivation
| 'two-factor-disabled';
export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-disabled' =>
status === 'two-factor-disabled';
export const is2FADisabled = (
status: TwoFactorStatus
): status is 'two-factor-disabled' => status === 'two-factor-disabled';
export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation =>
export const is2FAActivation = (
status: TwoFactorStatus
): status is TwoFactorActivation =>
(status as TwoFactorActivation)?.type === 'two-factor-activation';
export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' =>
status === 'two-factor-enabled';
export const is2FAEnabled = (
status: TwoFactorStatus
): status is 'two-factor-enabled' => status === 'two-factor-enabled';
export class TwoFactorAuth {
private _status: TwoFactorStatus | 'fetching' = 'fetching';
private _status: TwoFactorStatus = 'two-factor-disabled';
private _errorMessage: string | null;
constructor(
@@ -29,24 +33,28 @@ export class TwoFactorAuth {
makeAutoObservable<
TwoFactorAuth,
'_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation'
>(this, {
_status: observable,
_errorMessage: observable,
deactivateMfa: action,
startActivation: action,
});
>(
this,
{
_status: observable,
_errorMessage: observable,
deactivateMfa: action,
startActivation: action,
},
{ autoBind: true }
);
}
private startActivation(): void {
const setDisabled = action(() => (this._status = 'two-factor-disabled'));
const setEnabled = action(() => (this._status = 'two-factor-enabled'));
const setEnabled = action(() => this.fetchStatus());
this.mfaProvider
.generateMfaSecret()
.then(
action((secret) => {
this._status = new TwoFactorActivation(
this.mfaProvider,
this.userProvider,
this.userProvider.getUser()!.email,
secret,
setDisabled,
setEnabled
@@ -65,7 +73,7 @@ export class TwoFactorAuth {
.disableMfa()
.then(
action(() => {
this._status = 'two-factor-disabled';
this.fetchStatus();
})
)
.catch(
@@ -75,18 +83,16 @@ export class TwoFactorAuth {
);
}
private get isLoggedIn(): boolean {
isLoggedIn(): boolean {
return this.userProvider.getUser() != undefined;
}
fetchStatus(): void {
this._status = 'fetching';
if (!this.isLoggedIn) {
if (!this.isLoggedIn()) {
return;
}
if (!this.isMfaFeatureAvailable) {
if (!this.isMfaFeatureAvailable()) {
return;
}
@@ -111,11 +117,11 @@ export class TwoFactorAuth {
}
toggle2FA(): void {
if (!this.isLoggedIn) {
if (!this.isLoggedIn()) {
return;
}
if (!this.isMfaFeatureAvailable) {
if (!this.isMfaFeatureAvailable()) {
return;
}
@@ -129,23 +135,14 @@ export class TwoFactorAuth {
}
get errorMessage(): string | null {
if (!this.isLoggedIn) {
return 'Two-factor authentication not available / Sign in or register for an account to configure 2FA';
}
if (!this.isMfaFeatureAvailable) {
return 'Two-factor authentication not available / A paid subscription plan is required to enable 2FA.';
}
return this._errorMessage;
}
get status(): TwoFactorStatus {
if (this._status === 'fetching') {
return 'two-factor-disabled';
}
return this._status;
}
private get isMfaFeatureAvailable(): boolean {
isMfaFeatureAvailable(): boolean {
return this.mfaProvider.isMfaFeatureAvailable();
}
}

View File

@@ -4,41 +4,85 @@ import {
Text,
PreferencesGroup,
PreferencesSegment,
LinkButton,
} from '../../components';
import { Switch } from '../../../components/Switch';
import { observer } from 'mobx-react-lite';
import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorActivationView } from './TwoFactorActivationView';
const TwoFactorTitle: FunctionComponent<{ auth: TwoFactorAuth }> = observer(
({ auth }) => {
if (!auth.isLoggedIn()) {
return <Title>Two-factor authentication not available</Title>;
}
if (!auth.isMfaFeatureAvailable()) {
return <Title>Two-factor authentication not available</Title>;
}
return <Title>Two-factor authentication</Title>;
}
);
const TwoFactorDescription: FunctionComponent<{ auth: TwoFactorAuth }> =
observer(({ auth }) => {
if (!auth.isLoggedIn()) {
return <Text>Sign in or register for an account to configure 2FA.</Text>;
}
if (!auth.isMfaFeatureAvailable()) {
return (
<Text>
A paid subscription plan is required to enable 2FA.{' '}
<a target="_blank" href="https://standardnotes.com/features">
Learn more
</a>
.
</Text>
);
}
return (
<Text>An extra layer of security when logging in to your account.</Text>
);
});
const TwoFactorSwitch: FunctionComponent<{ auth: TwoFactorAuth }> = observer(
({ auth }) => {
if (auth.isLoggedIn() && auth.isMfaFeatureAvailable()) {
return (
<Switch
checked={!is2FADisabled(auth.status)}
onChange={auth.toggle2FA}
/>
);
}
return null;
}
);
export const TwoFactorAuthView: FunctionComponent<{
auth: TwoFactorAuth;
}> = observer(({ auth }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<Title>Two-factor authentication</Title>
<Text>
An extra layer of security when logging in to your account.
</Text>
<>
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<TwoFactorTitle auth={auth} />
<TwoFactorDescription auth={auth} />
</div>
<TwoFactorSwitch auth={auth} />
</div>
<Switch
checked={!is2FADisabled(auth.status)}
onChange={auth.toggle2FA}
/>
</div>
</PreferencesSegment>
</PreferencesSegment>
{auth.errorMessage != null && (
<PreferencesSegment>
<Text className="color-danger">{auth.errorMessage}</Text>
</PreferencesSegment>
)}
</PreferencesGroup>
{is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}
{auth.errorMessage != null && (
<PreferencesSegment>
<Text className="color-danger">{auth.errorMessage}</Text>
</PreferencesSegment>
)}
</PreferencesGroup>
</>
);
});

View File

@@ -0,0 +1,36 @@
import { Button } from '@/components/Button';
import ModalDialog, {
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
import { Subtitle } from '@/preferences/components';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { TwoFactorActivation } from './TwoFactorActivation';
export const TwoFactorSuccess: FunctionComponent<{
activation: TwoFactorActivation;
}> = observer(({ activation: act }) => (
<ModalDialog>
<ModalDialogLabel closeDialog={act.finishActivation}>
Successfully Enabled
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex flex-row items-center justify-center pt-2">
<Subtitle>
Two-factor authentication has been successfully enabled for your
account.
</Subtitle>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
<Button
className="min-w-20"
type="primary"
label="Finish"
onClick={act.finishActivation}
/>
</ModalDialogButtons>
</ModalDialog>
));

View File

@@ -2,51 +2,66 @@ import { Button } from '@/components/Button';
import { DecoratedInput } from '@/components/DecoratedInput';
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { Bullet } from './Bullet';
import { TwoFactorActivation } from './TwoFactorActivation';
import {
ModalDialog,
ModalDialogButtons,
ModalDialogDescription,
ModalDialogLabel
ModalDialogLabel,
} from '@/components/shared/ModalDialog';
export const Verification: FunctionComponent<{
activation: TwoFactorActivation;
}> = observer(({ activation: act }) => {
const borderInv =
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
const secretKeyClass =
act.verificationStatus === 'invalid-secret' ? 'border-danger' : '';
const authTokenClass =
act.verificationStatus === 'invalid-auth-code' ? 'border-danger' : '';
return (
<ModalDialog>
<ModalDialogLabel closeDialog={act.cancelActivation}>
Step 3 of 3 - Verification
</ModalDialogLabel>
<ModalDialogDescription>
<div className="flex-grow flex flex-col gap-1">
<div className="flex flex-row items-center gap-2">
<ModalDialogDescription className="h-33">
<div className="flex-grow flex flex-col">
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Enter your <b>secret key</b>:
Enter your <b>secret key</b>:
</div>
<div className="min-w-2" />
<DecoratedInput
className={borderInv}
className={`w-92 ${secretKeyClass}`}
onChange={act.setInputSecretKey}
/>
</div>
<div className="flex flex-row items-center gap-2">
<div className="min-h-1" />
<div className="flex flex-row items-center">
<Bullet />
<div className="min-w-1" />
<div className="text-sm">
Verify the <b>authentication code</b> generated by your
Verify the <b>authentication code</b> generated by your
authenticator app:
</div>
<div className="min-w-2" />
<DecoratedInput
className={`w-30 ${borderInv}`}
className={`w-30 ${authTokenClass}`}
onChange={act.setInputOtpToken}
/>
</div>
</div>
</ModalDialogDescription>
<ModalDialogButtons>
{act.verificationStatus === 'invalid' && (
<div className="text-sm color-danger">
Incorrect credentials, please try again.
{act.verificationStatus === 'invalid-auth-code' && (
<div className="text-sm color-danger flex-grow">
Incorrect authentication code, please try again.
</div>
)}
{act.verificationStatus === 'invalid-secret' && (
<div className="text-sm color-danger flex-grow">
Incorrect secret key, please try again.
</div>
)}
<Button