From 7b1499d75eeb25a1f85190e63aea1a0c70b08c9c Mon Sep 17 00:00:00 2001 From: Gorjan Petrovski Date: Thu, 9 Sep 2021 16:28:37 +0200 Subject: [PATCH] feat: disabled 2fa feature (#631) * feat: add dim and blur to unavailable 2fa * feat: message over disabled 2FA * feat: 2fa remove overlay and dimming * fix: add newline to _ui,css * fix: tsc errors * Update app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx Co-authored-by: Vardan Hakobyan * refactor: rename s to status Co-authored-by: Vardan Hakobyan --- ...preferences-menu.ts => PreferencesMenu.ts} | 0 .../preferences/PreferencesMenuView.tsx | 2 +- .../preferences/PreferencesView.tsx | 9 ++- .../preferences/PreferencesViewWrapper.tsx | 3 +- .../components/PreferencesPane.tsx | 4 +- .../preferences/panes/Security.tsx | 9 +-- .../panes/two-factor-auth/MfaProps.ts | 17 +---- .../two-factor-auth/TwoFactorActivation.ts | 11 ++-- .../panes/two-factor-auth/TwoFactorAuth.ts | 51 ++++++++++----- .../two-factor-auth/TwoFactorAuthView.tsx | 65 +++++++++---------- .../two-factor-auth/TwoFactorDisabledView.tsx | 14 ---- .../panes/two-factor-auth/index.tsx | 8 +-- .../preferences/providers/MfaProvider.ts | 13 ++++ .../preferences/providers/UserProvider.ts | 3 + .../preferences/providers/index.ts | 2 + package.json | 3 +- yarn.lock | 7 ++ 17 files changed, 123 insertions(+), 98 deletions(-) rename app/assets/javascripts/preferences/{preferences-menu.ts => PreferencesMenu.ts} (100%) delete mode 100644 app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx create mode 100644 app/assets/javascripts/preferences/providers/MfaProvider.ts create mode 100644 app/assets/javascripts/preferences/providers/UserProvider.ts create mode 100644 app/assets/javascripts/preferences/providers/index.ts diff --git a/app/assets/javascripts/preferences/preferences-menu.ts b/app/assets/javascripts/preferences/PreferencesMenu.ts similarity index 100% rename from app/assets/javascripts/preferences/preferences-menu.ts rename to app/assets/javascripts/preferences/PreferencesMenu.ts diff --git a/app/assets/javascripts/preferences/PreferencesMenuView.tsx b/app/assets/javascripts/preferences/PreferencesMenuView.tsx index d5cd62833..763227988 100644 --- a/app/assets/javascripts/preferences/PreferencesMenuView.tsx +++ b/app/assets/javascripts/preferences/PreferencesMenuView.tsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react-lite'; import { FunctionComponent } from 'preact'; import { MenuItem } from './components'; -import { PreferencesMenu } from './preferences-menu'; +import { PreferencesMenu } from './PreferencesMenu'; export const PreferencesMenuView: FunctionComponent<{ menu: PreferencesMenu; diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index b285c67fb..ae7c7f72d 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -3,7 +3,7 @@ import { TitleBar, Title } from '@/components/TitleBar'; import { FunctionComponent } from 'preact'; import { AccountPreferences, HelpAndFeedback, Security } from './panes'; import { observer } from 'mobx-react-lite'; -import { PreferencesMenu } from './preferences-menu'; +import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; import { MfaProps } from './panes/two-factor-auth/MfaProps'; @@ -24,7 +24,12 @@ const PaneSelector: FunctionComponent< case 'appearance': return null; case 'security': - return ; + return ( + + ); case 'listed': return null; case 'shortcuts': diff --git a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx index ddf131524..cd4007990 100644 --- a/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx +++ b/app/assets/javascripts/preferences/PreferencesViewWrapper.tsx @@ -18,7 +18,8 @@ export const PreferencesViewWrapper: FunctionComponent appState.preferences.closePreferences()} application={application} - mfaGateway={application} + mfaProvider={application} + userProvider={application} /> ); }); diff --git a/app/assets/javascripts/preferences/components/PreferencesPane.tsx b/app/assets/javascripts/preferences/components/PreferencesPane.tsx index ed8ec3295..6efab9c48 100644 --- a/app/assets/javascripts/preferences/components/PreferencesPane.tsx +++ b/app/assets/javascripts/preferences/components/PreferencesPane.tsx @@ -16,7 +16,9 @@ export const PreferencesGroup: FunctionComponent = ({ children }) => (
{Array.isArray(children) ? children - .filter((child) => child != undefined && child !== '') + .filter( + (child) => child != undefined && child !== '' && child !== false + ) .map((child, i, arr) => ( <> {child} diff --git a/app/assets/javascripts/preferences/panes/Security.tsx b/app/assets/javascripts/preferences/panes/Security.tsx index f5b0d1ecc..a0269a269 100644 --- a/app/assets/javascripts/preferences/panes/Security.tsx +++ b/app/assets/javascripts/preferences/panes/Security.tsx @@ -3,10 +3,11 @@ import { PreferencesPane } from '../components'; import { TwoFactorAuthWrapper } from './two-factor-auth'; import { MfaProps } from './two-factor-auth/MfaProps'; -interface SecurityProps extends MfaProps {} - -export const Security: FunctionComponent = (props) => ( +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 index 79e89298b..eb8b840b8 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/MfaProps.ts @@ -1,17 +1,6 @@ -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; -} +import { MfaProvider, UserProvider } from '../../providers'; export interface MfaProps { - mfaGateway: MfaGateway; + userProvider: UserProvider; + mfaProvider: MfaProvider; } diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts index 1f756162a..44d6891f2 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorActivation.ts @@ -1,5 +1,5 @@ -import { action, makeAutoObservable, observable, untracked } from 'mobx'; -import { MfaGateway } from './MfaProps'; +import { MfaProvider, UserProvider } from '../../providers'; +import { action, makeAutoObservable, observable } from 'mobx'; type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification'; type VerificationStatus = 'none' | 'invalid' | 'valid'; @@ -15,7 +15,8 @@ export class TwoFactorActivation { private inputOtpToken = ''; constructor( - private mfaGateway: MfaGateway, + private mfaProvider: MfaProvider, + private userProvider: UserProvider, private readonly _secretKey: string, private _cancelActivation: () => void, private _enabled2FA: () => void @@ -59,7 +60,7 @@ export class TwoFactorActivation { } get qrCode(): string { - const email = this.mfaGateway.getUser()!.email; + const email = this.userProvider.getUser()!.email; return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`; } @@ -101,7 +102,7 @@ export class TwoFactorActivation { enable2FA(): void { if (this.inputSecretKey === this._secretKey) { - this.mfaGateway + this.mfaProvider .enableMfa(this.inputSecretKey, this.inputOtpToken) .then( action(() => { diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts index 44aeb360f..629f75159 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuth.ts @@ -1,5 +1,5 @@ +import { MfaProvider, UserProvider } from '@/preferences/providers'; import { action, makeAutoObservable, observable } from 'mobx'; -import { MfaGateway } from './MfaProps'; import { TwoFactorActivation } from './TwoFactorActivation'; type TwoFactorStatus = @@ -7,20 +7,23 @@ type TwoFactorStatus = | TwoFactorActivation | 'two-factor-disabled'; -export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' => - s === 'two-factor-disabled'; +export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-disabled' => + status === 'two-factor-disabled'; -export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation => - (s as any).type === 'two-factor-activation'; +export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation => + (status as TwoFactorActivation)?.type === 'two-factor-activation'; -export const is2FAEnabled = (s: TwoFactorStatus): s is 'two-factor-enabled' => - s === 'two-factor-enabled'; +export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' => + status === 'two-factor-enabled'; export class TwoFactorAuth { private _status: TwoFactorStatus | 'fetching' = 'fetching'; private _errorMessage: string | null; - constructor(private readonly mfaGateway: MfaGateway) { + constructor( + private readonly mfaProvider: MfaProvider, + private readonly userProvider: UserProvider + ) { this._errorMessage = null; makeAutoObservable< @@ -37,12 +40,13 @@ export class TwoFactorAuth { private startActivation(): void { const setDisabled = action(() => (this._status = 'two-factor-disabled')); const setEnabled = action(() => (this._status = 'two-factor-enabled')); - this.mfaGateway + this.mfaProvider .generateMfaSecret() .then( action((secret) => { this._status = new TwoFactorActivation( - this.mfaGateway, + this.mfaProvider, + this.userProvider, secret, setDisabled, setEnabled @@ -57,7 +61,7 @@ export class TwoFactorAuth { } private deactivate2FA(): void { - this.mfaGateway + this.mfaProvider .disableMfa() .then( action(() => { @@ -72,18 +76,21 @@ export class TwoFactorAuth { } private get isLoggedIn(): boolean { - return this.mfaGateway.getUser() != undefined; + return this.userProvider.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 + if (!this.isMfaFeatureAvailable) { + return; + } + + this.mfaProvider .isMfaActivated() .then( action((active) => { @@ -99,7 +106,7 @@ export class TwoFactorAuth { ); } - setError(errorMessage: string | null): void { + private setError(errorMessage: string | null): void { this._errorMessage = errorMessage; } @@ -108,6 +115,10 @@ export class TwoFactorAuth { return; } + if (!this.isMfaFeatureAvailable) { + return; + } + if (this._status === 'two-factor-disabled') { return this.startActivation(); } @@ -118,6 +129,12 @@ export class TwoFactorAuth { } get errorMessage(): string | null { + if (!this.isLoggedIn) { + return 'Two-factor authentication not available / Sign in or register for an account to configure 2FA'; + } + if (!this.isMfaFeatureAvailable) { + return 'Two-factor authentication not available / A paid subscription plan is required to enable 2FA.'; + } return this._errorMessage; } @@ -127,4 +144,8 @@ export class TwoFactorAuth { } return this._status; } + + private get isMfaFeatureAvailable(): boolean { + return this.mfaProvider.isMfaFeatureAvailable(); + } } 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 d23c7ed77..0207efb7c 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorAuthView.tsx @@ -7,45 +7,38 @@ import { } from '../../components'; import { Switch } from '../../../components/Switch'; import { observer } from 'mobx-react-lite'; -import { - is2FAActivation, - is2FADisabled, - is2FAEnabled, - TwoFactorAuth, -} from './TwoFactorAuth'; -import { TwoFactorDisabledView } from './TwoFactorDisabledView'; +import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth'; import { TwoFactorActivationView } from './TwoFactorActivationView'; export const TwoFactorAuthView: FunctionComponent<{ auth: TwoFactorAuth; -}> = observer(({ auth }) => ( - - -
-
- Two-factor authentication - - An extra layer of security when logging in to your account. - - {auth.errorMessage != null && ( - {auth.errorMessage} - )} -
- auth.toggle2FA()} - /> -
-
- - {is2FAActivation(auth.status) ? ( - - ) : null} - - {!is2FAEnabled(auth.status) ? ( +}> = observer(({ auth }) => { + return ( + - +
+
+ Two-factor authentication + + An extra layer of security when logging in to your account. + +
+ +
- ) : null} -
-)); + + {is2FAActivation(auth.status) && ( + + )} + + {auth.errorMessage != null && ( + + {auth.errorMessage} + + )} +
+ ); +}); diff --git a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx b/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx deleted file mode 100644 index 327614485..000000000 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/TwoFactorDisabledView.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Text } from '../../components'; -import { FunctionComponent } from 'preact'; - -export const TwoFactorDisabledView: FunctionComponent = () => ( - - Enabling two-factor authentication will sign you out of all other sessions.{' '} - - Learn more - - -); 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 2d0494ad3..4d5cad0ab 100644 --- a/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx +++ b/app/assets/javascripts/preferences/panes/two-factor-auth/index.tsx @@ -4,10 +4,10 @@ import { MfaProps } from './MfaProps'; import { TwoFactorAuth } from './TwoFactorAuth'; import { TwoFactorAuthView } from './TwoFactorAuthView'; -export const TwoFactorAuthWrapper: FunctionComponent = ({ - mfaGateway, -}) => { - const [auth] = useState(() => new TwoFactorAuth(mfaGateway)); +export const TwoFactorAuthWrapper: FunctionComponent = (props) => { + const [auth] = useState( + () => new TwoFactorAuth(props.mfaProvider, props.userProvider) + ); auth.fetchStatus(); return ; }; diff --git a/app/assets/javascripts/preferences/providers/MfaProvider.ts b/app/assets/javascripts/preferences/providers/MfaProvider.ts new file mode 100644 index 000000000..338ff3131 --- /dev/null +++ b/app/assets/javascripts/preferences/providers/MfaProvider.ts @@ -0,0 +1,13 @@ +export interface MfaProvider { + isMfaActivated(): Promise; + + generateMfaSecret(): Promise; + + getOtpToken(secret: string): Promise; + + enableMfa(secret: string, otpToken: string): Promise; + + disableMfa(): Promise; + + isMfaFeatureAvailable(): boolean; +} diff --git a/app/assets/javascripts/preferences/providers/UserProvider.ts b/app/assets/javascripts/preferences/providers/UserProvider.ts new file mode 100644 index 000000000..7fad1d76b --- /dev/null +++ b/app/assets/javascripts/preferences/providers/UserProvider.ts @@ -0,0 +1,3 @@ +export interface UserProvider { + getUser(): { uuid: string; email: string } | undefined; +} diff --git a/app/assets/javascripts/preferences/providers/index.ts b/app/assets/javascripts/preferences/providers/index.ts new file mode 100644 index 000000000..db2179e81 --- /dev/null +++ b/app/assets/javascripts/preferences/providers/index.ts @@ -0,0 +1,2 @@ +export * from './MfaProvider'; +export * from './UserProvider'; diff --git a/package.json b/package.json index 9d0b23d53..99ce6f3ad 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,8 @@ "@reach/checkbox": "^0.13.2", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "1.5.2", - "@standardnotes/snjs": "2.12.1", + "@standardnotes/features": "1.6.1", + "@standardnotes/snjs": "2.12.3", "mobx": "^6.3.2", "mobx-react-lite": "^3.2.0", "preact": "^10.5.12", diff --git a/yarn.lock b/yarn.lock index 7da495c7c..4dc1f340d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2042,6 +2042,13 @@ dependencies: "@standardnotes/common" "^1.1.0" +"@standardnotes/features@1.6.1": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.6.1.tgz#bfa227bd231dc1b54449936663731f5132b08e23" + integrity sha512-IC6fEotUqs23JdZx96JnEgARxwYzjmPz3UwU/uVn8hHjxPev/W0nyZFRiSlj4v+dod0jSa6FTR8iLLsOQ6M4Ug== + dependencies: + "@standardnotes/common" "^1.1.0" + "@standardnotes/settings@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459"