Merge branch 'develop' into feature/subscription-info-in-preferences
This commit is contained in:
1
app/assets/javascripts/@types/qrcode.react.d.ts
vendored
Normal file
1
app/assets/javascripts/@types/qrcode.react.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module 'qrcode.react';
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => {
|
||||
);
|
||||
|
||||
useCloseOnClickOutside(
|
||||
contextMenuRef,
|
||||
contextMenuRef,
|
||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export enum RootScopeMessages {
|
||||
ReloadExtendedData = 'reload-ext-data',
|
||||
NewUpdateAvailable = 'new-update-available'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -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 \
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export * from './Content';
|
||||
export * from './MenuItem';
|
||||
export * from './Pane';
|
||||
export * from './PreferencesPane';
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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} />}
|
||||
</>
|
||||
));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
2
app/assets/javascripts/typings/pug.d.ts
vendored
2
app/assets/javascripts/typings/pug.d.ts
vendored
@@ -2,4 +2,4 @@ declare module "*.pug" {
|
||||
import { compileTemplate } from 'pug';
|
||||
const content: compileTemplate;
|
||||
export default content;
|
||||
}
|
||||
}
|
||||
|
||||
2
app/assets/javascripts/typings/stylekit.d.ts
vendored
2
app/assets/javascripts/typings/stylekit.d.ts
vendored
@@ -1 +1 @@
|
||||
declare module "sn-stylekit";
|
||||
declare module "sn-stylekit";
|
||||
|
||||
@@ -172,7 +172,7 @@ export class NotesState {
|
||||
} else {
|
||||
this.activeEditor.setNote(note);
|
||||
}
|
||||
|
||||
|
||||
this.appState.noteTags.reloadTags();
|
||||
await this.onActiveEditorChanged();
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -178,7 +178,7 @@ class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
if (this.submitting) {
|
||||
if (this.submitting || this.state.processing) {
|
||||
return;
|
||||
}
|
||||
this.submitting = true;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -153,7 +153,6 @@ class TagsViewCtrl extends PureViewCtrl<unknown, TagState> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @override */
|
||||
async onAppEvent(eventName: ApplicationEvent) {
|
||||
super.onAppEvent(eventName);
|
||||
|
||||
Reference in New Issue
Block a user