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

@@ -1,7 +1,7 @@
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { MenuItem } from './components'; import { MenuItem } from './components';
import { PreferencesMenu } from './preferences-menu'; import { PreferencesMenu } from './PreferencesMenu';
export const PreferencesMenuView: FunctionComponent<{ export const PreferencesMenuView: FunctionComponent<{
menu: PreferencesMenu; menu: PreferencesMenu;

View File

@@ -3,7 +3,7 @@ import { TitleBar, Title } from '@/components/TitleBar';
import { FunctionComponent } from 'preact'; import { FunctionComponent } from 'preact';
import { AccountPreferences, HelpAndFeedback, Security } from './panes'; import { AccountPreferences, HelpAndFeedback, Security } from './panes';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { PreferencesMenu } from './preferences-menu'; import { PreferencesMenu } from './PreferencesMenu';
import { PreferencesMenuView } from './PreferencesMenuView'; import { PreferencesMenuView } from './PreferencesMenuView';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { MfaProps } from './panes/two-factor-auth/MfaProps';
@@ -24,7 +24,12 @@ const PaneSelector: FunctionComponent<
case 'appearance': case 'appearance':
return null; return null;
case 'security': case 'security':
return <Security mfaGateway={props.mfaGateway} />; return (
<Security
mfaProvider={props.mfaProvider}
userProvider={props.userProvider}
/>
);
case 'listed': case 'listed':
return null; return null;
case 'shortcuts': case 'shortcuts':

View File

@@ -18,7 +18,8 @@ export const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperPro
<PreferencesView <PreferencesView
closePreferences={() => appState.preferences.closePreferences()} closePreferences={() => appState.preferences.closePreferences()}
application={application} application={application}
mfaGateway={application} mfaProvider={application}
userProvider={application}
/> />
); );
}); });

View File

