Merge branch 'develop' into feature/subscription-info-in-preferences

This commit is contained in:
Antonella Sgarlatta
2021-09-09 11:37:45 -03:00
committed by GitHub
17 changed files with 123 additions and 98 deletions

View File

@@ -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>
);

View File

@@ -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;
}

View File

@@ -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(() => {

View File

@@ -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();
}
}

View File

@@ -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>
)}
</div>
<Switch
checked={!is2FADisabled(auth.status)}
onChange={() => auth.toggle2FA()}
/>
</div>
</PreferencesSegment>
{is2FAActivation(auth.status) ? (
<TwoFactorActivationView activation={auth.status} />
) : null}
{!is2FAEnabled(auth.status) ? (
}> = observer(({ auth }) => {
return (
<PreferencesGroup>
<PreferencesSegment>
<TwoFactorDisabledView />
<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>
</PreferencesSegment>
) : null}
</PreferencesGroup>
));
{is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}
{auth.errorMessage != null && (
<PreferencesSegment>
<Text className="color-danger">{auth.errorMessage}</Text>
</PreferencesSegment>
)}
</PreferencesGroup>
);
});

View File

@@ -1,14 +0,0 @@
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>
);

View File

@@ -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} />;
};