From 377de393febd987f25e868c292c39d796df4320e Mon Sep 17 00:00:00 2001 From: Vardan Hakobyan Date: Mon, 6 Sep 2021 11:09:16 +0400 Subject: [PATCH 1/3] chore: version bump to 3.8.22 (#630) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7fddaee76..5f7acfeaa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "standard-notes-web", - "version": "3.8.21", + "version": "3.8.22", "license": "AGPL-3.0-or-later", "repository": { "type": "git", From c55946cb54d87459b6d91c47fd93f987e5bc4a53 Mon Sep 17 00:00:00 2001 From: Antonella Sgarlatta Date: Mon, 6 Sep 2021 10:49:17 -0300 Subject: [PATCH 2/3] fix: fix challenge modal submission debounce --- .../javascripts/views/challenge_modal/challenge_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx b/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx index 6530d7d71..105ca989c 100644 --- a/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx +++ b/app/assets/javascripts/views/challenge_modal/challenge_modal.tsx @@ -178,7 +178,7 @@ class ChallengeModalCtrl extends PureViewCtrl { if (!this.validate()) { return; } - if (this.submitting) { + if (this.submitting || this.state.processing) { return; } this.submitting = true; From 1294b941175e72a7298c246d58966289cba2086c Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Mon, 6 Sep 2021 17:15:34 +0200 Subject: [PATCH 3/3] feat: integrate two factor authentication (#626) * feat: integrate SNJS MFA with web * fix: create rudimentary typings file for qrcode.react * chore: lint fixes * fix: address PR feedback * fix: address PR feedback * fix: address PR feedback 2 * fix: replace spread props on TwoFactorAuthWrapper component * chore: change null check to undefined check --- .../javascripts/@types/qrcode.react.d.ts | 1 + .../components/AccountMenu/PasscodeLock.tsx | 1 - .../javascripts/components/DecoratedInput.tsx | 5 + app/assets/javascripts/components/NoteTag.tsx | 2 +- .../components/NotesContextMenu.tsx | 2 +- app/assets/javascripts/messages.ts | 2 +- .../preferences/PreferencesView.tsx | 58 +++--- .../preferences/PreferencesViewWrapper.tsx | 24 +++ .../preferences/components/Content.tsx | 7 +- .../{Pane.tsx => PreferencesPane.tsx} | 16 +- .../preferences/components/index.ts | 2 +- app/assets/javascripts/preferences/index.ts | 6 +- .../preferences/panes/Security.tsx | 7 +- .../panes/two-factor-auth/MfaProps.ts | 17 ++ .../panes/two-factor-auth/SaveSecretKey.tsx | 2 +- .../panes/two-factor-auth/ScanQRCode.tsx | 7 +- .../two-factor-auth/TwoFactorActivation.ts | 121 +++++++++++++ .../TwoFactorActivationView.tsx | 10 +- .../panes/two-factor-auth/TwoFactorAuth.ts | 130 ++++++++++++++ .../two-factor-auth/TwoFactorAuthView.tsx | 26 ++- .../panes/two-factor-auth/TwoFactorDialog.tsx | 4 +- .../two-factor-auth/TwoFactorEnabledView.tsx | 45 ----- .../panes/two-factor-auth/Verification.tsx | 24 +-- .../panes/two-factor-auth/index.tsx | 11 +- .../panes/two-factor-auth/model.ts | 168 ------------------ app/assets/javascripts/typings/pug.d.ts | 2 +- app/assets/javascripts/typings/stylekit.d.ts | 2 +- .../ui_models/app_state/notes_state.ts | 2 +- .../javascripts/ui_models/component_group.ts | 2 - app/assets/javascripts/views/index.ts | 2 +- .../javascripts/views/tags/tags_view.ts | 1 - package.json | 3 +- yarn.lock | 16 +- 33 files changed, 411 insertions(+), 317 deletions(-) create mode 100644 app/assets/javascripts/@types/qrcode.react.d.ts create mode 100644 app/assets/javascripts/preferences/PreferencesViewWrapper.tsx rename app/assets/javascripts/preferences/components/{Pane.tsx => PreferencesPane.tsx} (77%) create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts create mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorEnabledView.tsx delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/model.ts diff --git a/app/assets/javascripts/@types/qrcode.react.d.ts b/app/assets/javascripts/@types/qrcode.react.d.ts new file mode 100644 index 000000000..f997f01d9 --- /dev/null +++ b/app/assets/javascripts/@types/qrcode.react.d.ts @@ -0,0 +1 @@ +declare module 'qrcode.react'; diff --git a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx index 69d675e1a..95df944c4 100644 --- a/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx +++ b/app/assets/javascripts/components/AccountMenu/PasscodeLock.tsx @@ -40,7 +40,6 @@ const PasscodeLock = observer(({ const [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession()); const [hasPasscode, setHasPasscode] = useState(application.hasPasscode()); - const handleAddPassCode = () => { setShowPasscodeForm(true); setIsPasscodeFocused(true); diff --git a/app/assets/javascripts/components/DecoratedInput.tsx b/app/assets/javascripts/components/DecoratedInput.tsx index f649ab38f..97cccdad2 100644 --- a/app/assets/javascripts/components/DecoratedInput.tsx +++ b/app/assets/javascripts/components/DecoratedInput.tsx @@ -6,6 +6,7 @@ interface Props { left?: ComponentChild[]; right?: ComponentChild[]; text?: string; + onChange?: (text: string) => void; } /** @@ -17,6 +18,7 @@ export const DecoratedInput: FunctionalComponent = ({ left, right, text, + onChange, }) => { const base = 'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4'; @@ -34,6 +36,9 @@ export const DecoratedInput: FunctionalComponent = ({ className="w-full no-border color-black focus:shadow-none" disabled={disabled} value={text} + onChange={(e) => + onChange && onChange((e.target as HTMLInputElement).value) + } /> {right} diff --git a/app/assets/javascripts/components/NoteTag.tsx b/app/assets/javascripts/components/NoteTag.tsx index ba80ebe48..9a31fd1e6 100644 --- a/app/assets/javascripts/components/NoteTag.tsx +++ b/app/assets/javascripts/components/NoteTag.tsx @@ -54,7 +54,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => { const getTabIndex = () => { if (focusedTagUuid) { return focusedTagUuid === tag.uuid ? 0 : -1; - } + } if (autocompleteInputFocused) { return -1; } diff --git a/app/assets/javascripts/components/NotesContextMenu.tsx b/app/assets/javascripts/components/NotesContextMenu.tsx index 292e7265b..252516a98 100644 --- a/app/assets/javascripts/components/NotesContextMenu.tsx +++ b/app/assets/javascripts/components/NotesContextMenu.tsx @@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => { ); useCloseOnClickOutside( - contextMenuRef, + contextMenuRef, (open: boolean) => appState.notes.setContextMenuOpen(open) ); diff --git a/app/assets/javascripts/messages.ts b/app/assets/javascripts/messages.ts index 6f48d0777..2dcecff72 100644 --- a/app/assets/javascripts/messages.ts +++ b/app/assets/javascripts/messages.ts @@ -1,4 +1,4 @@ export enum RootScopeMessages { ReloadExtendedData = 'reload-ext-data', NewUpdateAvailable = 'new-update-available' -} \ No newline at end of file +} diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 239955ce5..b285c67fb 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -6,20 +6,25 @@ import { observer } from 'mobx-react-lite'; import { PreferencesMenu } from './preferences-menu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; +import { MfaProps } from './panes/two-factor-auth/MfaProps'; -const PaneSelector: FunctionComponent<{ - prefs: PreferencesMenu; +interface PreferencesProps extends MfaProps { application: WebApplication; -}> = observer(({ prefs: menu, application }) => { - switch (menu.selectedPaneId) { + closePreferences: () => void; +} + +const PaneSelector: FunctionComponent< + PreferencesProps & { menu: PreferencesMenu } +> = observer((props) => { + switch (props.menu.selectedPaneId) { case 'general': return null; case 'account': - return ; + return ; case 'appearance': return null; case 'security': - return ; + return ; case 'listed': return null; case 'shortcuts': @@ -33,23 +38,18 @@ const PaneSelector: FunctionComponent<{ } }); -const PreferencesCanvas: FunctionComponent<{ - preferences: PreferencesMenu; - application: WebApplication; -}> = observer(({ preferences: prefs, application }) => ( +const PreferencesCanvas: FunctionComponent< + PreferencesProps & { menu: PreferencesMenu } +> = observer((props) => (
- - + +
)); -const PreferencesView: FunctionComponent<{ - close: () => void; - application: WebApplication; -}> = observer( - ({ close, application }) => { - const prefs = new PreferencesMenu(); - +export const PreferencesView: FunctionComponent = observer( + (props) => { + const menu = new PreferencesMenu(); return (
@@ -58,30 +58,14 @@ const PreferencesView: FunctionComponent<{ Your preferences for Standard Notes { - close(); + props.closePreferences(); }} type="normal" icon="close" /> - +
); } ); - -export interface PreferencesWrapperProps { - appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; - application: WebApplication; -} - -export const PreferencesViewWrapper: FunctionComponent = - observer(({ appState, application }) => { - if (!appState.preferences.isOpen) return null; - return ( - appState.preferences.closePreferences()} - /> - ); - }); diff --git a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx new file mode 100644 index 000000000..ddf131524 --- /dev/null +++ b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx @@ -0,0 +1,24 @@ +import { FunctionComponent } from 'preact'; +import { observer } from 'mobx-react-lite'; +import { WebApplication } from '@/ui_models/application'; +import { PreferencesView } from './PreferencesView'; + +export interface PreferencesViewWrapperProps { + appState: { preferences: { isOpen: boolean; closePreferences: () => void } }; + application: WebApplication; +} + +export const PreferencesViewWrapper: FunctionComponent = + observer(({ appState, application }) => { + if (!appState.preferences.isOpen) { + return null; + } + + return ( + appState.preferences.closePreferences()} + application={application} + mfaGateway={application} + /> + ); + }); diff --git a/app/assets/javascripts/preferences/components/Content.tsx b/app/assets/javascripts/preferences/components/Content.tsx index 5f3da04f3..84cdae4d1 100644 --- a/app/assets/javascripts/preferences/components/Content.tsx +++ b/app/assets/javascripts/preferences/components/Content.tsx @@ -8,9 +8,10 @@ export const Subtitle: FunctionComponent = ({ children }) => (

{children}

); -export const Text: FunctionComponent = ({ children }) => ( -

{children}

-); +export const Text: FunctionComponent<{ className?: string }> = ({ + children, + className = '', +}) =>

