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
This commit is contained in:
Gorjan Petrovski
2021-09-06 17:15:34 +02:00
committed by GitHub
parent c55946cb54
commit 1294b94117
33 changed files with 411 additions and 317 deletions

View File

@@ -0,0 +1 @@
declare module 'qrcode.react';

View File

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

View File

@@ -6,6 +6,7 @@ interface Props {
left?: ComponentChild[];
right?: ComponentChild[];
text?: string;
onChange?: (text: string) => void;
}
/**
@@ -17,6 +18,7 @@ export const DecoratedInput: FunctionalComponent<Props> = ({
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<Props> = ({
className="w-full no-border color-black focus:shadow-none"
disabled={disabled}
value={text}
onChange={(e) =>
onChange && onChange((e.target as HTMLInputElement).value)
}
/>
</div>
{right}

View File

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

View File

@@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => {
);
useCloseOnClickOutside(
contextMenuRef,
contextMenuRef,
(open: boolean) => appState.notes.setContextMenuOpen(open)
);

View File

@@ -1,4 +1,4 @@
export enum RootScopeMessages {
ReloadExtendedData = 'reload-ext-data',
NewUpdateAvailable = 'new-update-available'
}
}

View File

@@ -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 <AccountPreferences application={application} />;
return <AccountPreferences application={props.application} />;
case 'appearance':
return null;
case 'security':
return <Security />;
return <Security mfaGateway={props.mfaGateway} />;
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) => (
<div className="flex flex-row flex-grow min-h-0 justify-between">
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
<PaneSelector prefs={prefs} application={application} />
<PreferencesMenuView menu={props.menu} />
<PaneSelector {...props} />
</div>
));
const PreferencesView: FunctionComponent<{
close: () => void;
application: WebApplication;
}> = observer(
({ close, application }) => {
const prefs = new PreferencesMenu();
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
(props) => {
const menu = new PreferencesMenu();
return (
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
<TitleBar className="items-center justify-between">
@@ -58,30 +58,14 @@ const PreferencesView: FunctionComponent<{
<Title className="text-lg">Your preferences for Standard Notes</Title>
<RoundIconButton
onClick={() => {
close();
props.closePreferences();
}}
type="normal"
icon="close"
/>
</TitleBar>
<PreferencesCanvas preferences={prefs} application={application} />
<PreferencesCanvas {...props} menu={menu} />
</div>
);
}
);
export interface PreferencesWrapperProps {
appState: { preferences: { isOpen: boolean; closePreferences: () => void } };
application: WebApplication;
}
export const PreferencesViewWrapper: FunctionComponent<PreferencesWrapperProps> =
observer(({ appState, application }) => {
if (!appState.preferences.isOpen) return null;
return (
<PreferencesView
application={application}
close={() => appState.preferences.closePreferences()}
/>
);
});

View File

@@ -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<PreferencesViewWrapperProps> =
observer(({ appState, application }) => {
if (!appState.preferences.isOpen) {
return null;
}
return (
<PreferencesView
closePreferences={() => appState.preferences.closePreferences()}
application={application}
mfaGateway={application}
/>
);
});

View File

@@ -8,9 +8,10 @@ export const Subtitle: FunctionComponent = ({ children }) => (
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
);
export const Text: FunctionComponent = ({ children }) => (
<p className="text-xs">{children}</p>
);
export const Text: FunctionComponent<{ className?: string }> = ({
children,
className = '',
}) => <p className={`${className} text-xs`}>{children}</p>;
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 \

View File

@@ -14,14 +14,16 @@ export const PreferencesSegment: FunctionComponent = ({ children }) => (
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)
{Array.isArray(children)
? children
: children.map((c, i, arr) => (
<>
{c}
<HorizontalLine index={i} length={arr.length} />
</>
))}
.filter((child) => child != undefined && child !== '')
.map((child, i, arr) => (
<>
{child}
<HorizontalLine index={i} length={arr.length} />
</>
))
: children}
</div>
);

View File

@@ -1,3 +1,3 @@
export * from './Content';
export * from './MenuItem';
export * from './Pane';
export * from './PreferencesPane';

View File

@@ -1,9 +1,9 @@
import { toDirective } from '../components/utils';
import {
PreferencesViewWrapper,
PreferencesWrapperProps,
} from './PreferencesView';
PreferencesViewWrapperProps,
} from './PreferencesViewWrapper';
export const PreferencesDirective = toDirective<PreferencesWrapperProps>(
export const PreferencesDirective = toDirective<PreferencesViewWrapperProps>(
PreferencesViewWrapper
);

View File

@@ -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<SecurityProps> = (props) => (
<PreferencesPane>
<TwoFactorAuthWrapper />
<TwoFactorAuthWrapper mfaGateway={props.mfaGateway} />
</PreferencesPane>
);

View File

@@ -0,0 +1,17 @@
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>;
}
export interface MfaProps {
mfaGateway: MfaGateway;
}

View File

@@ -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,

View File

@@ -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<{
<TwoFactorDialogDescription>
<div className="flex flex-row gap-3 items-center">
<div className="w-25 h-25 flex items-center justify-center bg-info">
QR code
<QRCode value={act.qrCode} size={100} />
</div>
<div className="flex-grow flex flex-col gap-2">
<div className="flex flex-row gap-1 items-center">

View File

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

View File

@@ -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' && <ScanQRCode activation={act} />}
{act.activationStep === 'scan-qr-code' && <ScanQRCode activation={act} />}
{act.step === 'save-secret-key' && <SaveSecretKey activation={act} />}
{act.activationStep === 'save-secret-key' && (
<SaveSecretKey activation={act} />
)}
{act.step === 'verification' && <Verification activation={act} />}
{act.activationStep === 'verification' && <Verification activation={act} />}
</>
));

View File

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

View File

@@ -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<{
<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)}
@@ -35,19 +37,15 @@ export const TwoFactorAuthView: FunctionComponent<{
/>
</div>
</PreferencesSegment>
<PreferencesSegment>
{is2FAEnabled(auth.status) && (
<TwoFactorEnabledView
secretKey={auth.status.secretKey}
authCode={auth.status.authCode}
/>
)}
{is2FAActivation(auth.status) && (
<TwoFactorActivationView activation={auth.status} />
)}
{is2FAActivation(auth.status) ? (
<TwoFactorActivationView activation={auth.status} />
) : null}
{!is2FAEnabled(auth.status) && <TwoFactorDisabledView />}
</PreferencesSegment>
{!is2FAEnabled(auth.status) ? (
<PreferencesSegment>
<TwoFactorDisabledView />
</PreferencesSegment>
) : null}
</PreferencesGroup>
));

View File

@@ -19,8 +19,8 @@ export const TwoFactorDialog: FunctionComponent<{
return (
<AlertDialog leastDestructiveRef={ldRef}>
{/* 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 */}
<div tabIndex={-1} className="sn-component">
<div

