Skip to content

Commit

Permalink
feat: disabled 2fa feature (#631)
Browse files Browse the repository at this point in the history
* 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 <vardan_live@live.com>

* refactor: rename s to status

Co-authored-by: Vardan Hakobyan <vardan_live@live.com>
  • Loading branch information
gorjan5sk and vardan-arm authored Sep 9, 2021
1 parent 1294b94 commit 7b1499d
Show file tree
Hide file tree
Showing 17 changed files with 122 additions and 97 deletions.
2 changes: 1 addition & 1 deletion app/assets/javascripts/preferences/PreferencesMenuView.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
9 changes: 7 additions & 2 deletions app/assets/javascripts/preferences/PreferencesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -24,7 +24,12 @@ const PaneSelector: FunctionComponent<
case 'appearance':
return null;
case 'security':
return <Security mfaGateway={props.mfaGateway} />;
return (
<Security
mfaProvider={props.mfaProvider}
userProvider={props.userProvider}
/>
);
case 'listed':
return null;
case 'shortcuts':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperPro
<PreferencesView
closePreferences={() => appState.preferences.closePreferences()}
application={application}
mfaGateway={application}
mfaProvider={application}
userProvider={application}
/>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ export const PreferencesGroup: FunctionComponent = ({ children }) => (
<div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
{Array.isArray(children)
? children
.filter((child) => child != undefined && child !== '')
.filter(
(child) => child != undefined && child !== '' && child !== false
)
.map((child, i, arr) => (
<>
{child}
Expand Down
9 changes: 5 additions & 4 deletions app/assets/javascripts/preferences/panes/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<SecurityProps> = (props) => (
export const Security: FunctionComponent<MfaProps> = (props) => (
<PreferencesPane>
<TwoFactorAuthWrapper mfaGateway={props.mfaGateway} />
<TwoFactorAuthWrapper
mfaProvider={props.mfaProvider}
userProvider={props.userProvider}
/>
</PreferencesPane>
);
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
export interface MfaGateway {
getUser(): { uuid: string; email: string } | undefined;

isMfaActivated(): Promise<boolean>;

generateMfaSecret(): Promise<string>;

getOtpToken(secret: string): Promise<string>;

enableMfa(secret: string, otpToken: string): Promise<void>;

disableMfa(): Promise<void>;
}
import { MfaProvider, UserProvider } from '../../providers';

export interface MfaProps {
mfaGateway: MfaGateway;
userProvider: UserProvider;
mfaProvider: MfaProvider;
}
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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}`;
}

Expand Down Expand Up @@ -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(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { MfaProvider, UserProvider } from '@/preferences/providers';
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 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<
Expand All @@ -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
Expand All @@ -57,7 +61,7 @@ export class TwoFactorAuth {
}

private deactivate2FA(): void {
this.mfaGateway
this.mfaProvider
.disableMfa()
.then(
action(() => {
Expand All @@ -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) => {
Expand All @@ -99,7 +106,7 @@ export class TwoFactorAuth {
);
}

setError(errorMessage: string | null): void {
private setError(errorMessage: string | null): void {
this._errorMessage = errorMessage;
}

Expand All @@ -108,6 +115,10 @@ export class TwoFactorAuth {
return;
}

if (!this.isMfaFeatureAvailable) {
return;
}

if (this._status === 'two-factor-disabled') {
return this.startActivation();
}
Expand All @@ -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;
}

Expand All @@ -127,4 +144,8 @@ export class TwoFactorAuth {
}
return this._status;
}

private get isMfaFeatureAvailable(): boolean {
return this.mfaProvider.isMfaFeatureAvailable();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<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>
{auth.errorMessage != null && (
<Text className="color-danger">{auth.errorMessage}</Text>
)}
}> = observer(({ auth }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<div className="flex flex-row items-center">
<div className="flex-grow flex flex-col">
<Title>Two-factor authentication</Title>
<Text>
An extra layer of security when logging in to your account.
</Text>
</div>
<Switch
checked={!is2FADisabled(auth.status)}
onChange={auth.toggle2FA}
/>
</div>
<Switch
checked={!is2FADisabled(auth.status)}
onChange={() => auth.toggle2FA()}
/>
</div>
</PreferencesSegment>
</PreferencesSegment>

{is2FAActivation(auth.status) ? (
<TwoFactorActivationView activation={auth.status} />
) : null}
{is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}

{!is2FAEnabled(auth.status) ? (
<PreferencesSegment>
<TwoFactorDisabledView />
</PreferencesSegment>
) : null}
</PreferencesGroup>
));
{auth.errorMessage != null && (
<PreferencesSegment>
<Text className="color-danger">{auth.errorMessage}</Text>
</PreferencesSegment>
)}
</PreferencesGroup>
);
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { MfaProps } from './MfaProps';
import { TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorAuthView } from './TwoFactorAuthView';

export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = ({
mfaGateway,
}) => {
const [auth] = useState(() => new TwoFactorAuth(mfaGateway));
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
const [auth] = useState(
() => new TwoFactorAuth(props.mfaProvider, props.userProvider)
);
auth.fetchStatus();
return <TwoFactorAuthView auth={auth} />;
};
13 changes: 13 additions & 0 deletions app/assets/javascripts/preferences/providers/MfaProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface MfaProvider {
isMfaActivated(): Promise<boolean>;

generateMfaSecret(): Promise<string>;

getOtpToken(secret: string): Promise<string>;

enableMfa(secret: string, otpToken: string): Promise<void>;

disableMfa(): Promise<void>;

isMfaFeatureAvailable(): boolean;
}
3 changes: 3 additions & 0 deletions app/assets/javascripts/preferences/providers/UserProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface UserProvider {
getUser(): { uuid: string; email: string } | undefined;
}
2 changes: 2 additions & 0 deletions app/assets/javascripts/preferences/providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './MfaProvider';
export * from './UserProvider';
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading

0 comments on commit 7b1499d

Please sign in to comment.