{children}

; 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 \ diff --git a/app/assets/javascripts/preferences/components/Pane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx similarity index 77% rename from app/assets/javascripts/preferences/components/Pane.tsx rename to app/assets/javascripts/preferences/components/PreferencesPane.tsx index ab2638f03..ed8ec3295 100644 --- a/app/assets/javascripts/preferences/components/Pane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -14,14 +14,16 @@ export const PreferencesSegment: FunctionComponent = ({ children }) => ( export const PreferencesGroup: FunctionComponent = ({ children }) => (
- {!Array.isArray(children) + {Array.isArray(children) ? children - : children.map((c, i, arr) => ( - <> - {c} - - - ))} + .filter((child) => child != undefined && child !== '') + .map((child, i, arr) => ( + <> + {child} + + + )) + : children}
); diff --git a/app/assets/javascripts/preferences/components/index.ts b/app/assets/javascripts/preferences/components/index.ts index ab868c871..0c4046f06 100644 --- a/app/assets/javascripts/preferences/components/index.ts +++ b/app/assets/javascripts/preferences/components/index.ts @@ -1,3 +1,3 @@ export * from './Content'; export * from './MenuItem'; -export * from './Pane'; +export * from './PreferencesPane'; diff --git a/app/assets/javascripts/preferences/index.ts b/app/assets/javascripts/preferences/index.ts index 8849e4b8c..ba89cd2b5 100644 --- a/app/assets/javascripts/preferences/index.ts +++ b/app/assets/javascripts/preferences/index.ts @@ -1,9 +1,9 @@ import { toDirective } from '../components/utils'; import { PreferencesViewWrapper, - PreferencesWrapperProps, -} from './PreferencesView'; + PreferencesViewWrapperProps, +} from './PreferencesViewWrapper'; -export const PreferencesDirective = toDirective( +export const PreferencesDirective = toDirective( PreferencesViewWrapper ); diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index c344e0c85..f5b0d1ecc 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -1,9 +1,12 @@ import { FunctionComponent } from 'preact'; import { PreferencesPane } from '../components'; import { TwoFactorAuthWrapper } from './two-factor-auth'; +import { MfaProps } from './two-factor-auth/MfaProps'; -export const Security: FunctionComponent = () => ( +interface SecurityProps extends MfaProps {} + +export const Security: FunctionComponent = (props) => ( - + ); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts new file mode 100644 index 000000000..79e89298b --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts @@ -0,0 +1,17 @@ +export interface MfaGateway { + getUser(): { uuid: string; email: string } | undefined; + + isMfaActivated(): Promise; + + generateMfaSecret(): Promise; + + getOtpToken(secret: string): Promise; + + enableMfa(secret: string, otpToken: string): Promise; + + disableMfa(): Promise; +} + +export interface MfaProps { + mfaGateway: MfaGateway; +} 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 34e060b73..7244240ea 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/SaveSecretKey.tsx @@ -4,7 +4,7 @@ import { IconButton } from '@/components/IconButton'; import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { downloadSecretKey } from './download-secret-key'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, 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 82c73769e..9d12d0de8 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/ScanQRCode.tsx @@ -1,9 +1,12 @@ import { FunctionComponent } from 'preact'; import { observer } from 'mobx-react-lite'; + +import QRCode from 'qrcode.react'; + import { DecoratedInput } from '../../../components/DecoratedInput'; import { IconButton } from '../../../components/IconButton'; import { Button } from '@/components/Button'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, @@ -35,7 +38,7 @@ export const ScanQRCode: FunctionComponent<{
- QR code +
diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts new file mode 100644 index 000000000..1f756162a --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts @@ -0,0 +1,121 @@ +import { action, makeAutoObservable, observable, untracked } from 'mobx'; +import { MfaGateway } from './MfaProps'; + +type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification'; +type VerificationStatus = 'none' | 'invalid' | 'valid'; + +export class TwoFactorActivation { + public readonly type = 'two-factor-activation' as const; + + private _activationStep: ActivationStep; + + private _2FAVerification: VerificationStatus = 'none'; + + private inputSecretKey = ''; + private inputOtpToken = ''; + + constructor( + private mfaGateway: MfaGateway, + private readonly _secretKey: string, + private _cancelActivation: () => void, + private _enabled2FA: () => void + ) { + this._activationStep = 'scan-qr-code'; + + makeAutoObservable< + TwoFactorActivation, + | '_secretKey' + | '_authCode' + | '_step' + | '_enable2FAVerification' + | 'refreshOtp' + | 'inputOtpToken' + | 'inputSecretKey' + >( + this, + { + _secretKey: observable, + _authCode: observable, + _step: observable, + _enable2FAVerification: observable, + refreshOtp: action, + inputOtpToken: observable, + inputSecretKey: observable, + }, + { autoBind: true } + ); + } + + get secretKey(): string { + return this._secretKey; + } + + get activationStep(): ActivationStep { + return this._activationStep; + } + + get verificationStatus(): VerificationStatus { + return this._2FAVerification; + } + + get qrCode(): string { + const email = this.mfaGateway.getUser()!.email; + return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`; + } + + cancelActivation(): void { + this._cancelActivation(); + } + + openScanQRCode(): void { + const preconditions: ActivationStep[] = ['save-secret-key']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'scan-qr-code'; + } + } + + openSaveSecretKey(): void { + const preconditions: ActivationStep[] = ['scan-qr-code', 'verification']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'save-secret-key'; + } + } + + openVerification(): void { + this.inputOtpToken = ''; + this.inputSecretKey = ''; + const preconditions: ActivationStep[] = ['save-secret-key']; + if (preconditions.includes(this._activationStep)) { + this._activationStep = 'verification'; + this._2FAVerification = 'none'; + } + } + + setInputSecretKey(secretKey: string): void { + this.inputSecretKey = secretKey; + } + + setInputOtpToken(otpToken: string): void { + this.inputOtpToken = otpToken; + } + + enable2FA(): void { + if (this.inputSecretKey === this._secretKey) { + this.mfaGateway + .enableMfa(this.inputSecretKey, this.inputOtpToken) + .then( + action(() => { + this._2FAVerification = 'valid'; + this._enabled2FA(); + }) + ) + .catch( + action(() => { + this._2FAVerification = 'invalid'; + }) + ); + } else { + this._2FAVerification = 'invalid'; + } + } +} diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx index ef06f00ae..72d335f57 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivationView.tsx @@ -1,6 +1,6 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; -import { TwoFactorActivation } from './model'; +import { TwoFactorActivation } from './TwoFactorActivation'; import { SaveSecretKey } from './SaveSecretKey'; import { ScanQRCode } from './ScanQRCode'; import { Verification } from './Verification'; @@ -9,10 +9,12 @@ export const TwoFactorActivationView: FunctionComponent<{ activation: TwoFactorActivation; }> = observer(({ activation: act }) => ( <> - {act.step === 'scan-qr-code' && } + {act.activationStep === 'scan-qr-code' && } - {act.step === 'save-secret-key' && } + {act.activationStep === 'save-secret-key' && ( + + )} - {act.step === 'verification' && } + {act.activationStep === 'verification' && } )); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts new file mode 100644 index 000000000..44aeb360f --- /dev/null +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts @@ -0,0 +1,130 @@ +import { action, makeAutoObservable, observable } from 'mobx'; +import { MfaGateway } from './MfaProps'; +import { TwoFactorActivation } from './TwoFactorActivation'; + +type TwoFactorStatus = + | 'two-factor-enabled' + | 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 'two-factor-enabled' => + s === 'two-factor-enabled'; + +export class TwoFactorAuth { + private _status: TwoFactorStatus | 'fetching' = 'fetching'; + private _errorMessage: string | null; + + constructor(private readonly mfaGateway: MfaGateway) { + this._errorMessage = null; + + makeAutoObservable< + TwoFactorAuth, + '_status' | '_errorMessage' | 'deactivateMfa' | 'startActivation' + >(this, { + _status: observable, + _errorMessage: observable, + deactivateMfa: action, + startActivation: action, + }); + } + + private startActivation(): void { + const setDisabled = action(() => (this._status = 'two-factor-disabled')); + const setEnabled = action(() => (this._status = 'two-factor-enabled')); + this.mfaGateway + .generateMfaSecret() + .then( + action((secret) => { + this._status = new TwoFactorActivation( + this.mfaGateway, + secret, + setDisabled, + setEnabled + ); + }) + ) + .catch( + action((e) => { + this.setError(e.message); + }) + ); + } + + private deactivate2FA(): void { + this.mfaGateway + .disableMfa() + .then( + action(() => { + this._status = 'two-factor-disabled'; + }) + ) + .catch( + action((e) => { + this.setError(e.message); + }) + ); + } + + private get isLoggedIn(): boolean { + return this.mfaGateway.getUser() != undefined; + } + + fetchStatus(): void { + this._status = 'fetching'; + + if (!this.isLoggedIn) { + this.setError('To enable 2FA, sign in or register for an account.'); + return; + } + + this.mfaGateway + .isMfaActivated() + .then( + action((active) => { + this._status = active ? 'two-factor-enabled' : 'two-factor-disabled'; + this.setError(null); + }) + ) + .catch( + action((e) => { + this._status = 'two-factor-disabled'; + this.setError(e.message); + }) + ); + } + + setError(errorMessage: string | null): void { + this._errorMessage = errorMessage; + } + + toggle2FA(): void { + if (!this.isLoggedIn) { + return; + } + + if (this._status === 'two-factor-disabled') { + return this.startActivation(); + } + + if (this._status === 'two-factor-enabled') { + return this.deactivate2FA(); + } + } + + get errorMessage(): string | null { + return this._errorMessage; + } + + get status(): TwoFactorStatus { + if (this._status === 'fetching') { + return 'two-factor-disabled'; + } + return this._status; + } +} 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 4df5b0b39..d23c7ed77 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -12,9 +12,8 @@ import { is2FADisabled, is2FAEnabled, TwoFactorAuth, -} from './model'; +} from './TwoFactorAuth'; import { TwoFactorDisabledView } from './TwoFactorDisabledView'; -import { TwoFactorEnabledView } from './TwoFactorEnabledView'; import { TwoFactorActivationView } from './TwoFactorActivationView'; export const TwoFactorAuthView: FunctionComponent<{ @@ -28,6 +27,9 @@ export const TwoFactorAuthView: FunctionComponent<{ An extra layer of security when logging in to your account. + {auth.errorMessage != null && ( + {auth.errorMessage} + )}
- - {is2FAEnabled(auth.status) && ( - - )} - {is2FAActivation(auth.status) && ( - - )} + {is2FAActivation(auth.status) ? ( + + ) : null} - {!is2FAEnabled(auth.status) && } - + {!is2FAEnabled(auth.status) ? ( + + + + ) : null} )); 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 f9ffe7757..8c0b45987 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDialog.tsx @@ -19,8 +19,8 @@ export const TwoFactorDialog: FunctionComponent<{ return ( {/* 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 + 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 */}
= ({ secretKey, authCode }) => { - const download = ( - { - downloadSecretKey(secretKey); - }} - /> - ); - const copy = ( - { - navigator?.clipboard?.writeText(secretKey); - }} - /> - ); - const progress = ; - return ( -
-
- Secret Key - -
-
- Authentication Code - -
-
- ); -}; 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 73795da72..f411a35b2 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/Verification.tsx @@ -2,7 +2,7 @@ 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 { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorDialog, TwoFactorDialogLabel, @@ -17,11 +17,7 @@ export const Verification: FunctionComponent<{ act.verificationStatus === 'invalid' ? 'border-dark-red' : ''; return ( - { - act.cancelActivation(); - }} - > + Step 3 of 3 - Verification @@ -30,20 +26,26 @@ export const Verification: FunctionComponent<{
・Enter your secret key:
- +
・Verify the authentication code generated by your authenticator app:
- +
{act.verificationStatus === 'invalid' && ( -
+
Incorrect credentials, please try again.
)} @@ -51,13 +53,13 @@ export const Verification: FunctionComponent<{ className="min-w-20" type="normal" label="Back" - onClick={() => act.openSaveSecretKey()} + onClick={act.openSaveSecretKey} />