View File

@@ -1,45 +0,0 @@
import { CircleProgressTime } from '@/components/CircleProgressTime';
import { DecoratedInput } from '@/components/DecoratedInput';
import { IconButton } from '@/components/IconButton';
import { FunctionComponent } from 'preact';
import { downloadSecretKey } from './download-secret-key';
import { Text } from '../../components';
export const TwoFactorEnabledView: FunctionComponent<{
secretKey: string;
authCode: string;
}> = ({ secretKey, authCode }) => {
const download = (
<IconButton
icon="download"
onClick={() => {
downloadSecretKey(secretKey);
}}
/>
);
const copy = (
<IconButton
icon="copy"
onClick={() => {
navigator?.clipboard?.writeText(secretKey);
}}
/>
);
const progress = <CircleProgressTime time={30000} />;
return (
<div className="flex flex-row gap-4">
<div className="flex-grow flex flex-col">
<Text>Secret Key</Text>
<DecoratedInput
disabled={true}
right={[copy, download]}
text={secretKey}
/>
</div>
<div className="w-30 flex flex-col">
<Text>Authentication Code</Text>
<DecoratedInput disabled={true} text={authCode} right={[progress]} />
</div>
</div>
);
};

View File

@@ -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 (
<TwoFactorDialog>
<TwoFactorDialogLabel
closeDialog={() => {
act.cancelActivation();
}}
>
<TwoFactorDialogLabel closeDialog={act.cancelActivation}>
Step 3 of 3 - Verification
</TwoFactorDialogLabel>
<TwoFactorDialogDescription>
@@ -30,20 +26,26 @@ export const Verification: FunctionComponent<{
<div className="text-sm">
Enter your <b>secret key</b>:
</div>
<DecoratedInput className={borderInv} />
<DecoratedInput
className={borderInv}
onChange={act.setInputSecretKey}
/>
</div>
<div className="flex flex-row items-center gap-2">
<div className="text-sm">
Verify the <b>authentication code</b> generated by your
authenticator app:
</div>
<DecoratedInput className={`w-30 ${borderInv}`} />
<DecoratedInput
className={`w-30 ${borderInv}`}
onChange={act.setInputOtpToken}
/>
</div>
</div>
</TwoFactorDialogDescription>
<TwoFactorDialogButtons>
{act.verificationStatus === 'invalid' && (
<div className="text-sm color-dark-red">
<div className="text-sm color-danger">
Incorrect credentials, please try again.
</div>
)}
@@ -51,13 +53,13 @@ export const Verification: FunctionComponent<{
className="min-w-20"
type="normal"
label="Back"
onClick={() => act.openSaveSecretKey()}
onClick={act.openSaveSecretKey}
/>
<Button
className="min-w-20"
type="primary"
label="Next"
onClick={() => act.enable2FA('X', 'X')}
onClick={act.enable2FA}
/>
</TwoFactorDialogButtons>
</TwoFactorDialog>

View File

@@ -1,10 +1,13 @@
import { observer } from 'mobx-react-lite';
import { FunctionComponent } from 'preact';
import { useState } from 'preact/hooks';
import { TwoFactorAuth } from './model';
import { MfaProps } from './MfaProps';
import { TwoFactorAuth } from './TwoFactorAuth';
import { TwoFactorAuthView } from './TwoFactorAuthView';
export const TwoFactorAuthWrapper: FunctionComponent = () => {
const [auth] = useState(() => new TwoFactorAuth());
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = ({
mfaGateway,
}) => {
const [auth] = useState(() => new TwoFactorAuth(mfaGateway));
auth.fetchStatus();
return <TwoFactorAuthView auth={auth} />;
};

View File

@@ -1,168 +0,0 @@
import { action, makeAutoObservable, observable, untracked } from 'mobx';
function getNewAuthCode() {
const MIN = 100000;
const MAX = 999999;
const code = Math.floor(Math.random() * (MAX - MIN) + MIN);
return code.toString();
}
const activationSteps = [
'scan-qr-code',
'save-secret-key',
'verification',
] as const;
type ActivationStep = typeof activationSteps[number];
export class TwoFactorActivation {
public readonly type = 'two-factor-activation' as const;
private _step: ActivationStep;
private _secretKey: string;
private _authCode: string;
private _2FAVerification: 'none' | 'invalid' | 'valid' = 'none';
constructor(
private _cancelActivation: () => void,
private _enable2FA: (secretKey: string) => void
) {
this._secretKey = 'FHJJSAJKDASKW43KJS';
this._authCode = getNewAuthCode();
this._step = 'scan-qr-code';
makeAutoObservable<
TwoFactorActivation,
'_secretKey' | '_authCode' | '_step' | '_enable2FAVerification'
>(
this,
{
_secretKey: observable,
_authCode: observable,
_step: observable,
_enable2FAVerification: observable,
},
{ autoBind: true }
);
}
get secretKey() {
return this._secretKey;
}
get authCode() {
return this._authCode;
}
get step() {
return this._step;
}
get verificationStatus() {
return this._2FAVerification;
}
cancelActivation() {
this._cancelActivation();
}
openScanQRCode() {
this._step = 'scan-qr-code';
}
openSaveSecretKey() {
this._step = 'save-secret-key';
}
openVerification() {
this._step = 'verification';
this._2FAVerification = 'none';
}
enable2FA(secretKey: string, authCode: string) {
if (secretKey === this._secretKey && authCode === this._authCode) {
this._2FAVerification = 'valid';
this._enable2FA(secretKey);
return;
}
// Change to invalid upon implementation
this._2FAVerification = 'valid';
// Remove after implementation
this._enable2FA(secretKey);
}
}
export class TwoFactorEnabled {
public readonly type = 'two-factor-enabled' as const;
private _secretKey: string;
private _authCode: string;
constructor(secretKey: string) {
this._secretKey = secretKey;
this._authCode = getNewAuthCode();
makeAutoObservable<TwoFactorEnabled, '_secretKey' | '_authCode'>(this, {
_secretKey: observable,
_authCode: observable,
});
}
get secretKey() {
return this._secretKey;
}
get authCode() {
return this._authCode;
}
refreshAuthCode() {
this._authCode = getNewAuthCode();
}
}
type TwoFactorStatus =
| TwoFactorEnabled
| 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 TwoFactorEnabled =>
(s as any).type === 'two-factor-enabled';
export class TwoFactorAuth {
private _status: TwoFactorStatus = 'two-factor-disabled';
constructor() {
makeAutoObservable<TwoFactorAuth, '_status'>(this, {
_status: observable,
});
}
private startActivation() {
const cancel = action(() => (this._status = 'two-factor-disabled'));
const enable = action(
(secretKey: string) => (this._status = new TwoFactorEnabled(secretKey))
);
this._status = new TwoFactorActivation(cancel, enable);
}
private deactivate2FA() {
this._status = 'two-factor-disabled';
}
toggle2FA() {
if (this._status === 'two-factor-disabled') this.startActivation();
else this.deactivate2FA();
}
get status() {
return this._status;
}
}

View File

@@ -2,4 +2,4 @@ declare module "*.pug" {
import { compileTemplate } from 'pug';
const content: compileTemplate;
export default content;
}
}

View File

@@ -1 +1 @@
declare module "sn-stylekit";
declare module "sn-stylekit";

View File

@@ -172,7 +172,7 @@ export class NotesState {
} else {
this.activeEditor.setNote(note);
}
this.appState.noteTags.reloadTags();
await this.onActiveEditorChanged();

View File

@@ -1,7 +1,6 @@
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
import { WebApplication } from './application';
/** Areas that only allow a single component to be active */
const SingleComponentAreas = [
ComponentArea.Editor,
@@ -82,7 +81,6 @@ export class ComponentGroup {
return this.application.getAll(this.activeComponents) as SNComponent[];
}
/**
* Notifies observer when the active editor has changed.
*/

View File

@@ -6,4 +6,4 @@ export { EditorView } from './editor/editor_view';
export { FooterView } from './footer/footer_view';
export { NotesView } from './notes/notes_view';
export { TagsView } from './tags/tags_view';
export { ChallengeModal } from './challenge_modal/challenge_modal';
export { ChallengeModal } from './challenge_modal/challenge_modal';

View File

@@ -153,7 +153,6 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
}
}
/** @override */
async onAppEvent(eventName: ApplicationEvent) {
super.onAppEvent(eventName);

View File

@@ -74,6 +74,7 @@
"@standardnotes/snjs": "2.12.1",
"mobx": "^6.3.2",
"mobx-react-lite": "^3.2.0",
"preact": "^10.5.12"
"preact": "^10.5.12",
"qrcode.react": "^1.0.1"
}
}

View File

@@ -7039,7 +7039,7 @@ promise@^7.0.1:
dependencies:
asap "~2.0.3"
prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -7245,6 +7245,20 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qr.js@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f"
integrity sha1-ys6GOG9ZoNuAUPqQ2baw6IoeNk8=
qrcode.react@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-1.0.1.tgz#2834bb50e5e275ffe5af6906eff15391fe9e38a5"
integrity sha512-8d3Tackk8IRLXTo67Y+c1rpaiXjoz/Dd2HpcMdW//62/x8J1Nbho14Kh8x974t9prsLHN6XqVgcnRiBGFptQmg==
dependencies:
loose-envify "^1.4.0"
prop-types "^15.6.0"
qr.js "0.0.0"
qs@6.7.0:
version "6.7.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"