From a83dba418c26b107747272f5841730e5d6097be5 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Mon, 12 Jul 2021 14:14:37 +0200 Subject: [PATCH 01/14] feat: 2fa dialog flow partial --- app/assets/icons/ic-info.svg | 3 + app/assets/javascripts/components/Button.tsx | 17 ++ app/assets/javascripts/components/Icon.tsx | 2 + .../javascripts/components/IconButton.tsx | 2 +- .../preferences/PreferencesMenu.tsx | 31 +-- .../preferences/PreferencesView.tsx | 21 +-- .../preferences/components/Content.tsx | 14 +- .../preferences/models/preferences.ts | 35 ++-- .../preferences/models/two-factor-auth.ts | 177 ++++++++++++++---- .../preferences/panes/HelpFeedback.tsx | 11 +- .../preferences/panes/Security.tsx | 15 +- .../preferences/panes/TwoFactorAuth.tsx | 110 ----------- .../panes/two-factor-auth/email-recovery.tsx | 7 + .../panes/two-factor-auth/index.tsx | 10 + .../panes/two-factor-auth/save-secret-key.tsx | 55 ++++++ .../panes/two-factor-auth/scan-qr-code.tsx | 64 +++++++ .../panes/two-factor-auth/utils.tsx | 104 ++++++++++ .../panes/two-factor-auth/verification.tsx | 7 + .../panes/two-factor-auth/view.tsx | 130 +++++++++++++ .../ui_models/app_state/preferences_state.ts | 3 +- 20 files changed, 603 insertions(+), 215 deletions(-) create mode 100644 app/assets/icons/ic-info.svg create mode 100644 app/assets/javascripts/components/Button.tsx delete mode 100644 app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx diff --git a/app/assets/icons/ic-info.svg b/app/assets/icons/ic-info.svg new file mode 100644 index 00000000000..47ea73219a4 --- /dev/null +++ b/app/assets/icons/ic-info.svg @@ -0,0 +1,3 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M9.16675 7.50008H10.8334V5.83342H9.16675V7.50008ZM10.0001 16.6667C6.32508 16.6667 3.33341 13.6751 3.33341 10.0001C3.33341 6.32508 6.32508 3.33341 10.0001 3.33341C13.6751 3.33341 16.6667 6.32508 16.6667 10.0001C16.6667 13.6751 13.6751 16.6667 10.0001 16.6667ZM10.0001 1.66675C8.90573 1.66675 7.8221 1.8823 6.81105 2.30109C5.80001 2.71987 4.88135 3.3337 4.10752 4.10752C2.54472 5.67033 1.66675 7.78994 1.66675 10.0001C1.66675 12.2102 2.54472 14.3298 4.10752 15.8926C4.88135 16.6665 5.80001 17.2803 6.81105 17.6991C7.8221 18.1179 8.90573 18.3334 10.0001 18.3334C12.2102 18.3334 14.3298 17.4554 15.8926 15.8926C17.4554 14.3298 18.3334 12.2102 18.3334 10.0001C18.3334 8.90573 18.1179 7.8221 17.6991 6.81105C17.2803 5.80001 16.6665 4.88135 15.8926 4.10752C15.1188 3.3337 14.2002 2.71987 13.1891 2.30109C12.1781 1.8823 11.0944 1.66675 10.0001 1.66675ZM9.16675 14.1667H10.8334V9.16675H9.16675V14.1667Z" fill="#72767E"/> +</svg> diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx new file mode 100644 index 00000000000..a8b9ff8be17 --- /dev/null +++ b/app/assets/javascripts/components/Button.tsx @@ -0,0 +1,17 @@ +import { FunctionComponent } from 'preact'; + +const base = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`; + +const normal = `${base} bg-default color-text border-solid border-gray-300 border-1 \ +focus:bg-contrast hover:bg-contrast`; +const primary = `${base} no-border bg-info color-info-contrast hover:brightness-130 \ +focus:brightness-130`; + +export const Button: FunctionComponent<{ + className?: string; + type: 'normal' | 'primary'; + label: string; +}> = ({ type, label, className = '' }) => { + const buttonClass = type === 'primary' ? primary : normal; + return <button className={`${buttonClass} ${className}`}>{label}</button>; +}; diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index c6f9f632cb1..1a30ee761d0 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -25,6 +25,7 @@ import ThemesIcon from '../../icons/ic-themes.svg'; import UserIcon from '../../icons/ic-user.svg'; import CopyIcon from '../../icons/ic-copy.svg'; import DownloadIcon from '../../icons/ic-download.svg'; +import InfoIcon from '../../icons/ic-info.svg'; import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; @@ -56,6 +57,7 @@ const ICONS = { user: UserIcon, copy: CopyIcon, download: DownloadIcon, + info: InfoIcon, }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/components/IconButton.tsx b/app/assets/javascripts/components/IconButton.tsx index 8c0920c64d5..e1551741961 100644 --- a/app/assets/javascripts/components/IconButton.tsx +++ b/app/assets/javascripts/components/IconButton.tsx @@ -27,7 +27,7 @@ export const IconButton: FunctionComponent<Props> = ({ }; return ( <button - className={`no-border bg-transparent hover:brightness-130 p-0 ${ + className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${ className ?? '' }`} onClick={click} diff --git a/app/assets/javascripts/preferences/PreferencesMenu.tsx b/app/assets/javascripts/preferences/PreferencesMenu.tsx index 8e74450e2f4..06ed02dd548 100644 --- a/app/assets/javascripts/preferences/PreferencesMenu.tsx +++ b/app/assets/javascripts/preferences/PreferencesMenu.tsx @@ -1,19 +1,20 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { MenuItem } from './components'; -import { Preferences } from './models/preferences'; +import { PreferencesMenu } from './models/preferences'; -export const PreferencesMenu: FunctionComponent<{ preferences: Preferences }> = - observer(({ preferences }) => ( - <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> - {preferences.menuItems.map((pref) => ( - <MenuItem - key={pref.id} - iconType={pref.icon} - label={pref.label} - selected={pref.selected} - onClick={() => preferences.selectPane(pref.id)} - /> - ))} - </div> - )); +export const PreferencesMenuView: FunctionComponent<{ + preferences: PreferencesMenu; +}> = observer(({ preferences }) => ( + <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> + {preferences.menuItems.map((pref) => ( + <MenuItem + key={pref.id} + iconType={pref.icon} + label={pref.label} + selected={pref.selected} + onClick={() => preferences.selectPane(pref.id)} + /> + ))} + </div> +)); diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 423017cde04..b1ce7604de8 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -1,20 +1,19 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; -import { Preferences } from './models/preferences'; -import { PreferencesMenu } from './PreferencesMenu'; -import { HelpAndFeedback } from './panes/HelpFeedback'; +import { PreferencesMenu } from './models/preferences'; +import { PreferencesMenuView } from './PreferencesMenu'; +import { HelpAndFeedback, Security } from './panes'; import { observer } from 'mobx-react-lite'; -import { Security } from './panes/Security'; interface PreferencesViewProps { close: () => void; } const PaneSelector: FunctionComponent<{ - prefs: Preferences; -}> = observer(({ prefs }) => { - switch (prefs.selectedPaneId) { + prefs: PreferencesMenu; +}> = observer(({ prefs: menu }) => { + switch (menu.selectedPaneId) { case 'general': return null; case 'account': @@ -22,7 +21,7 @@ const PaneSelector: FunctionComponent<{ case 'appearance': return null; case 'security': - return <Security prefs={prefs} />; + return <Security />; case 'listed': return null; case 'shortcuts': @@ -37,17 +36,17 @@ const PaneSelector: FunctionComponent<{ }); const PreferencesCanvas: FunctionComponent<{ - preferences: Preferences; + preferences: PreferencesMenu; }> = observer(({ preferences: prefs }) => ( <div className="flex flex-row flex-grow min-h-0 justify-between"> - <PreferencesMenu preferences={prefs}></PreferencesMenu> + <PreferencesMenuView preferences={prefs}></PreferencesMenuView> <PaneSelector prefs={prefs} /> </div> )); const PreferencesView: FunctionComponent<PreferencesViewProps> = observer( ({ close }) => { - const prefs = new Preferences(); + const prefs = new PreferencesMenu(); return ( <div className="sn-full-screen flex flex-col bg-contrast z-index-preferences"> <TitleBar className="items-center justify-between"> diff --git a/app/assets/javascripts/preferences/components/Content.tsx b/app/assets/javascripts/preferences/components/Content.tsx index cbc71ef4b3c..5f3da04f388 100644 --- a/app/assets/javascripts/preferences/components/Content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -12,17 +12,15 @@ export const Text: FunctionComponent = ({ children }) => ( <p className="text-xs">{children}</p> ); -export const Button: FunctionComponent<{ label: string; link: string }> = ({ +const buttonClasses = `block bg-default color-text rounded border-solid \ +border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \ +focus:bg-contrast hover:bg-contrast `; + +export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({ label, link, }) => ( - <a - target="_blank" - className="block bg-default color-text rounded border-solid border-1 - border-gray-300 px-4 py-2 font-bold text-sm fit-content mt-3 - focus:bg-contrast hover:bg-contrast " - href={link} - > + <a target="_blank" className={buttonClasses} href={link}> {label} </a> ); diff --git a/app/assets/javascripts/preferences/models/preferences.ts b/app/assets/javascripts/preferences/models/preferences.ts index 2402931cf18..2fbfecb56b1 100644 --- a/app/assets/javascripts/preferences/models/preferences.ts +++ b/app/assets/javascripts/preferences/models/preferences.ts @@ -15,18 +15,16 @@ const PREFERENCE_IDS = [ ] as const; export type PreferenceId = typeof PREFERENCE_IDS[number]; -interface PreferenceMenuItem { +interface PreferencesMenuItem { readonly id: PreferenceId; readonly icon: IconType; readonly label: string; } -type PreferencesMenu = PreferenceMenuItem[]; - /** * Items are in order of appearance */ -const PREFERENCES_MENU: PreferencesMenu = [ +const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ { id: 'general', label: 'General', icon: 'settings' }, { id: 'account', label: 'Account', icon: 'user' }, { id: 'appearance', label: 'Appearance', icon: 'themes' }, @@ -38,20 +36,23 @@ const PREFERENCES_MENU: PreferencesMenu = [ { id: 'help-feedback', label: 'Help & feedback', icon: 'help' }, ]; -export class Preferences { - private _selectedPane: PreferenceId = 'general'; - - private _twoFactorAuth: TwoFactorAuth; +export class PreferencesMenu { + // TODO change to 'general' before merge + private _selectedPane: PreferenceId = 'security'; - constructor(private readonly _menu: PreferencesMenu = PREFERENCES_MENU) { - this._twoFactorAuth = new TwoFactorAuth(); - makeAutoObservable<Preferences, '_selectedPane' | '_twoFactorAuth'>(this, { - _twoFactorAuth: observable, - _selectedPane: observable, - }); + constructor( + private readonly _menu: PreferencesMenuItem[] = PREFERENCES_MENU_ITEMS + ) { + makeAutoObservable<PreferencesMenu, '_selectedPane' | '_twoFactorAuth'>( + this, + { + _twoFactorAuth: observable, + _selectedPane: observable, + } + ); } - get menuItems(): (PreferenceMenuItem & { + get menuItems(): (PreferencesMenuItem & { selected: boolean; })[] { return this._menu.map((p) => ({ @@ -69,8 +70,4 @@ export class Preferences { selectPane(key: PreferenceId) { this._selectedPane = key; } - - get twoFactorAuth() { - return this._twoFactorAuth; - } } diff --git a/app/assets/javascripts/preferences/models/two-factor-auth.ts b/app/assets/javascripts/preferences/models/two-factor-auth.ts index c3b2503b95e..06498d0d614 100644 --- a/app/assets/javascripts/preferences/models/two-factor-auth.ts +++ b/app/assets/javascripts/preferences/models/two-factor-auth.ts @@ -1,4 +1,4 @@ -import { makeAutoObservable, observable } from 'mobx'; +import { action, makeAutoObservable, observable } from 'mobx'; function getNewAuthCode() { const MIN = 100000; @@ -7,21 +7,122 @@ function getNewAuthCode() { return code.toString(); } -class TwoFactorData { +const activationSteps = [ + 'scan-qr-code', + 'save-secret-key', + 'email-recovery', + 'verification', +] as const; + +type ActivationStep = typeof activationSteps[number]; + +export class TwoFactorActivation { + public readonly type = 'two-factor-activation' as const; + + private _step: ActivationStep; + + private _secretKey: string; + private _authCode: string; + private _allowEmailRecovery: boolean = false; + private _2FAVerification: 'none' | 'invalid' | 'valid' = 'none'; + + constructor( + private _cancelActivation: () => void, + private _enable2FA: (secretKey: string) => void + ) { + this._secretKey = 'FHJJSAJKDASKW43KJS'; + this._authCode = getNewAuthCode(); + this._step = 'save-secret-key'; + + makeAutoObservable< + TwoFactorActivation, + | '_secretKey' + | '_authCode' + | '_step' + | '_allowEmailRecovery' + | '_enable2FAVerification' + >(this, { + _secretKey: observable, + _authCode: observable, + _step: observable, + _allowEmailRecovery: observable, + _enable2FAVerification: observable, + }); + } + + get secretKey() { + return this._secretKey; + } + + get authCode() { + return this._authCode; + } + + get allowEmailRecovery() { + return this._allowEmailRecovery; + } + + get step() { + return this._step; + } + + get enable2FAVerification() { + return this._2FAVerification; + } + + cancelActivation() { + this._cancelActivation; + } + + nextScanQRCode() { + this._step = 'save-secret-key'; + } + + backSaveSecretKey() { + this._step = 'scan-qr-code'; + } + + nextSaveSecretKey() { + this._step = 'email-recovery'; + } + + backEmailRecovery() { + this._step = 'save-secret-key'; + } + + nextEmailRecovery(allowEmailRecovery: boolean) { + this._step = 'verification'; + this._allowEmailRecovery = allowEmailRecovery; + } + + backVerification() { + this._step = 'email-recovery'; + } + + enable2FA(secretKey: string, authCode: string) { + if (secretKey === this._secretKey && authCode === this._authCode) { + this._2FAVerification = 'valid'; + this._enable2FA(secretKey); + return; + } + + this._2FAVerification = 'invalid'; + } +} + +export class TwoFactorEnabled { + public readonly type = 'enabled' as const; private _secretKey: string; private _authCode: string; constructor(secretKey: string) { this._secretKey = secretKey; this._authCode = getNewAuthCode(); - makeAutoObservable<TwoFactorData, '_secretKey' | '_authCode'>( - this, - { - _secretKey: observable, - _authCode: observable, - }, - { autoBind: true } - ); + + makeAutoObservable<TwoFactorEnabled, '_secretKey' | '_authCode'>(this, { + _secretKey: observable, + _authCode: observable, + }); } get secretKey() { @@ -37,45 +138,51 @@ class TwoFactorData { } } -type TwoFactorStatus = 'enabled' | 'disabled'; - export class TwoFactorAuth { - private _twoFactorStatus: TwoFactorStatus = 'disabled'; - private _twoFactorData: TwoFactorData | null = null; + private _status: + | TwoFactorEnabled + | TwoFactorActivation + | 'two-factor-disabled' = new TwoFactorActivation( + () => {}, + () => {} + ); constructor() { - makeAutoObservable<TwoFactorAuth, '_twoFactorStatus' | '_twoFactorData'>( - this, - { - _twoFactorStatus: observable, - _twoFactorData: observable, - }, - { autoBind: true } - ); + makeAutoObservable<TwoFactorAuth, '_status'>(this, { + _status: observable, + }); } - private activate2FA() { - this._twoFactorData = new TwoFactorData('FHJJSAJKDASKW43KJS'); - this._twoFactorStatus = 'enabled'; + private startActivation() { + const cancel = action(() => (this._status = 'two-factor-disabled')); + const enable = action( + (secretKey: string) => (this._status = new TwoFactorEnabled(secretKey)) + ); + this._status = new TwoFactorActivation(cancel, enable); } private deactivate2FA() { - this._twoFactorData = null; - this._twoFactorStatus = 'disabled'; + this._status = 'two-factor-disabled'; } toggle2FA() { - if (this._twoFactorStatus === 'enabled') this.deactivate2FA(); - else this.activate2FA(); + if (this._status === 'two-factor-disabled') this.startActivation(); + else this.deactivate2FA(); } - get twoFactorStatus() { - return this._twoFactorStatus; + get enabled() { + return ( + (this._status instanceof TwoFactorEnabled && + (this._status as TwoFactorEnabled)) || + false + ); } - get twoFactorData() { - if (this._twoFactorStatus !== 'enabled') - throw new Error(`Can't provide 2FA data if not enabled`); - return this._twoFactorData; + get activation() { + return ( + (this._status instanceof TwoFactorActivation && + (this._status as TwoFactorActivation)) || + false + ); } } diff --git a/app/assets/javascripts/preferences/panes/HelpFeedback.tsx b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx index 231304cb2d1..3749689ed1f 100644 --- a/app/assets/javascripts/preferences/panes/HelpFeedback.tsx +++ b/app/assets/javascripts/preferences/panes/HelpFeedback.tsx @@ -1,10 +1,9 @@ import { FunctionComponent } from 'preact'; -import {} from '../components'; import { Title, Subtitle, Text, - Button, + LinkButton, PreferencesGroup, PreferencesPane, PreferencesSegment, @@ -53,7 +52,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( </PreferencesSegment> <PreferencesSegment> <Subtitle>Can’t find your question here?</Subtitle> - <Button label="Open FAQ" link="https://standardnotes.com/help" /> + <LinkButton label="Open FAQ" link="https://standardnotes.com/help" /> </PreferencesSegment> </PreferencesGroup> <PreferencesGroup> @@ -68,7 +67,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( </a>{' '} before advocating for a feature request. </Text> - <Button + <LinkButton label="Go to the forum" link="https://forum.standardnotes.org/" /> @@ -82,7 +81,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( Want to share your feedback with us? Join the Standard Notes Slack group for discussions on security, themes, editors and more. </Text> - <Button + <LinkButton link="https://standardnotes.com/slack" label="Join our Slack group" /> @@ -94,7 +93,7 @@ export const HelpAndFeedback: FunctionComponent = () => ( <Text> Send an email to help@standardnotes.org and we’ll sort it out. </Text> - <Button link="mailto: help@standardnotes.org" label="Email us" /> + <LinkButton link="mailto: help@standardnotes.org" label="Email us" /> </PreferencesSegment> </PreferencesGroup> </PreferencesPane> diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index b026564a0e3..7269e485a6d 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -1,13 +1,10 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; -import { Preferences } from '../models'; -import { TwoFactorAuthComponent } from './TwoFactorAuth'; +import { TwoFactorAuthWrapper } from './two-factor-auth'; -export const Security: FunctionComponent<{ prefs: Preferences }> = observer( - ({ prefs }) => ( - <PreferencesPane> - <TwoFactorAuthComponent tfAuth={prefs.twoFactorAuth} /> - </PreferencesPane> - ) -); +export const Security: FunctionComponent = observer(() => ( + <PreferencesPane> + <TwoFactorAuthWrapper /> + </PreferencesPane> +)); diff --git a/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx b/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx deleted file mode 100644 index f360dc68064..00000000000 --- a/app/assets/javascripts/preferences/panes/TwoFactorAuth.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { FunctionComponent } from 'preact'; -import { - Title, - Text, - PreferencesGroup, - PreferencesSegment, -} from '../components'; -import { Switch } from '../../components/Switch'; -import { observer } from 'mobx-react-lite'; -import { DecoratedInput } from '../../components/DecoratedInput'; -import { IconButton } from '../../components/IconButton'; -import { TwoFactorAuth } from '../models'; - -// Temporary implementation until integration -function downloadSecretKey(text: string) { - const link = document.createElement('a'); - const blob = new Blob([text], { - type: 'text/plain;charset=utf-8', - }); - link.href = window.URL.createObjectURL(blob); - link.setAttribute('download', 'secret_key.txt'); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(link.href); -} -export const TwoFactorAuthComponent: FunctionComponent<{ - tfAuth: TwoFactorAuth; -}> = observer(({ tfAuth }) => { - 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> - </div> - <Switch - checked={tfAuth.twoFactorStatus === 'enabled'} - onChange={() => tfAuth.toggle2FA()} - /> - </div> - </PreferencesSegment> - <PreferencesSegment> - {tfAuth.twoFactorStatus === 'enabled' && - tfAuth.twoFactorData != null ? ( - <TwoFactorEnabled tfAuth={tfAuth} /> - ) : ( - <TwoFactorDisabled /> - )} - </PreferencesSegment> - </PreferencesGroup> - ); -}); - -const TwoFactorEnabled: FunctionComponent<{ tfAuth: TwoFactorAuth }> = observer( - ({ tfAuth }) => { - const state = tfAuth.twoFactorData!; - const download = ( - <IconButton - icon="download" - onClick={() => { - downloadSecretKey(state.secretKey); - }} - /> - ); - const copy = ( - <IconButton - icon="copy" - onClick={() => { - navigator?.clipboard?.writeText(state.secretKey); - }} - /> - ); - const spinner = <div class="sk-spinner info w-8 h-3.5" />; - return ( - <div className="flex flex-row gap-4"> - <div className="flex-grow flex flex-col"> - <Text>Secret Key</Text> - <DecoratedInput - disabled={true} - right={[copy, download]} - text={state.secretKey} - /> - </div> - <div className="w-30 flex flex-col"> - <Text>Authentication Code</Text> - <DecoratedInput - disabled={true} - text={state.authCode} - right={[spinner]} - /> - </div> - </div> - ); - } -); -const TwoFactorDisabled: FunctionComponent = () => ( - <Text> - Enabling two-factor authentication will sign you out of all other sessions.{' '} - <a - target="_blank" - href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key" - > - Learn more - </a> - </Text> -); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx new file mode 100644 index 00000000000..e02d368dd96 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx @@ -0,0 +1,7 @@ +import { TwoFactorActivation } from '@/preferences/models'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +export const EmailRecovery: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation }) => <></>); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx new file mode 100644 index 00000000000..61293e22177 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx @@ -0,0 +1,10 @@ +import { TwoFactorAuth } from '../../models'; +import { observable } from 'mobx'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { TwoFactorAuthView } from './view'; + +export const TwoFactorAuthWrapper: FunctionComponent = observer(() => { + const tfAuth = observable(new TwoFactorAuth()); + return <TwoFactorAuthView tfAuth={tfAuth} />; +}); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx new file mode 100644 index 00000000000..963ba8eba49 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx @@ -0,0 +1,55 @@ +import { Button } from '@/components/Button'; +import { IconButton } from '@/components/IconButton'; +import { TwoFactorActivation } from '@/preferences/models'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { + downloadSecretKey, + TwoFactorDialog, + TwoFactorDialogLabel, + TwoFactorDialogDescription, + TwoFactorDialogButtons, +} from './utils'; + +export const SaveSecretKey: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation: act }) => { + const download = ( + <IconButton + icon="download" + onClick={() => { + downloadSecretKey(act.secretKey); + }} + /> + ); + const copy = ( + <IconButton + icon="copy" + onClick={() => { + navigator?.clipboard?.writeText(act.secretKey); + }} + /> + ); + return ( + <TwoFactorDialog> + <TwoFactorDialogLabel close={() => {}}> + Step 2 of 4 - Save secret key + </TwoFactorDialogLabel> + <TwoFactorDialogDescription> + <div className="flex-grow flex flex-col gap-2"> + <div className="text-sm"> + ・<b>Save your secret key</b> somewhere safe: + </div> + <div className="text-sm"> + ・You can use this key to generate codes if you lose access to your + authenticator app. Learn more + </div> + </div> + </TwoFactorDialogDescription> + <TwoFactorDialogButtons> + <Button className="min-w-20" type="normal" label="Back" /> + <Button className="min-w-20" type="primary" label="Next" /> + </TwoFactorDialogButtons> + </TwoFactorDialog> + ); +}); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx new file mode 100644 index 00000000000..abc02a58ce7 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx @@ -0,0 +1,64 @@ +import { FunctionComponent } from 'preact'; +import { observer } from 'mobx-react-lite'; +import { DecoratedInput } from '../../../components/DecoratedInput'; +import { IconButton } from '../../../components/IconButton'; +import { TwoFactorActivation } from '../../models'; +import { Button } from '@/components/Button'; +import { + AuthAppInfoPopup, + TwoFactorDialog, + TwoFactorDialogButtons, + TwoFactorDialogDescription, + TwoFactorDialogLabel, +} from './utils'; + +export const ScanQRCode: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation: act }) => { + const copy = ( + <IconButton + icon="copy" + onClick={() => { + navigator?.clipboard?.writeText(act.secretKey); + }} + /> + ); + return ( + <TwoFactorDialog> + <TwoFactorDialogLabel close={() => {}}> + Step 1 of 4 - Scan QR code + </TwoFactorDialogLabel> + <TwoFactorDialogDescription> + <div className="flex flex-row gap-3 items-center"> + <div className="w-25 h-25 flex items-center justify-center bg-info"> + QR code + </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> + <AuthAppInfoPopup /> + </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> + </div> + </div> + </TwoFactorDialogDescription> + <TwoFactorDialogButtons> + <Button className="min-w-20" type="normal" label="Cancel" /> + <Button className="min-w-20" type="primary" label="Next" /> + </TwoFactorDialogButtons> + </TwoFactorDialog> + ); +}); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx new file mode 100644 index 00000000000..fef01e1df57 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx @@ -0,0 +1,104 @@ +import { ComponentChildren, FunctionComponent } from 'preact'; +import { IconButton } from '../../../components/IconButton'; +import { + AlertDialog, + AlertDialogDescription, + AlertDialogLabel, +} from '@reach/alert-dialog'; +import { useEffect, useRef, useState } from 'preact/hooks'; + +// Temporary implementation until integration +export function downloadSecretKey(text: string) { + const link = document.createElement('a'); + const blob = new Blob([text], { + type: 'text/plain;charset=utf-8', + }); + link.href = window.URL.createObjectURL(blob); + link.setAttribute('download', 'secret_key.txt'); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(link.href); +} + +export const AuthAppInfoPopup: FunctionComponent = () => { + const [shown, setShow] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const dismiss = () => setShow(false); + document.addEventListener('mousedown', dismiss); + return () => { + document.removeEventListener('mousedown', dismiss); + }; + }, [ref]); + + return ( + <div className="relative"> + <IconButton + icon="info" + className="mt-1" + onClick={() => { + setShow(!shown); + }} + /> + {shown && ( + <div + className={`bg-black color-white text-center rounded shadow-overlay \ + py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} + > + Some apps, like Google Authenticator, do not back up and restore your + secret keys if you lose your device or get a new one. + </div> + )} + </div> + ); +}; + +export const TwoFactorDialog: FunctionComponent<{ + children: ComponentChildren; +}> = ({ children }) => { + // TODO discover what this does + const ldRef = useRef<HTMLButtonElement>(); + + return ( + <AlertDialog leastDestructiveRef={ldRef}> + <div className="sn-component w-160"> + <div className="w-160 bg-default rounded shadow-overlay"> + {children} + </div> + </div> + </AlertDialog> + ); +}; + +export const TwoFactorDialogLabel: FunctionComponent<{ close: () => void }> = ({ + children, +}) => ( + <AlertDialogLabel className=""> + <div className="px-4 py-4 flex flex-row"> + <div className="flex-grow color-black text-lg-sm-lh font-bold"> + {children} + </div> + <IconButton + className="color-grey-1 h-5 w-5" + icon="close" + onClick={close} + /> + </div> + <hr className="h-1px bg-border no-border m-0" /> + </AlertDialogLabel> +); + +export const TwoFactorDialogDescription: FunctionComponent = ({ children }) => ( + <AlertDialogDescription className="px-4 py-4"> + {children} + </AlertDialogDescription> +); + +export const TwoFactorDialogButtons: FunctionComponent = ({ children }) => ( + <> + <hr className="h-1px bg-border no-border m-0" /> + <div className="px-4 py-4 flex flex-row justify-end gap-3">{children}</div> + </> +); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx new file mode 100644 index 00000000000..f9f82ab779c --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx @@ -0,0 +1,7 @@ +import { TwoFactorActivation } from '@/preferences/models'; +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; + +export const Verification: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation }) => <></>); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx new file mode 100644 index 00000000000..27b19dbf0e8 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx @@ -0,0 +1,130 @@ +import { FunctionComponent } from 'preact'; +import { + Title, + Text, + PreferencesGroup, + PreferencesSegment, +} from '../../components'; +import { Switch } from '../../../components/Switch'; +import { observer } from 'mobx-react-lite'; +import { DecoratedInput } from '../../../components/DecoratedInput'; +import { IconButton } from '../../../components/IconButton'; +import { TwoFactorActivation, TwoFactorAuth } from '../../models'; +import { ScanQRCode } from './scan-qr-code'; +import { EmailRecovery } from './email-recovery'; +import { SaveSecretKey } from './save-secret-key'; +import { Verification } from './verification'; + +// Temporary implementation until integration +function downloadSecretKey(text: string) { + const link = document.createElement('a'); + const blob = new Blob([text], { + type: 'text/plain;charset=utf-8', + }); + link.href = window.URL.createObjectURL(blob); + link.setAttribute('download', 'secret_key.txt'); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(link.href); +} + +export const TwoFactorAuthView: FunctionComponent<{ + tfAuth: TwoFactorAuth; +}> = observer(({ tfAuth }) => ( + <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> + </div> + <Switch + checked={tfAuth.enabled !== false} + onChange={() => tfAuth.toggle2FA()} + /> + </div> + </PreferencesSegment> + <PreferencesSegment> + {tfAuth.enabled && ( + <TwoFactorEnabledView + secretKey={tfAuth.enabled.secretKey} + authCode={tfAuth.enabled.authCode} + /> + )} + + {tfAuth.activation instanceof TwoFactorActivation && ( + <TwoFactorActivationView activation={tfAuth.activation} /> + )} + + {tfAuth.enabled === false && <TwoFactorDisabledView />} + </PreferencesSegment> + </PreferencesGroup> +)); + +const TwoFactorEnabledView: FunctionComponent<{ + secretKey: string; + authCode: string; +}> = ({ secretKey, authCode }) => { + const download = ( + <IconButton + icon="download" + onClick={() => { + downloadSecretKey(secretKey); + }} + /> + ); + const copy = ( + <IconButton + icon="copy" + onClick={() => { + navigator?.clipboard?.writeText(secretKey); + }} + /> + ); + const spinner = <div class="sk-spinner info w-8 h-3.5" />; + return ( + <div className="flex flex-row gap-4"> + <div className="flex-grow flex flex-col"> + <Text>Secret Key</Text> + <DecoratedInput + disabled={true} + right={[copy, download]} + text={secretKey} + /> + </div> + <div className="w-30 flex flex-col"> + <Text>Authentication Code</Text> + <DecoratedInput disabled={true} text={authCode} right={[spinner]} /> + </div> + </div> + ); +}; + +const TwoFactorDisabledView: FunctionComponent = () => ( + <Text> + Enabling two-factor authentication will sign you out of all other sessions.{' '} + <a + target="_blank" + href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key" + > + Learn more + </a> + </Text> +); + +export const TwoFactorActivationView: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation: act }) => ( + <> + {act.step === 'scan-qr-code' && <ScanQRCode activation={act} />} + + {act.step === 'save-secret-key' && <SaveSecretKey activation={act} />} + + {act.step === 'email-recovery' && <EmailRecovery activation={act} />} + + {act.step === 'verification' && <Verification activation={act} />} + </> +)); diff --git a/app/assets/javascripts/ui_models/app_state/preferences_state.ts b/app/assets/javascripts/ui_models/app_state/preferences_state.ts index 607cd23e351..24192fdae1f 100644 --- a/app/assets/javascripts/ui_models/app_state/preferences_state.ts +++ b/app/assets/javascripts/ui_models/app_state/preferences_state.ts @@ -1,7 +1,8 @@ import { action, computed, makeObservable, observable } from 'mobx'; export class PreferencesState { - private _open = false; + // TODO change to false before merge + private _open = true; constructor() { makeObservable<PreferencesState, '_open'>(this, { From cd7e34725bd2d14baa52f1abf17a9425b30762d3 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 14:03:59 +0200 Subject: [PATCH 02/14] refactor: rename and reorganize two factor auth dialog flow --- .../preferences/PreferencesMenu.tsx | 20 -------- .../preferences/PreferencesView.tsx | 32 +++++++++--- .../javascripts/preferences/models/index.ts | 2 - .../two-factor-auth/AuthAppInfoPopup.tsx | 37 ++++++++++++++ .../{email-recovery.tsx => EmailRecovery.tsx} | 0 ...{save-secret-key.tsx => SaveSecretKey.tsx} | 6 +-- .../{scan-qr-code.tsx => ScanQRCode.tsx} | 10 ++-- .../{view.tsx => TwoFactorAuthView.tsx} | 25 +++------- .../{utils.tsx => TwoFactorDialog.tsx} | 50 +------------------ .../{verification.tsx => Verification.tsx} | 2 +- .../two-factor-auth/download-secret-key.tsx | 13 +++++ .../panes/two-factor-auth/index.tsx | 4 +- .../two-factor-auth/model.ts} | 0 .../preferences.ts => preferences-menu.ts} | 1 - 14 files changed, 92 insertions(+), 110 deletions(-) delete mode 100644 app/assets/javascripts/preferences/PreferencesMenu.tsx delete mode 100644 app/assets/javascripts/preferences/models/index.ts create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx rename app/assets/javascripts/preferences/panes/two-factor-auth/{email-recovery.tsx => EmailRecovery.tsx} (100%) rename app/assets/javascripts/preferences/panes/two-factor-auth/{save-secret-key.tsx => SaveSecretKey.tsx} (91%) rename app/assets/javascripts/preferences/panes/two-factor-auth/{scan-qr-code.tsx => ScanQRCode.tsx} (93%) rename app/assets/javascripts/preferences/panes/two-factor-auth/{view.tsx => TwoFactorAuthView.tsx} (82%) rename app/assets/javascripts/preferences/panes/two-factor-auth/{utils.tsx => TwoFactorDialog.tsx} (52%) rename app/assets/javascripts/preferences/panes/two-factor-auth/{verification.tsx => Verification.tsx} (78%) create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/download-secret-key.tsx rename app/assets/javascripts/preferences/{models/two-factor-auth.ts => panes/two-factor-auth/model.ts} (100%) rename app/assets/javascripts/preferences/{models/preferences.ts => preferences-menu.ts} (97%) diff --git a/app/assets/javascripts/preferences/PreferencesMenu.tsx b/app/assets/javascripts/preferences/PreferencesMenu.tsx deleted file mode 100644 index 06ed02dd548..00000000000 --- a/app/assets/javascripts/preferences/PreferencesMenu.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; -import { MenuItem } from './components'; -import { PreferencesMenu } from './models/preferences'; - -export const PreferencesMenuView: FunctionComponent<{ - preferences: PreferencesMenu; -}> = observer(({ preferences }) => ( - <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> - {preferences.menuItems.map((pref) => ( - <MenuItem - key={pref.id} - iconType={pref.icon} - label={pref.label} - selected={pref.selected} - onClick={() => preferences.selectPane(pref.id)} - /> - ))} - </div> -)); diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index b1ce7604de8..b9492cad9ee 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -1,14 +1,10 @@ import { RoundIconButton } from '@/components/RoundIconButton'; import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; -import { PreferencesMenu } from './models/preferences'; -import { PreferencesMenuView } from './PreferencesMenu'; import { HelpAndFeedback, Security } from './panes'; import { observer } from 'mobx-react-lite'; - -interface PreferencesViewProps { - close: () => void; -} +import { MenuItem } from './components'; +import { PreferencesMenu } from './preferences-menu'; const PaneSelector: FunctionComponent<{ prefs: PreferencesMenu; @@ -39,12 +35,16 @@ const PreferencesCanvas: FunctionComponent<{ preferences: PreferencesMenu; }> = observer(({ preferences: prefs }) => ( <div className="flex flex-row flex-grow min-h-0 justify-between"> - <PreferencesMenuView preferences={prefs}></PreferencesMenuView> + <PreferencesMenuView menu={prefs}></PreferencesMenuView> <PaneSelector prefs={prefs} /> </div> )); -const PreferencesView: FunctionComponent<PreferencesViewProps> = observer( +interface PreferencesViewProps { + close: () => void; +} + +const PreferencesView: FunctionComponent<{ close: () => void }> = observer( ({ close }) => { const prefs = new PreferencesMenu(); return ( @@ -78,3 +78,19 @@ export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> <PreferencesView close={() => appState.preferences.closePreferences()} /> ); }); + +export const PreferencesMenuView: FunctionComponent<{ + menu: PreferencesMenu; +}> = observer(({ menu }) => ( + <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> + {menu.menuItems.map((pref) => ( + <MenuItem + key={pref.id} + iconType={pref.icon} + label={pref.label} + selected={pref.selected} + onClick={() => menu.selectPane(pref.id)} + /> + ))} + </div> +)); diff --git a/app/assets/javascripts/preferences/models/index.ts b/app/assets/javascripts/preferences/models/index.ts deleted file mode 100644 index eee27f7f4f8..00000000000 --- a/app/assets/javascripts/preferences/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './preferences'; -export * from './two-factor-auth'; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx new file mode 100644 index 00000000000..2ce9bf100c6 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -0,0 +1,37 @@ +import { IconButton } from '@/components/IconButton'; +import { FunctionComponent } from 'preact'; +import { useState, useRef, useEffect } from 'react'; + +export const AuthAppInfoPopup: FunctionComponent = () => { + const [shown, setShow] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const dismiss = () => setShow(false); + document.addEventListener('mousedown', dismiss); + return () => { + document.removeEventListener('mousedown', dismiss); + }; + }, [ref]); + + return ( + <div className="relative"> + <IconButton + icon="info" + className="mt-1" + onClick={() => { + setShow(!shown); + }} + /> + {shown && ( + <div + className={`bg-black color-white text-center rounded shadow-overlay \ + py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} + > + Some apps, like Google Authenticator, do not back up and restore your + secret keys if you lose your device or get a new one. + </div> + )} + </div> + ); +}; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx similarity index 100% rename from app/assets/javascripts/preferences/panes/two-factor-auth/email-recovery.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx similarity index 91% rename from app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index 963ba8eba49..f02de536844 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/save-secret-key.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -1,15 +1,15 @@ import { Button } from '@/components/Button'; import { IconButton } from '@/components/IconButton'; -import { TwoFactorActivation } from '@/preferences/models'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; +import { downloadSecretKey } from './download-secret-key'; +import { TwoFactorActivation } from './model'; import { - downloadSecretKey, TwoFactorDialog, TwoFactorDialogLabel, TwoFactorDialogDescription, TwoFactorDialogButtons, -} from './utils'; +} from './TwoFactorDialog'; export const SaveSecretKey: FunctionComponent<{ activation: TwoFactorActivation; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx similarity index 93% rename from app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx index abc02a58ce7..8cc87beb6dd 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/scan-qr-code.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -2,15 +2,15 @@ import { FunctionComponent } from 'preact'; import { observer } from 'mobx-react-lite'; import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; -import { TwoFactorActivation } from '../../models'; import { Button } from '@/components/Button'; +import { AuthAppInfoPopup } from './download-secret-key'; +import { TwoFactorActivation } from './model'; import { - AuthAppInfoPopup, TwoFactorDialog, - TwoFactorDialogButtons, - TwoFactorDialogDescription, TwoFactorDialogLabel, -} from './utils'; + TwoFactorDialogDescription, + TwoFactorDialogButtons, +} from './TwoFactorDialog'; export const ScanQRCode: FunctionComponent<{ activation: TwoFactorActivation; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx similarity index 82% rename from app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index 27b19dbf0e8..d41939ae2d9 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/view.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -9,25 +9,12 @@ import { Switch } from '../../../components/Switch'; import { observer } from 'mobx-react-lite'; import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; -import { TwoFactorActivation, TwoFactorAuth } from '../../models'; -import { ScanQRCode } from './scan-qr-code'; -import { EmailRecovery } from './email-recovery'; -import { SaveSecretKey } from './save-secret-key'; -import { Verification } from './verification'; - -// Temporary implementation until integration -function downloadSecretKey(text: string) { - const link = document.createElement('a'); - const blob = new Blob([text], { - type: 'text/plain;charset=utf-8', - }); - link.href = window.URL.createObjectURL(blob); - link.setAttribute('download', 'secret_key.txt'); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(link.href); -} +import { ScanQRCode } from './ScanQRCode'; +import { EmailRecovery } from './EmailRecovery'; +import { SaveSecretKey } from './SaveSecretKey'; +import { Verification } from './Verification'; +import { TwoFactorActivation, TwoFactorAuth } from './model'; +import { downloadSecretKey } from './download-secret-key'; export const TwoFactorAuthView: FunctionComponent<{ tfAuth: TwoFactorAuth; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx similarity index 52% rename from app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index fef01e1df57..d6b1eb17b5b 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/utils.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -5,55 +5,7 @@ import { AlertDialogDescription, AlertDialogLabel, } from '@reach/alert-dialog'; -import { useEffect, useRef, useState } from 'preact/hooks'; - -// Temporary implementation until integration -export function downloadSecretKey(text: string) { - const link = document.createElement('a'); - const blob = new Blob([text], { - type: 'text/plain;charset=utf-8', - }); - link.href = window.URL.createObjectURL(blob); - link.setAttribute('download', 'secret_key.txt'); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(link.href); -} - -export const AuthAppInfoPopup: FunctionComponent = () => { - const [shown, setShow] = useState(false); - const ref = useRef(null); - - useEffect(() => { - const dismiss = () => setShow(false); - document.addEventListener('mousedown', dismiss); - return () => { - document.removeEventListener('mousedown', dismiss); - }; - }, [ref]); - - return ( - <div className="relative"> - <IconButton - icon="info" - className="mt-1" - onClick={() => { - setShow(!shown); - }} - /> - {shown && ( - <div - className={`bg-black color-white text-center rounded shadow-overlay \ - py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} - > - Some apps, like Google Authenticator, do not back up and restore your - secret keys if you lose your device or get a new one. - </div> - )} - </div> - ); -}; +import { useRef } from 'preact/hooks'; export const TwoFactorDialog: FunctionComponent<{ children: ComponentChildren; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx similarity index 78% rename from app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx rename to app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx index f9f82ab779c..74fc96f40db 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -1,6 +1,6 @@ -import { TwoFactorActivation } from '@/preferences/models'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; +import { TwoFactorActivation } from './model'; export const Verification: FunctionComponent<{ activation: TwoFactorActivation; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/download-secret-key.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/download-secret-key.tsx new file mode 100644 index 00000000000..076a33e2a5e --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/download-secret-key.tsx @@ -0,0 +1,13 @@ +// Temporary implementation until integration +export function downloadSecretKey(text: string) { + const link = document.createElement('a'); + const blob = new Blob([text], { + type: 'text/plain;charset=utf-8', + }); + link.href = window.URL.createObjectURL(blob); + link.setAttribute('download', 'secret_key.txt'); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(link.href); +} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx index 61293e22177..bc881642ba0 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx @@ -1,8 +1,8 @@ -import { TwoFactorAuth } from '../../models'; import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -import { TwoFactorAuthView } from './view'; +import { TwoFactorAuth } from './model'; +import { TwoFactorAuthView } from './TwoFactorAuthView'; export const TwoFactorAuthWrapper: FunctionComponent = observer(() => { const tfAuth = observable(new TwoFactorAuth()); diff --git a/app/assets/javascripts/preferences/models/two-factor-auth.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts similarity index 100% rename from app/assets/javascripts/preferences/models/two-factor-auth.ts rename to app/assets/javascripts/preferences/panes/two-factor-auth/model.ts diff --git a/app/assets/javascripts/preferences/models/preferences.ts b/app/assets/javascripts/preferences/preferences-menu.ts similarity index 97% rename from app/assets/javascripts/preferences/models/preferences.ts rename to app/assets/javascripts/preferences/preferences-menu.ts index 2fbfecb56b1..7ad24835b65 100644 --- a/app/assets/javascripts/preferences/models/preferences.ts +++ b/app/assets/javascripts/preferences/preferences-menu.ts @@ -1,6 +1,5 @@ import { IconType } from '@/components/Icon'; import { makeAutoObservable, observable } from 'mobx'; -import { TwoFactorAuth } from './two-factor-auth'; const PREFERENCE_IDS = [ 'general', From 7229880ca24a7f03b6589d17c1e701b16847cf4b Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 15:41:57 +0200 Subject: [PATCH 03/14] feat: implement save secret key dialog for 2FA --- .../two-factor-auth/AuthAppInfoPopup.tsx | 9 ++++++- .../panes/two-factor-auth/EmailRecovery.tsx | 7 ----- .../panes/two-factor-auth/SaveSecretKey.tsx | 27 ++++++++++++++++--- .../panes/two-factor-auth/ScanQRCode.tsx | 4 +-- .../two-factor-auth/TwoFactorAuthView.tsx | 3 --- .../panes/two-factor-auth/TwoFactorDialog.tsx | 1 - .../panes/two-factor-auth/model.ts | 23 ++-------------- 7 files changed, 36 insertions(+), 38 deletions(-) delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx index 2ce9bf100c6..fd2f715290e 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -2,7 +2,14 @@ import { IconButton } from '@/components/IconButton'; import { FunctionComponent } from 'preact'; import { useState, useRef, useEffect } from 'react'; -export const AuthAppInfoPopup: FunctionComponent = () => { +/** + * AuthAppInfoPopup is an info icon that shows a tooltip when clicked + * Tooltip is dismissible by clicking outside + * + * Note: it can be generalized but more use cases are required + * @returns + */ +export const AuthAppInfoTooltip: FunctionComponent = () => { const [shown, setShow] = useState(false); const ref = useRef(null); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx deleted file mode 100644 index e02d368dd96..00000000000 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/EmailRecovery.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { TwoFactorActivation } from '@/preferences/models'; -import { observer } from 'mobx-react-lite'; -import { FunctionComponent } from 'preact'; - -export const EmailRecovery: FunctionComponent<{ - activation: TwoFactorActivation; -}> = observer(({ activation }) => <></>); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index f02de536844..df84b0a9cd3 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -1,4 +1,5 @@ import { Button } from '@/components/Button'; +import { DecoratedInput } from '@/components/DecoratedInput'; import { IconButton } from '@/components/IconButton'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; @@ -37,12 +38,32 @@ export const SaveSecretKey: FunctionComponent<{ </TwoFactorDialogLabel> <TwoFactorDialogDescription> <div className="flex-grow flex flex-col gap-2"> - <div className="text-sm"> - ・<b>Save your secret key</b> somewhere safe: + <div className="flex flex-row items-center gap-1"> + <div className="text-sm"> + ・<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" + > + somewhere safe + </a> + : + </div> + <DecoratedInput + disabled={true} + right={[copy, 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. Learn more + 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> </TwoFactorDialogDescription> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx index 8cc87beb6dd..3a206318af6 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -3,7 +3,6 @@ import { observer } from 'mobx-react-lite'; import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; import { Button } from '@/components/Button'; -import { AuthAppInfoPopup } from './download-secret-key'; import { TwoFactorActivation } from './model'; import { TwoFactorDialog, @@ -11,6 +10,7 @@ import { TwoFactorDialogDescription, TwoFactorDialogButtons, } from './TwoFactorDialog'; +import { AuthAppInfoTooltip } from './AuthAppInfoPopup'; export const ScanQRCode: FunctionComponent<{ activation: TwoFactorActivation; @@ -38,7 +38,7 @@ export const ScanQRCode: FunctionComponent<{ <div className="text-sm"> ・Open your <b>authenticator app</b>. </div> - <AuthAppInfoPopup /> + <AuthAppInfoTooltip /> </div> <div className="flex flex-row items-center"> <div className="text-sm flex-grow"> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index d41939ae2d9..ea0ad1352b6 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -10,7 +10,6 @@ import { observer } from 'mobx-react-lite'; import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; import { ScanQRCode } from './ScanQRCode'; -import { EmailRecovery } from './EmailRecovery'; import { SaveSecretKey } from './SaveSecretKey'; import { Verification } from './Verification'; import { TwoFactorActivation, TwoFactorAuth } from './model'; @@ -110,8 +109,6 @@ export const TwoFactorActivationView: FunctionComponent<{ {act.step === 'save-secret-key' && <SaveSecretKey activation={act} />} - {act.step === 'email-recovery' && <EmailRecovery activation={act} />} - {act.step === 'verification' && <Verification activation={act} />} </> )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index d6b1eb17b5b..9b25512fe63 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -10,7 +10,6 @@ import { useRef } from 'preact/hooks'; export const TwoFactorDialog: FunctionComponent<{ children: ComponentChildren; }> = ({ children }) => { - // TODO discover what this does const ldRef = useRef<HTMLButtonElement>(); return ( diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 06498d0d614..57b658e8461 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -23,7 +23,6 @@ export class TwoFactorActivation { private _secretKey: string; private _authCode: string; - private _allowEmailRecovery: boolean = false; private _2FAVerification: 'none' | 'invalid' | 'valid' = 'none'; constructor( @@ -36,16 +35,11 @@ export class TwoFactorActivation { makeAutoObservable< TwoFactorActivation, - | '_secretKey' - | '_authCode' - | '_step' - | '_allowEmailRecovery' - | '_enable2FAVerification' + '_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' >(this, { _secretKey: observable, _authCode: observable, _step: observable, - _allowEmailRecovery: observable, _enable2FAVerification: observable, }); } @@ -58,10 +52,6 @@ export class TwoFactorActivation { return this._authCode; } - get allowEmailRecovery() { - return this._allowEmailRecovery; - } - get step() { return this._step; } @@ -83,20 +73,11 @@ export class TwoFactorActivation { } nextSaveSecretKey() { - this._step = 'email-recovery'; - } - - backEmailRecovery() { - this._step = 'save-secret-key'; - } - - nextEmailRecovery(allowEmailRecovery: boolean) { this._step = 'verification'; - this._allowEmailRecovery = allowEmailRecovery; } backVerification() { - this._step = 'email-recovery'; + this._step = 'save-secret-key'; } enable2FA(secretKey: string, authCode: string) { From 33db79ae339496053dc350fd782934d6cbed06e7 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 16:02:38 +0200 Subject: [PATCH 04/14] feat: verification dialog from 2FA activation flow --- .../panes/two-factor-auth/SaveSecretKey.tsx | 2 +- .../panes/two-factor-auth/ScanQRCode.tsx | 2 +- .../panes/two-factor-auth/Verification.tsx | 39 ++++++++++++++++++- .../panes/two-factor-auth/model.ts | 4 +- 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index df84b0a9cd3..3dea8ef5e7a 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -34,7 +34,7 @@ export const SaveSecretKey: FunctionComponent<{ return ( <TwoFactorDialog> <TwoFactorDialogLabel close={() => {}}> - Step 2 of 4 - Save secret key + Step 2 of 3 - Save secret key </TwoFactorDialogLabel> <TwoFactorDialogDescription> <div className="flex-grow flex flex-col gap-2"> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx index 3a206318af6..fbbddb9e627 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -26,7 +26,7 @@ export const ScanQRCode: FunctionComponent<{ return ( <TwoFactorDialog> <TwoFactorDialogLabel close={() => {}}> - Step 1 of 4 - Scan QR code + Step 1 of 3 - Scan QR code </TwoFactorDialogLabel> <TwoFactorDialogDescription> <div className="flex flex-row gap-3 items-center"> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx index 74fc96f40db..121cf163f04 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -1,7 +1,44 @@ +import { Button } from '@/components/Button'; +import { DecoratedInput } from '@/components/DecoratedInput'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { TwoFactorActivation } from './model'; +import { + TwoFactorDialog, + TwoFactorDialogLabel, + TwoFactorDialogDescription, + TwoFactorDialogButtons, +} from './TwoFactorDialog'; export const Verification: FunctionComponent<{ activation: TwoFactorActivation; -}> = observer(({ activation }) => <></>); +}> = observer(({ activation: act }) => { + return ( + <TwoFactorDialog> + <TwoFactorDialogLabel close={() => {}}> + Step 3 of 3 - Verification + </TwoFactorDialogLabel> + <TwoFactorDialogDescription> + <div className="flex-grow flex flex-col gap-2"> + <div className="flex flex-row items-center gap-2"> + <div className="text-sm"> + ・Enter your <b>secret key</b>: + </div> + <DecoratedInput /> + </div> + <div className="flex flex-row items-center gap-2"> + <div className="text-sm"> + ・Verify the <b>authentication code</b> generated by your + authenticator app: + </div> + <DecoratedInput className="w-30" /> + </div> + </div> + </TwoFactorDialogDescription> + <TwoFactorDialogButtons> + <Button className="min-w-20" type="normal" label="Back" /> + <Button className="min-w-20" type="primary" label="Next" /> + </TwoFactorDialogButtons> + </TwoFactorDialog> + ); +}); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 57b658e8461..675dcadadc2 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -10,7 +10,6 @@ function getNewAuthCode() { const activationSteps = [ 'scan-qr-code', 'save-secret-key', - 'email-recovery', 'verification', ] as const; @@ -31,7 +30,7 @@ export class TwoFactorActivation { ) { this._secretKey = 'FHJJSAJKDASKW43KJS'; this._authCode = getNewAuthCode(); - this._step = 'save-secret-key'; + this._step = 'verification'; makeAutoObservable< TwoFactorActivation, @@ -78,6 +77,7 @@ export class TwoFactorActivation { backVerification() { this._step = 'save-secret-key'; + this._2FAVerification = 'none'; } enable2FA(secretKey: string, authCode: string) { From 499ed67a1f4cdd86946ab3e8eb7ef69d8f58ea8c Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 17:39:17 +0200 Subject: [PATCH 05/14] feat: connect dialogs flow to mobx store --- app/assets/javascripts/components/Button.tsx | 15 +++++- .../panes/two-factor-auth/SaveSecretKey.tsx | 20 ++++++-- .../panes/two-factor-auth/ScanQRCode.tsx | 20 ++++++-- .../two-factor-auth/TwoFactorAuthView.tsx | 20 ++++---- .../panes/two-factor-auth/TwoFactorDialog.tsx | 12 +++-- .../panes/two-factor-auth/Verification.tsx | 31 ++++++++++-- .../panes/two-factor-auth/model.ts | 47 ++++++++++--------- .../preferences/preferences-menu.ts | 2 +- 8 files changed, 116 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index a8b9ff8be17..c5d014c3550 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -11,7 +11,18 @@ export const Button: FunctionComponent<{ className?: string; type: 'normal' | 'primary'; label: string; -}> = ({ type, label, className = '' }) => { + onClick: () => void; +}> = ({ type, label, className = '', onClick }) => { const buttonClass = type === 'primary' ? primary : normal; - return <button className={`${buttonClass} ${className}`}>{label}</button>; + return ( + <button + className={`${buttonClass} ${className}`} + onClick={(e) => { + onClick(); + e.preventDefault(); + }} + > + {label} + </button> + ); }; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx index 3dea8ef5e7a..34e060b7303 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -33,7 +33,11 @@ export const SaveSecretKey: FunctionComponent<{ ); return ( <TwoFactorDialog> - <TwoFactorDialogLabel close={() => {}}> + <TwoFactorDialogLabel + closeDialog={() => { + act.cancelActivation(); + }} + > Step 2 of 3 - Save secret key </TwoFactorDialogLabel> <TwoFactorDialogDescription> @@ -68,8 +72,18 @@ export const SaveSecretKey: FunctionComponent<{ </div> </TwoFactorDialogDescription> <TwoFactorDialogButtons> - <Button className="min-w-20" type="normal" label="Back" /> - <Button className="min-w-20" type="primary" label="Next" /> + <Button + className="min-w-20" + type="normal" + label="Back" + onClick={() => act.openScanQRCode()} + /> + <Button + className="min-w-20" + type="primary" + label="Next" + onClick={() => act.openVerification()} + /> </TwoFactorDialogButtons> </TwoFactorDialog> ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx index fbbddb9e627..82c73769e45 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -25,7 +25,11 @@ export const ScanQRCode: FunctionComponent<{ ); return ( <TwoFactorDialog> - <TwoFactorDialogLabel close={() => {}}> + <TwoFactorDialogLabel + closeDialog={() => { + act.cancelActivation(); + }} + > Step 1 of 3 - Scan QR code </TwoFactorDialogLabel> <TwoFactorDialogDescription> @@ -56,8 +60,18 @@ export const ScanQRCode: FunctionComponent<{ </div> </TwoFactorDialogDescription> <TwoFactorDialogButtons> - <Button className="min-w-20" type="normal" label="Cancel" /> - <Button className="min-w-20" type="primary" label="Next" /> + <Button + className="min-w-20" + type="normal" + label="Cancel" + onClick={() => act.cancelActivation()} + /> + <Button + className="min-w-20" + type="primary" + label="Next" + onClick={() => act.openSaveSecretKey()} + /> </TwoFactorDialogButtons> </TwoFactorDialog> ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index ea0ad1352b6..3861222550a 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -16,8 +16,8 @@ import { TwoFactorActivation, TwoFactorAuth } from './model'; import { downloadSecretKey } from './download-secret-key'; export const TwoFactorAuthView: FunctionComponent<{ - tfAuth: TwoFactorAuth; -}> = observer(({ tfAuth }) => ( + auth: TwoFactorAuth; +}> = observer(({ auth }) => ( <PreferencesGroup> <PreferencesSegment> <div className="flex flex-row items-center"> @@ -28,24 +28,24 @@ export const TwoFactorAuthView: FunctionComponent<{ </Text> </div> <Switch - checked={tfAuth.enabled !== false} - onChange={() => tfAuth.toggle2FA()} + checked={auth.enabled !== false} + onChange={() => auth.toggle2FA()} /> </div> </PreferencesSegment> <PreferencesSegment> - {tfAuth.enabled && ( + {auth.enabled && ( <TwoFactorEnabledView - secretKey={tfAuth.enabled.secretKey} - authCode={tfAuth.enabled.authCode} + secretKey={auth.enabled.secretKey} + authCode={auth.enabled.authCode} /> )} - {tfAuth.activation instanceof TwoFactorActivation && ( - <TwoFactorActivationView activation={tfAuth.activation} /> + {auth.activation instanceof TwoFactorActivation && ( + <TwoFactorActivationView activation={auth.activation} /> )} - {tfAuth.enabled === false && <TwoFactorDisabledView />} + {auth.enabled === false && <TwoFactorDisabledView />} </PreferencesSegment> </PreferencesGroup> )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index 9b25512fe63..5e2fbb4f872 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -7,6 +7,10 @@ import { } from '@reach/alert-dialog'; import { useRef } from 'preact/hooks'; +/** + * TwoFactorDialog is AlertDialog styled for 2FA + * Can be generalized but more use cases are needed + */ export const TwoFactorDialog: FunctionComponent<{ children: ComponentChildren; }> = ({ children }) => { @@ -23,9 +27,9 @@ export const TwoFactorDialog: FunctionComponent<{ ); }; -export const TwoFactorDialogLabel: FunctionComponent<{ close: () => void }> = ({ - children, -}) => ( +export const TwoFactorDialogLabel: FunctionComponent<{ + closeDialog: () => void; +}> = ({ children, closeDialog }) => ( <AlertDialogLabel className=""> <div className="px-4 py-4 flex flex-row"> <div className="flex-grow color-black text-lg-sm-lh font-bold"> @@ -34,7 +38,7 @@ export const TwoFactorDialogLabel: FunctionComponent<{ close: () => void }> = ({ <IconButton className="color-grey-1 h-5 w-5" icon="close" - onClick={close} + onClick={() => closeDialog()} /> </div> <hr className="h-1px bg-border no-border m-0" /> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx index 121cf163f04..cddc99cd6f8 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -13,9 +13,15 @@ import { export const Verification: FunctionComponent<{ activation: TwoFactorActivation; }> = observer(({ activation: act }) => { + const borderInv = + act.verificationStatus === 'invalid' ? 'border-dark-red' : ''; return ( <TwoFactorDialog> - <TwoFactorDialogLabel close={() => {}}> + <TwoFactorDialogLabel + closeDialog={() => { + act.cancelActivation(); + }} + > Step 3 of 3 - Verification </TwoFactorDialogLabel> <TwoFactorDialogDescription> @@ -24,20 +30,35 @@ export const Verification: FunctionComponent<{ <div className="text-sm"> ・Enter your <b>secret key</b>: </div> - <DecoratedInput /> + <DecoratedInput className={borderInv} /> </div> <div className="flex flex-row items-center gap-2"> <div className="text-sm"> ・Verify the <b>authentication code</b> generated by your authenticator app: </div> - <DecoratedInput className="w-30" /> + <DecoratedInput className={`w-30 ${borderInv}`} /> </div> </div> </TwoFactorDialogDescription> <TwoFactorDialogButtons> - <Button className="min-w-20" type="normal" label="Back" /> - <Button className="min-w-20" type="primary" label="Next" /> + {act.verificationStatus === 'invalid' && ( + <div className="text-sm color-dark-red"> + Incorrect credentials, please try again. + </div> + )} + <Button + className="min-w-20" + type="normal" + label="Back" + onClick={() => act.openSaveSecretKey()} + /> + <Button + className="min-w-20" + type="primary" + label="Next" + onClick={() => act.enable2FA('X', 'X')} + /> </TwoFactorDialogButtons> </TwoFactorDialog> ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 675dcadadc2..9cc1030d276 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -30,17 +30,21 @@ export class TwoFactorActivation { ) { this._secretKey = 'FHJJSAJKDASKW43KJS'; this._authCode = getNewAuthCode(); - this._step = 'verification'; + this._step = 'scan-qr-code'; makeAutoObservable< TwoFactorActivation, '_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' - >(this, { - _secretKey: observable, - _authCode: observable, - _step: observable, - _enable2FAVerification: observable, - }); + >( + this, + { + _secretKey: observable, + _authCode: observable, + _step: observable, + _enable2FAVerification: observable, + }, + { autoBind: true } + ); } get secretKey() { @@ -55,28 +59,24 @@ export class TwoFactorActivation { return this._step; } - get enable2FAVerification() { + get verificationStatus() { return this._2FAVerification; } cancelActivation() { - this._cancelActivation; - } - - nextScanQRCode() { - this._step = 'save-secret-key'; + this._cancelActivation(); } - backSaveSecretKey() { + openScanQRCode() { this._step = 'scan-qr-code'; } - nextSaveSecretKey() { - this._step = 'verification'; + openSaveSecretKey() { + this._step = 'save-secret-key'; } - backVerification() { - this._step = 'save-secret-key'; + openVerification() { + this._step = 'verification'; this._2FAVerification = 'none'; } @@ -87,7 +87,10 @@ export class TwoFactorActivation { return; } - this._2FAVerification = 'invalid'; + // TODO remove this + this._2FAVerification = 'valid'; + this._enable2FA(secretKey); + // this._2FAVerification = 'invalid'; } } @@ -123,15 +126,13 @@ export class TwoFactorAuth { private _status: | TwoFactorEnabled | TwoFactorActivation - | 'two-factor-disabled' = new TwoFactorActivation( - () => {}, - () => {} - ); + | 'two-factor-disabled'; constructor() { makeAutoObservable<TwoFactorAuth, '_status'>(this, { _status: observable, }); + this._status = 'two-factor-disabled'; } private startActivation() { diff --git a/app/assets/javascripts/preferences/preferences-menu.ts b/app/assets/javascripts/preferences/preferences-menu.ts index 7ad24835b65..8d0492ed79d 100644 --- a/app/assets/javascripts/preferences/preferences-menu.ts +++ b/app/assets/javascripts/preferences/preferences-menu.ts @@ -37,7 +37,7 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ export class PreferencesMenu { // TODO change to 'general' before merge - private _selectedPane: PreferenceId = 'security'; + private _selectedPane: PreferenceId = 'general'; constructor( private readonly _menu: PreferencesMenuItem[] = PREFERENCES_MENU_ITEMS From 5bb0a06e65bcbdac692e05373a5d1a6238d4696d Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 17:56:32 +0200 Subject: [PATCH 06/14] fix: set initial value to 2FA status before makeAutoObservable --- .../preferences/panes/two-factor-auth/TwoFactorAuthView.tsx | 4 ++-- .../javascripts/preferences/panes/two-factor-auth/index.tsx | 4 ++-- .../javascripts/preferences/panes/two-factor-auth/model.ts | 5 ++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index 3861222550a..c80c20c5f86 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -34,14 +34,14 @@ export const TwoFactorAuthView: FunctionComponent<{ </div> </PreferencesSegment> <PreferencesSegment> - {auth.enabled && ( + {auth.enabled !== false && ( <TwoFactorEnabledView secretKey={auth.enabled.secretKey} authCode={auth.enabled.authCode} /> )} - {auth.activation instanceof TwoFactorActivation && ( + {auth.activation !== false && ( <TwoFactorActivationView activation={auth.activation} /> )} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx index bc881642ba0..da4d4d26d72 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx @@ -5,6 +5,6 @@ import { TwoFactorAuth } from './model'; import { TwoFactorAuthView } from './TwoFactorAuthView'; export const TwoFactorAuthWrapper: FunctionComponent = observer(() => { - const tfAuth = observable(new TwoFactorAuth()); - return <TwoFactorAuthView tfAuth={tfAuth} />; + const auth = observable(new TwoFactorAuth()); + return <TwoFactorAuthView auth={auth} />; }); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 9cc1030d276..f7d8cf25c95 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -1,4 +1,4 @@ -import { action, makeAutoObservable, observable } from 'mobx'; +import { action, makeAutoObservable, observable, untracked } from 'mobx'; function getNewAuthCode() { const MIN = 100000; @@ -126,13 +126,12 @@ export class TwoFactorAuth { private _status: | TwoFactorEnabled | TwoFactorActivation - | 'two-factor-disabled'; + | 'two-factor-disabled' = 'two-factor-disabled'; constructor() { makeAutoObservable<TwoFactorAuth, '_status'>(this, { _status: observable, }); - this._status = 'two-factor-disabled'; } private startActivation() { From 44806f2165bae055c16463b5f7d57aeb39fadf8c Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 19:04:49 +0200 Subject: [PATCH 07/14] fix: remove unnecessary observers --- app/assets/javascripts/preferences/PreferencesView.tsx | 10 +++++++++- app/assets/javascripts/preferences/panes/Security.tsx | 5 ++--- .../preferences/panes/two-factor-auth/index.tsx | 8 ++++---- .../ui_models/app_state/preferences_state.ts | 3 +-- 4 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index b9492cad9ee..e2f974c92f5 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -44,11 +44,19 @@ interface PreferencesViewProps { close: () => void; } +const blockPropagation = (e: MouseEvent) => { + e.stopPropagation(); +}; + const PreferencesView: FunctionComponent<{ close: () => void }> = observer( ({ close }) => { const prefs = new PreferencesMenu(); + return ( - <div className="sn-full-screen flex flex-col bg-contrast z-index-preferences"> + <div + onClick={blockPropagation} + className="sn-full-screen flex flex-col bg-contrast z-index-preferences" + > <TitleBar className="items-center justify-between"> {/* div is added so flex justify-between can center the title */} <div className="h-8 w-8" /> diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index 7269e485a6d..c344e0c857a 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -1,10 +1,9 @@ -import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; import { TwoFactorAuthWrapper } from './two-factor-auth'; -export const Security: FunctionComponent = observer(() => ( +export const Security: FunctionComponent = () => ( <PreferencesPane> <TwoFactorAuthWrapper /> </PreferencesPane> -)); +); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx index da4d4d26d72..71dde9df35e 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx @@ -1,10 +1,10 @@ -import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; +import { useState } from 'preact/hooks'; import { TwoFactorAuth } from './model'; import { TwoFactorAuthView } from './TwoFactorAuthView'; -export const TwoFactorAuthWrapper: FunctionComponent = observer(() => { - const auth = observable(new TwoFactorAuth()); +export const TwoFactorAuthWrapper: FunctionComponent = () => { + const [auth] = useState(() => new TwoFactorAuth()); return <TwoFactorAuthView auth={auth} />; -}); +}; diff --git a/app/assets/javascripts/ui_models/app_state/preferences_state.ts b/app/assets/javascripts/ui_models/app_state/preferences_state.ts index 24192fdae1f..607cd23e351 100644 --- a/app/assets/javascripts/ui_models/app_state/preferences_state.ts +++ b/app/assets/javascripts/ui_models/app_state/preferences_state.ts @@ -1,8 +1,7 @@ import { action, computed, makeObservable, observable } from 'mobx'; export class PreferencesState { - // TODO change to false before merge - private _open = true; + private _open = false; constructor() { makeObservable<PreferencesState, '_open'>(this, { From 85431cd3a35d88e99a839acaf0c9e15d991af8a7 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Wed, 21 Jul 2021 19:13:32 +0200 Subject: [PATCH 08/14] feat: log auth code and secret key to console for testing --- .../preferences/panes/two-factor-auth/model.ts | 9 +++++---- app/assets/javascripts/preferences/preferences-menu.ts | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index f7d8cf25c95..1d056e1f7bf 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -32,6 +32,10 @@ export class TwoFactorActivation { this._authCode = getNewAuthCode(); this._step = 'scan-qr-code'; + // Only present for early testing + console.log('Secret Key', this._secretKey); + console.log('Auth code', this._authCode); + makeAutoObservable< TwoFactorActivation, '_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' @@ -87,10 +91,7 @@ export class TwoFactorActivation { return; } - // TODO remove this - this._2FAVerification = 'valid'; - this._enable2FA(secretKey); - // this._2FAVerification = 'invalid'; + this._2FAVerification = 'invalid'; } } diff --git a/app/assets/javascripts/preferences/preferences-menu.ts b/app/assets/javascripts/preferences/preferences-menu.ts index 8d0492ed79d..2d1c7585676 100644 --- a/app/assets/javascripts/preferences/preferences-menu.ts +++ b/app/assets/javascripts/preferences/preferences-menu.ts @@ -36,7 +36,6 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ ]; export class PreferencesMenu { - // TODO change to 'general' before merge private _selectedPane: PreferenceId = 'general'; constructor( From 789dc0fc075f1d175c04333599586fd12d110a11 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Thu, 22 Jul 2021 16:21:47 +0200 Subject: [PATCH 09/14] feat(2fa): circle progress 30s refresh, better focus for DecoratedInput --- .../javascripts/components/CircleProgress.tsx | 38 +++++++++++++++++++ .../javascripts/components/DecoratedInput.tsx | 4 +- .../preferences/PreferencesView.tsx | 4 -- .../two-factor-auth/TwoFactorAuthView.tsx | 27 +++++++++++-- .../panes/two-factor-auth/TwoFactorDialog.tsx | 4 +- .../panes/two-factor-auth/model.ts | 9 ++--- 6 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 app/assets/javascripts/components/CircleProgress.tsx diff --git a/app/assets/javascripts/components/CircleProgress.tsx b/app/assets/javascripts/components/CircleProgress.tsx new file mode 100644 index 00000000000..16fa917dc52 --- /dev/null +++ b/app/assets/javascripts/components/CircleProgress.tsx @@ -0,0 +1,38 @@ +import { FunctionComponent } from 'preact'; + +export const CircleProgress: FunctionComponent<{ + percent: number; + className?: string; +}> = ({ percent, className = '' }) => { + const size = 16; + const ratioStrokeRadius = 0.25; + const outerRadius = size / 2; + + const radius = outerRadius * (1 - ratioStrokeRadius); + const stroke = outerRadius - radius; + + const circumference = radius * 2 * Math.PI; + const offset = circumference - (percent / 100) * circumference; + + const transition = `transition: 0.35s stroke-dashoffset;`; + const transform = `transform: rotate(-90deg);`; + const transformOrigin = `transform-origin: 50% 50%;`; + const dasharray = `stroke-dasharray: ${circumference} ${circumference};`; + const dashoffset = `stroke-dashoffset: ${offset};`; + const style = `${transition} ${transform} ${transformOrigin} ${dasharray} ${dashoffset}`; + return ( + <div className="h-5 w-5 min-w-5 min-h-5"> + <svg viewBox={`0 0 ${size} ${size}`}> + <circle + stroke="#086DD6" + stroke-width={stroke} + fill="transparent" + r={radius} + cx="50%" + cy="50%" + style={style} + /> + </svg> + </div> + ); +}; diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index 860b6b36263..f649ab38fae 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -26,12 +26,12 @@ export const DecoratedInput: FunctionalComponent<Props> = ({ const classes = `${base} ${stateClasses} ${className}`; return ( - <div className={classes}> + <div className={`${classes} focus-within:ring-info`}> {left} <div className="flex-grow"> <input type="text" - className="w-full no-border color-black" + className="w-full no-border color-black focus:shadow-none" disabled={disabled} value={text} /> diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index e2f974c92f5..005f740006d 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -40,10 +40,6 @@ const PreferencesCanvas: FunctionComponent<{ </div> )); -interface PreferencesViewProps { - close: () => void; -} - const blockPropagation = (e: MouseEvent) => { e.stopPropagation(); }; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index c80c20c5f86..77628d01939 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -1,4 +1,4 @@ -import { FunctionComponent } from 'preact'; +import { FunctionalComponent, FunctionComponent } from 'preact'; import { Title, Text, @@ -14,6 +14,9 @@ import { SaveSecretKey } from './SaveSecretKey'; import { Verification } from './Verification'; import { TwoFactorActivation, TwoFactorAuth } from './model'; import { downloadSecretKey } from './download-secret-key'; +import { CircleProgress } from '@/components/CircleProgress'; +import { useState } from 'preact/hooks'; +import { useEffect } from 'react'; export const TwoFactorAuthView: FunctionComponent<{ auth: TwoFactorAuth; @@ -50,6 +53,24 @@ export const TwoFactorAuthView: FunctionComponent<{ </PreferencesGroup> )); +const ProgressTime: FunctionalComponent<{ time: number }> = ({ time }) => { + const [percent, setPercent] = useState(0); + const interval = time / 100; + useEffect(() => { + const tick = setInterval(() => { + if (percent === 100) { + setPercent(0); + } else { + setPercent(percent + 1); + } + }, interval); + return () => { + clearInterval(tick); + }; + }); + return <CircleProgress percent={percent} />; +}; + const TwoFactorEnabledView: FunctionComponent<{ secretKey: string; authCode: string; @@ -70,7 +91,7 @@ const TwoFactorEnabledView: FunctionComponent<{ }} /> ); - const spinner = <div class="sk-spinner info w-8 h-3.5" />; + const progress = <ProgressTime time={30000} />; return ( <div className="flex flex-row gap-4"> <div className="flex-grow flex flex-col"> @@ -83,7 +104,7 @@ const TwoFactorEnabledView: FunctionComponent<{ </div> <div className="w-30 flex flex-col"> <Text>Authentication Code</Text> - <DecoratedInput disabled={true} text={authCode} right={[spinner]} /> + <DecoratedInput disabled={true} text={authCode} right={[progress]} /> </div> </div> ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index 5e2fbb4f872..b9062342447 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -18,8 +18,8 @@ export const TwoFactorDialog: FunctionComponent<{ return ( <AlertDialog leastDestructiveRef={ldRef}> - <div className="sn-component w-160"> - <div className="w-160 bg-default rounded shadow-overlay"> + <div className="sn-component"> + <div className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info"> {children} </div> </div> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 1d056e1f7bf..4218a0caf38 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -32,10 +32,6 @@ export class TwoFactorActivation { this._authCode = getNewAuthCode(); this._step = 'scan-qr-code'; - // Only present for early testing - console.log('Secret Key', this._secretKey); - console.log('Auth code', this._authCode); - makeAutoObservable< TwoFactorActivation, '_secretKey' | '_authCode' | '_step' | '_enable2FAVerification' @@ -91,7 +87,10 @@ export class TwoFactorActivation { return; } - this._2FAVerification = 'invalid'; + // Change to invalid upon implementation + this._2FAVerification = 'valid'; + // Remove after implementation + this._enable2FA(secretKey); } } From 18fb0d1edc20d0f25775a51aa223247bed478c67 Mon Sep 17 00:00:00 2001 From: Gorjan <mogi57@gmail.com> Date: Thu, 22 Jul 2021 16:29:32 +0200 Subject: [PATCH 10/14] feat: use new stylekit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cb2cd4b7b43..1674d92b119 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "pug-loader": "^2.4.0", "sass-loader": "^8.0.2", "serve-static": "^1.14.1", - "sn-stylekit": "5.2.6", + "sn-stylekit": "5.2.7", "ts-loader": "^8.0.17", "typescript": "4.2.3", "typescript-eslint": "0.0.1-alpha.0", From 3d3c5f41167fc2299231dea0d115fdde35f33b19 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski <mogi57@gmail.com> Date: Thu, 22 Jul 2021 18:43:32 +0200 Subject: [PATCH 11/14] fix: lint error for InfoPopup --- .../preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx index fd2f715290e..b3f6d3beb75 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -32,8 +32,8 @@ export const AuthAppInfoTooltip: FunctionComponent = () => { /> {shown && ( <div - className={`bg-black color-white text-center rounded shadow-overlay \ - py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} + className={`bg-black color-white text-center rounded shadow-overlay +py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} > Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or get a new one. From 26fbf1a72b327b1f9f5762d29f93d90611631821 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski <mogi57@gmail.com> Date: Thu, 22 Jul 2021 20:20:17 +0200 Subject: [PATCH 12/14] fix: use standardised utility classes --- .../panes/two-factor-auth/AuthAppInfoPopup.tsx | 2 +- .../preferences/panes/two-factor-auth/TwoFactorDialog.tsx | 6 ++---- package.json | 2 +- yarn.lock | 8 ++++---- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx index b3f6d3beb75..c84b14a3b31 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -33,7 +33,7 @@ export const AuthAppInfoTooltip: FunctionComponent = () => { {shown && ( <div className={`bg-black color-white text-center rounded shadow-overlay -py-1.5 px-2 absolute w-103 top-neg-10 left-neg-51`} +py-1.5 px-2 absolute w-103 -top-10 -left-51`} > Some apps, like Google Authenticator, do not back up and restore your secret keys if you lose your device or get a new one. diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index b9062342447..0954d1ebbed 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -31,10 +31,8 @@ export const TwoFactorDialogLabel: FunctionComponent<{ closeDialog: () => void; }> = ({ children, closeDialog }) => ( <AlertDialogLabel className=""> - <div className="px-4 py-4 flex flex-row"> - <div className="flex-grow color-black text-lg-sm-lh font-bold"> - {children} - </div> + <div className="px-4 pt-4 pb-3 flex flex-row"> + <div className="flex-grow color-black text-lg font-bold">{children}</div> <IconButton className="color-grey-1 h-5 w-5" icon="close" diff --git a/package.json b/package.json index 1674d92b119..4fc5386aba9 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "pug-loader": "^2.4.0", "sass-loader": "^8.0.2", "serve-static": "^1.14.1", - "sn-stylekit": "5.2.7", + "sn-stylekit": "5.2.8", "ts-loader": "^8.0.17", "typescript": "4.2.3", "typescript-eslint": "0.0.1-alpha.0", diff --git a/yarn.lock b/yarn.lock index 4cd60b9680c..789fa1a7097 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7908,10 +7908,10 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -sn-stylekit@5.2.6: - version "5.2.6" - resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.6.tgz#3c63d4a6b20bb002b6add685e28abce4663475d2" - integrity sha512-jZI+D/evLwcJjNbxBueoczjEOjJZL61eGd4cluC0lpPYvkMwHGYj/gWi09KN1E16K++FQRNVvZ3/mMZjSfam2g== +sn-stylekit@5.2.8: + version "5.2.7" + resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.7.tgz#193a15d717c45d02354c7e574e9c10ddd47ff545" + integrity sha512-ORC+Gt8dKPTTO+JE91phYgw1LINltvJiC2Aq6zquo06ttEZIUv+Qnskx7hRmQCMfQ2HhU93xoBXT6V7POGlhCA== dependencies: "@reach/listbox" "^0.15.0" "@reach/menu-button" "^0.15.1" From 311f50d00cef43129e5fa08819e9987a3625ee87 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski <mogi57@gmail.com> Date: Mon, 26 Jul 2021 16:26:52 +0200 Subject: [PATCH 13/14] fix: address PR feedback items --- app/assets/javascripts/components/Button.tsx | 8 +- .../components/CircleProgressTime.tsx | 27 ++++++ .../preferences/PreferencesMenuView.tsx | 20 ++++ .../preferences/PreferencesView.tsx | 27 +----- .../two-factor-auth/AuthAppInfoPopup.tsx | 53 ++++++---- .../TwoFactorActivationView.tsx | 18 ++++ .../two-factor-auth/TwoFactorAuthView.tsx | 97 +------------------ .../panes/two-factor-auth/TwoFactorDialog.tsx | 11 ++- .../two-factor-auth/TwoFactorDisabledView.tsx | 14 +++ .../two-factor-auth/TwoFactorEnabledView.tsx | 45 +++++++++ .../panes/two-factor-auth/Verification.tsx | 2 +- yarn.lock | 6 +- 12 files changed, 182 insertions(+), 146 deletions(-) create mode 100644 app/assets/javascripts/components/CircleProgressTime.tsx create mode 100644 app/assets/javascripts/preferences/PreferencesMenuView.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx diff --git a/app/assets/javascripts/components/Button.tsx b/app/assets/javascripts/components/Button.tsx index c5d014c3550..4a383edda04 100644 --- a/app/assets/javascripts/components/Button.tsx +++ b/app/assets/javascripts/components/Button.tsx @@ -1,10 +1,10 @@ import { FunctionComponent } from 'preact'; -const base = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`; +const baseClass = `rounded px-4 py-1.75 font-bold text-sm fit-content cursor-pointer`; -const normal = `${base} bg-default color-text border-solid border-gray-300 border-1 \ +const normalClass = `${baseClass} bg-default color-text border-solid border-gray-300 border-1 \ focus:bg-contrast hover:bg-contrast`; -const primary = `${base} no-border bg-info color-info-contrast hover:brightness-130 \ +const primaryClass = `${baseClass} no-border bg-info color-info-contrast hover:brightness-130 \ focus:brightness-130`; export const Button: FunctionComponent<{ @@ -13,7 +13,7 @@ export const Button: FunctionComponent<{ label: string; onClick: () => void; }> = ({ type, label, className = '', onClick }) => { - const buttonClass = type === 'primary' ? primary : normal; + const buttonClass = type === 'primary' ? primaryClass : normalClass; return ( <button className={`${buttonClass} ${className}`} diff --git a/app/assets/javascripts/components/CircleProgressTime.tsx b/app/assets/javascripts/components/CircleProgressTime.tsx new file mode 100644 index 00000000000..1f7b6b7d96d --- /dev/null +++ b/app/assets/javascripts/components/CircleProgressTime.tsx @@ -0,0 +1,27 @@ +import { FunctionalComponent } from 'preact'; +import { useEffect, useState } from 'preact/hooks'; +import { CircleProgress } from './CircleProgress'; + +/** + * Circular progress bar which runs in a specified time interval + * @param time - time interval in ms + */ +export const CircleProgressTime: FunctionalComponent<{ time: number }> = ({ + time, +}) => { + const [percent, setPercent] = useState(0); + const interval = time / 100; + useEffect(() => { + const tick = setInterval(() => { + if (percent === 100) { + setPercent(0); + } else { + setPercent(percent + 1); + } + }, interval); + return () => { + clearInterval(tick); + }; + }); + return <CircleProgress percent={percent} />; +}; diff --git a/app/assets/javascripts/preferences/PreferencesMenuView.tsx b/app/assets/javascripts/preferences/PreferencesMenuView.tsx new file mode 100644 index 00000000000..d5cd62833c7 --- /dev/null +++ b/app/assets/javascripts/preferences/PreferencesMenuView.tsx @@ -0,0 +1,20 @@ +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { MenuItem } from './components'; +import { PreferencesMenu } from './preferences-menu'; + +export const PreferencesMenuView: FunctionComponent<{ + menu: PreferencesMenu; +}> = observer(({ menu }) => ( + <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> + {menu.menuItems.map((pref) => ( + <MenuItem + key={pref.id} + iconType={pref.icon} + label={pref.label} + selected={pref.selected} + onClick={() => menu.selectPane(pref.id)} + /> + ))} + </div> +)); diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 005f740006d..e1472fcbc14 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -3,8 +3,8 @@ import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; import { HelpAndFeedback, Security } from './panes'; import { observer } from 'mobx-react-lite'; -import { MenuItem } from './components'; import { PreferencesMenu } from './preferences-menu'; +import { PreferencesMenuView } from './PreferencesMenuView'; const PaneSelector: FunctionComponent<{ prefs: PreferencesMenu; @@ -40,19 +40,12 @@ const PreferencesCanvas: FunctionComponent<{ </div> )); -const blockPropagation = (e: MouseEvent) => { - e.stopPropagation(); -}; - const PreferencesView: FunctionComponent<{ close: () => void }> = observer( ({ close }) => { const prefs = new PreferencesMenu(); return ( - <div - onClick={blockPropagation} - className="sn-full-screen flex flex-col bg-contrast z-index-preferences" - > + <div className="sn-full-screen flex flex-col bg-contrast z-index-preferences"> <TitleBar className="items-center justify-between"> {/* div is added so flex justify-between can center the title */} <div className="h-8 w-8" /> @@ -82,19 +75,3 @@ export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> <PreferencesView close={() => appState.preferences.closePreferences()} /> ); }); - -export const PreferencesMenuView: FunctionComponent<{ - menu: PreferencesMenu; -}> = observer(({ menu }) => ( - <div className="min-w-55 overflow-y-auto flex flex-col px-3 py-6"> - {menu.menuItems.map((pref) => ( - <MenuItem - key={pref.id} - iconType={pref.icon} - label={pref.label} - selected={pref.selected} - onClick={() => menu.selectPane(pref.id)} - /> - ))} - </div> -)); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx index c84b14a3b31..c486a214128 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/AuthAppInfoPopup.tsx @@ -1,7 +1,26 @@ +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'; +const DisclosureIconButton: FunctionComponent<{ + className?: string; + icon: IconType; +}> = ({ className = '', icon }) => ( + <DisclosureButton + className={`no-border cursor-pointer bg-transparent hover:brightness-130 p-0 ${ + className ?? '' + }`} + > + <Icon type={icon} /> + </DisclosureButton> +); + /** * AuthAppInfoPopup is an info icon that shows a tooltip when clicked * Tooltip is dismissible by clicking outside @@ -10,11 +29,11 @@ import { useState, useRef, useEffect } from 'react'; * @returns */ export const AuthAppInfoTooltip: FunctionComponent = () => { - const [shown, setShow] = useState(false); + const [isShown, setShown] = useState(false); const ref = useRef(null); useEffect(() => { - const dismiss = () => setShow(false); + const dismiss = () => setShown(false); document.addEventListener('mousedown', dismiss); return () => { document.removeEventListener('mousedown', dismiss); @@ -22,23 +41,19 @@ export const AuthAppInfoTooltip: FunctionComponent = () => { }, [ref]); return ( - <div className="relative"> - <IconButton - icon="info" - className="mt-1" - onClick={() => { - setShow(!shown); - }} - /> - {shown && ( - <div - className={`bg-black color-white text-center rounded shadow-overlay + <Disclosure open={isShown} onChange={() => setShown(!isShown)}> + <div className="relative"> + <DisclosureIconButton icon="info" className="mt-1" /> + <DisclosurePanel> + <div + className={`bg-black color-white text-center rounded shadow-overlay py-1.5 px-2 absolute w-103 -top-10 -left-51`} - > - Some apps, like Google Authenticator, do not back up and restore your - secret keys if you lose your device or get a new one. - </div> - )} - </div> + > + Some apps, like Google Authenticator, do not back up and restore + your secret keys if you lose your device or get a new one. + </div> + </DisclosurePanel> + </div> + </Disclosure> ); }; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx new file mode 100644 index 00000000000..ef06f00ae05 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx @@ -0,0 +1,18 @@ +import { observer } from 'mobx-react-lite'; +import { FunctionComponent } from 'preact'; +import { TwoFactorActivation } from './model'; +import { SaveSecretKey } from './SaveSecretKey'; +import { ScanQRCode } from './ScanQRCode'; +import { Verification } from './Verification'; + +export const TwoFactorActivationView: FunctionComponent<{ + activation: TwoFactorActivation; +}> = observer(({ activation: act }) => ( + <> + {act.step === 'scan-qr-code' && <ScanQRCode activation={act} />} + + {act.step === 'save-secret-key' && <SaveSecretKey activation={act} />} + + {act.step === 'verification' && <Verification activation={act} />} + </> +)); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index 77628d01939..25a8d78b033 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -1,4 +1,4 @@ -import { FunctionalComponent, FunctionComponent } from 'preact'; +import { FunctionComponent } from 'preact'; import { Title, Text, @@ -7,16 +7,10 @@ import { } from '../../components'; import { Switch } from '../../../components/Switch'; import { observer } from 'mobx-react-lite'; -import { DecoratedInput } from '../../../components/DecoratedInput'; -import { IconButton } from '../../../components/IconButton'; -import { ScanQRCode } from './ScanQRCode'; -import { SaveSecretKey } from './SaveSecretKey'; -import { Verification } from './Verification'; -import { TwoFactorActivation, TwoFactorAuth } from './model'; -import { downloadSecretKey } from './download-secret-key'; -import { CircleProgress } from '@/components/CircleProgress'; -import { useState } from 'preact/hooks'; -import { useEffect } from 'react'; +import { TwoFactorAuth } from './model'; +import { TwoFactorDisabledView } from './TwoFactorDisabledView'; +import { TwoFactorEnabledView } from './TwoFactorEnabledView'; +import { TwoFactorActivationView } from './TwoFactorActivationView'; export const TwoFactorAuthView: FunctionComponent<{ auth: TwoFactorAuth; @@ -52,84 +46,3 @@ export const TwoFactorAuthView: FunctionComponent<{ </PreferencesSegment> </PreferencesGroup> )); - -const ProgressTime: FunctionalComponent<{ time: number }> = ({ time }) => { - const [percent, setPercent] = useState(0); - const interval = time / 100; - useEffect(() => { - const tick = setInterval(() => { - if (percent === 100) { - setPercent(0); - } else { - setPercent(percent + 1); - } - }, interval); - return () => { - clearInterval(tick); - }; - }); - return <CircleProgress percent={percent} />; -}; - -const TwoFactorEnabledView: FunctionComponent<{ - secretKey: string; - authCode: string; -}> = ({ secretKey, authCode }) => { - const download = ( - <IconButton - icon="download" - onClick={() => { - downloadSecretKey(secretKey); - }} - /> - ); - const copy = ( - <IconButton - icon="copy" - onClick={() => { - navigator?.clipboard?.writeText(secretKey); - }} - /> - ); - const progress = <ProgressTime time={30000} />; - return ( - <div className="flex flex-row gap-4"> - <div className="flex-grow flex flex-col"> - <Text>Secret Key</Text> - <DecoratedInput - disabled={true} - right={[copy, download]} - text={secretKey} - /> - </div> - <div className="w-30 flex flex-col"> - <Text>Authentication Code</Text> - <DecoratedInput disabled={true} text={authCode} right={[progress]} /> - </div> - </div> - ); -}; - -const TwoFactorDisabledView: FunctionComponent = () => ( - <Text> - Enabling two-factor authentication will sign you out of all other sessions.{' '} - <a - target="_blank" - href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key" - > - Learn more - </a> - </Text> -); - -export const TwoFactorActivationView: FunctionComponent<{ - activation: TwoFactorActivation; -}> = observer(({ activation: act }) => ( - <> - {act.step === 'scan-qr-code' && <ScanQRCode activation={act} />} - - {act.step === 'save-secret-key' && <SaveSecretKey activation={act} />} - - {act.step === 'verification' && <Verification activation={act} />} - </> -)); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx index 0954d1ebbed..f9ffe7757ac 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -18,8 +18,15 @@ export const TwoFactorDialog: FunctionComponent<{ return ( <AlertDialog leastDestructiveRef={ldRef}> - <div className="sn-component"> - <div className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info"> + {/* sn-component is focusable by default, but doesn't stretch to child width + resulting in a badly focused dialog. Utility classes are not available + at the sn-component level, only below it. tabIndex -1 disables focus + and enables it on the child component */} + <div tabIndex={-1} className="sn-component"> + <div + tabIndex={0} + className="w-160 bg-default rounded shadow-overlay focus:padded-ring-info" + > {children} </div> </div> diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx new file mode 100644 index 00000000000..3276144857e --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx @@ -0,0 +1,14 @@ +import { Text } from '../../components'; +import { FunctionComponent } from 'preact'; + +export const TwoFactorDisabledView: FunctionComponent = () => ( + <Text> + Enabling two-factor authentication will sign you out of all other sessions.{' '} + <a + target="_blank" + href="https://standardnotes.com/help/21/where-should-i-store-my-two-factor-authentication-secret-key" + > + Learn more + </a> + </Text> +); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx new file mode 100644 index 00000000000..cbba572f074 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx @@ -0,0 +1,45 @@ +import { CircleProgressTime } from '@/components/CircleProgressTime'; +import { DecoratedInput } from '@/components/DecoratedInput'; +import { IconButton } from '@/components/IconButton'; +import { FunctionComponent } from 'preact'; +import { downloadSecretKey } from './download-secret-key'; +import { Text } from '../../components'; + +export const TwoFactorEnabledView: FunctionComponent<{ + secretKey: string; + authCode: string; +}> = ({ secretKey, authCode }) => { + const download = ( + <IconButton + icon="download" + onClick={() => { + downloadSecretKey(secretKey); + }} + /> + ); + const copy = ( + <IconButton + icon="copy" + onClick={() => { + navigator?.clipboard?.writeText(secretKey); + }} + /> + ); + const progress = <CircleProgressTime time={30000} />; + return ( + <div className="flex flex-row gap-4"> + <div className="flex-grow flex flex-col"> + <Text>Secret Key</Text> + <DecoratedInput + disabled={true} + right={[copy, download]} + text={secretKey} + /> + </div> + <div className="w-30 flex flex-col"> + <Text>Authentication Code</Text> + <DecoratedInput disabled={true} text={authCode} right={[progress]} /> + </div> + </div> + ); +}; diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx index cddc99cd6f8..73795da7255 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -25,7 +25,7 @@ export const Verification: FunctionComponent<{ Step 3 of 3 - Verification </TwoFactorDialogLabel> <TwoFactorDialogDescription> - <div className="flex-grow flex flex-col gap-2"> + <div className="flex-grow flex flex-col gap-1"> <div className="flex flex-row items-center gap-2"> <div className="text-sm"> ・Enter your <b>secret key</b>: diff --git a/yarn.lock b/yarn.lock index 789fa1a7097..e54b8deb922 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7909,9 +7909,9 @@ slice-ansi@^4.0.0: is-fullwidth-code-point "^3.0.0" sn-stylekit@5.2.8: - version "5.2.7" - resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.7.tgz#193a15d717c45d02354c7e574e9c10ddd47ff545" - integrity sha512-ORC+Gt8dKPTTO+JE91phYgw1LINltvJiC2Aq6zquo06ttEZIUv+Qnskx7hRmQCMfQ2HhU93xoBXT6V7POGlhCA== + version "5.2.8" + resolved "https://registry.yarnpkg.com/sn-stylekit/-/sn-stylekit-5.2.8.tgz#320d7f536036110fe57e0392cb7ff52076186d9a" + integrity sha512-08eKl2Eigb8kzxl+Tqp7PyzSVA0psCR6+hIVNL4V7//J7DjmM2RanBPMRxIUqBBTX/1b+CbZdZb8wnbzD18NZw== dependencies: "@reach/listbox" "^0.15.0" "@reach/menu-button" "^0.15.1" From 1f75df8e6c5ab944f885face09ba067836916592 Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski <mogi57@gmail.com> Date: Mon, 26 Jul 2021 17:52:34 +0200 Subject: [PATCH 14/14] refactor: usage of TwoFactorAuth status --- .../two-factor-auth/TwoFactorAuthView.tsx | 21 +++++++---- .../panes/two-factor-auth/model.ts | 37 +++++++++---------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx index 25a8d78b033..4df5b0b39cd 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -7,7 +7,12 @@ import { } from '../../components'; import { Switch } from '../../../components/Switch'; import { observer } from 'mobx-react-lite'; -import { TwoFactorAuth } from './model'; +import { + is2FAActivation, + is2FADisabled, + is2FAEnabled, + TwoFactorAuth, +} from './model'; import { TwoFactorDisabledView } from './TwoFactorDisabledView'; import { TwoFactorEnabledView } from './TwoFactorEnabledView'; import { TwoFactorActivationView } from './TwoFactorActivationView'; @@ -25,24 +30,24 @@ export const TwoFactorAuthView: FunctionComponent<{ </Text> </div> <Switch - checked={auth.enabled !== false} + checked={!is2FADisabled(auth.status)} onChange={() => auth.toggle2FA()} /> </div> </PreferencesSegment> <PreferencesSegment> - {auth.enabled !== false && ( + {is2FAEnabled(auth.status) && ( <TwoFactorEnabledView - secretKey={auth.enabled.secretKey} - authCode={auth.enabled.authCode} + secretKey={auth.status.secretKey} + authCode={auth.status.authCode} /> )} - {auth.activation !== false && ( - <TwoFactorActivationView activation={auth.activation} /> + {is2FAActivation(auth.status) && ( + <TwoFactorActivationView activation={auth.status} /> )} - {auth.enabled === false && <TwoFactorDisabledView />} + {!is2FAEnabled(auth.status) && <TwoFactorDisabledView />} </PreferencesSegment> </PreferencesGroup> )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts index 4218a0caf38..48ff9cf1635 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/model.ts @@ -95,7 +95,7 @@ export class TwoFactorActivation { } export class TwoFactorEnabled { - public readonly type = 'enabled' as const; + public readonly type = 'two-factor-enabled' as const; private _secretKey: string; private _authCode: string; @@ -122,11 +122,22 @@ export class TwoFactorEnabled { } } +type TwoFactorStatus = + | TwoFactorEnabled + | TwoFactorActivation + | 'two-factor-disabled'; + +export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' => + s === 'two-factor-disabled'; + +export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation => + (s as any).type === 'two-factor-activation'; + +export const is2FAEnabled = (s: TwoFactorStatus): s is TwoFactorEnabled => + (s as any).type === 'two-factor-enabled'; + export class TwoFactorAuth { - private _status: - | TwoFactorEnabled - | TwoFactorActivation - | 'two-factor-disabled' = 'two-factor-disabled'; + private _status: TwoFactorStatus = 'two-factor-disabled'; constructor() { makeAutoObservable<TwoFactorAuth, '_status'>(this, { @@ -151,19 +162,7 @@ export class TwoFactorAuth { else this.deactivate2FA(); } - get enabled() { - return ( - (this._status instanceof TwoFactorEnabled && - (this._status as TwoFactorEnabled)) || - false - ); - } - - get activation() { - return ( - (this._status instanceof TwoFactorActivation && - (this._status as TwoFactorActivation)) || - false - ); + get status() { + return this._status; } }