@@ -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"> <div className="bg-default border-1 border-solid rounded border-gray-300 px-6 py-6 flex flex-col gap-2">
{Array.isArray(children) {Array.isArray(children)
? children ? children
.filter((child) => child != undefined && child !== '') .filter(
(child) => child != undefined && child !== '' && child !== false
)
.map((child, i, arr) => ( .map((child, i, arr) => (
<> <>
{child} {child}

View File

@@ -3,10 +3,11 @@ import { PreferencesPane } from '../components';
import { TwoFactorAuthWrapper } from './two-factor-auth'; import { TwoFactorAuthWrapper } from './two-factor-auth';
import { MfaProps } from './two-factor-auth/MfaProps'; import { MfaProps } from './two-factor-auth/MfaProps';
interface SecurityProps extends MfaProps {} export const Security: FunctionComponent<MfaProps> = (props) => (
export const Security: FunctionComponent<SecurityProps> = (props) => (
<PreferencesPane> <PreferencesPane>
<TwoFactorAuthWrapper mfaGateway={props.mfaGateway} /> <TwoFactorAuthWrapper
mfaProvider={props.mfaProvider}
userProvider={props.userProvider}
/>
</PreferencesPane> </PreferencesPane>
); );

View File

@@ -1,17 +1,6 @@
export interface MfaGateway { import { MfaProvider, UserProvider } from '../../providers';
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>;
}
export interface MfaProps { export interface MfaProps {
mfaGateway: MfaGateway; userProvider: UserProvider;
mfaProvider: MfaProvider;
} }

View File

@@ -1,5 +1,5 @@
import { action, makeAutoObservable, observable, untracked } from 'mobx'; import { MfaProvider, UserProvider } from '../../providers';
import { MfaGateway } from './MfaProps'; import { action, makeAutoObservable, observable } from 'mobx';
type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification'; type ActivationStep = 'scan-qr-code' | 'save-secret-key' | 'verification';
type VerificationStatus = 'none' | 'invalid' | 'valid'; type VerificationStatus = 'none' | 'invalid' | 'valid';
@@ -15,7 +15,8 @@ export class TwoFactorActivation {
private inputOtpToken = ''; private inputOtpToken = '';
constructor( constructor(
private mfaGateway: MfaGateway, private mfaProvider: MfaProvider,
private userProvider: UserProvider,
private readonly _secretKey: string, private readonly _secretKey: string,
private _cancelActivation: () => void, private _cancelActivation: () => void,
private _enabled2FA: () => void private _enabled2FA: () => void
@@ -59,7 +60,7 @@ export class TwoFactorActivation {
} }
get qrCode(): string { 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}`; return `otpauth://totp/2FA?secret=${this._secretKey}&issuer=Standard%20Notes&label=${email}`;
} }
@@ -101,7 +102,7 @@ export class TwoFactorActivation {
enable2FA(): void { enable2FA(): void {
if (this.inputSecretKey === this._secretKey) { if (this.inputSecretKey === this._secretKey) {
this.mfaGateway this.mfaProvider
.enableMfa(this.inputSecretKey, this.inputOtpToken) .enableMfa(this.inputSecretKey, this.inputOtpToken)
.then( .then(
action(() => { action(() => {

View File

@@ -1,5 +1,5 @@
import { MfaProvider, UserProvider } from '@/preferences/providers';
import { action, makeAutoObservable, observable } from 'mobx'; import { action, makeAutoObservable, observable } from 'mobx';
import { MfaGateway } from './MfaProps';
import { TwoFactorActivation } from './TwoFactorActivation'; import { TwoFactorActivation } from './TwoFactorActivation';
type TwoFactorStatus = type TwoFactorStatus =
@@ -7,20 +7,23 @@ type TwoFactorStatus =
| TwoFactorActivation | TwoFactorActivation
| 'two-factor-disabled'; | 'two-factor-disabled';
export const is2FADisabled = (s: TwoFactorStatus): s is 'two-factor-disabled' => export const is2FADisabled = (status: TwoFactorStatus): status is 'two-factor-disabled' =>
s === 'two-factor-disabled'; status === 'two-factor-disabled';
export const is2FAActivation = (s: TwoFactorStatus): s is TwoFactorActivation => export const is2FAActivation = (status: TwoFactorStatus): status is TwoFactorActivation =>
(s as any).type === 'two-factor-activation'; (status as TwoFactorActivation)?.type === 'two-factor-activation';
export const is2FAEnabled = (s: TwoFactorStatus): s is 'two-factor-enabled' => export const is2FAEnabled = (status: TwoFactorStatus): status is 'two-factor-enabled' =>
s === 'two-factor-enabled'; status === 'two-factor-enabled';
export class TwoFactorAuth { export class TwoFactorAuth {
private _status: TwoFactorStatus | 'fetching' = 'fetching'; private _status: TwoFactorStatus | 'fetching' = 'fetching';
private _errorMessage: string | null; private _errorMessage: string | null;
constructor(private readonly mfaGateway: MfaGateway) { constructor(
private readonly mfaProvider: MfaProvider,
private readonly userProvider: UserProvider
) {
this._errorMessage = null; this._errorMessage = null;
makeAutoObservable< makeAutoObservable<
@@ -37,12 +40,13 @@ export class TwoFactorAuth {
private startActivation(): void { private startActivation(): void {
const setDisabled = action(() => (this._status = 'two-factor-disabled')); const setDisabled = action(() => (this._status = 'two-factor-disabled'));
const setEnabled = action(() => (this._status = 'two-factor-enabled')); const setEnabled = action(() => (this._status = 'two-factor-enabled'));
this.mfaGateway this.mfaProvider
.generateMfaSecret() .generateMfaSecret()
.then( .then(
action((secret) => { action((secret) => {
this._status = new TwoFactorActivation( this._status = new TwoFactorActivation(
this.mfaGateway, this.mfaProvider,
this.userProvider,
secret, secret,
setDisabled, setDisabled,
setEnabled setEnabled
@@ -57,7 +61,7 @@ export class TwoFactorAuth {
} }
private deactivate2FA(): void { private deactivate2FA(): void {
this.mfaGateway this.mfaProvider
.disableMfa() .disableMfa()
.then( .then(
action(() => { action(() => {
@@ -72,18 +76,21 @@ export class TwoFactorAuth {
} }
private get isLoggedIn(): boolean { private get isLoggedIn(): boolean {
return this.mfaGateway.getUser() != undefined; return this.userProvider.getUser() != undefined;
} }
fetchStatus(): void { fetchStatus(): void {
this._status = 'fetching'; this._status = 'fetching';
if (!this.isLoggedIn) { if (!this.isLoggedIn) {
this.setError('To enable 2FA, sign in or register for an account.');
return; return;
} }
this.mfaGateway if (!this.isMfaFeatureAvailable) {
return;
}
this.mfaProvider
.isMfaActivated() .isMfaActivated()
.then( .then(
action((active) => { action((active) => {
@@ -99,7 +106,7 @@ export class TwoFactorAuth {
); );
} }
setError(errorMessage: string | null): void { private setError(errorMessage: string | null): void {
this._errorMessage = errorMessage; this._errorMessage = errorMessage;
} }
@@ -108,6 +115,10 @@ export class TwoFactorAuth {
return; return;
} }
if (!this.isMfaFeatureAvailable) {
return;
}
if (this._status === 'two-factor-disabled') { if (this._status === 'two-factor-disabled') {
return this.startActivation(); return this.startActivation();
} }
@@ -118,6 +129,12 @@ export class TwoFactorAuth {
} }
get errorMessage(): string | null { 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; return this._errorMessage;
} }
@@ -127,4 +144,8 @@ export class TwoFactorAuth {
} }
return this._status; return this._status;
} }
private get isMfaFeatureAvailable(): boolean {
return this.mfaProvider.isMfaFeatureAvailable();
}
} }

View File

@@ -7,45 +7,38 @@ import {
} from '../../components'; } from '../../components';
import { Switch } from '../../../components/Switch'; import { Switch } from '../../../components/Switch';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { import { is2FAActivation, is2FADisabled, TwoFactorAuth } from './TwoFactorAuth';
is2FAActivation,
is2FADisabled,
is2FAEnabled,
TwoFactorAuth,
} from './TwoFactorAuth';
import { TwoFactorDisabledView } from './TwoFactorDisabledView';
import { TwoFactorActivationView } from './TwoFactorActivationView'; import { TwoFactorActivationView } from './TwoFactorActivationView';
export const TwoFactorAuthView: FunctionComponent<{ export const TwoFactorAuthView: FunctionComponent<{
auth: TwoFactorAuth; auth: TwoFactorAuth;
}> = observer(({ auth }) => ( }> = observer(({ auth }) => {
<PreferencesGroup> return (
<PreferencesSegment> <PreferencesGroup>
<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) ? (
<PreferencesSegment> <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> </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 { TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorAuthView } from './TwoFactorAuthView'; import { TwoFactorAuthView } from './TwoFactorAuthView';
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = ({ export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = (props) => {
mfaGateway, const [auth] = useState(
}) => { () => new TwoFactorAuth(props.mfaProvider, props.userProvider)
const [auth] = useState(() => new TwoFactorAuth(mfaGateway)); );
auth.fetchStatus(); auth.fetchStatus();
return <TwoFactorAuthView auth={auth} />; return <TwoFactorAuthView auth={auth} />;
}; };

View File

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

View File

@@ -0,0 +1,3 @@
export interface UserProvider {
getUser(): { uuid: string; email: string } | undefined;
}

View File

@@ -0,0 +1,2 @@
export * from './MfaProvider';
export * from './UserProvider';

View File

@@ -71,7 +71,8 @@
"@reach/checkbox": "^0.13.2", "@reach/checkbox": "^0.13.2",
"@reach/dialog": "^0.13.0", "@reach/dialog": "^0.13.0",
"@standardnotes/sncrypto-web": "1.5.2", "@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": "^6.3.2",
"mobx-react-lite": "^3.2.0", "mobx-react-lite": "^3.2.0",
"preact": "^10.5.12", "preact": "^10.5.12",

View File

@@ -2042,6 +2042,13 @@
dependencies: dependencies:
"@standardnotes/common" "^1.1.0" "@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": "@standardnotes/settings@1.2.0":
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459" resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459"