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 [canAddPasscode, setCanAddPasscode] = useState(!application.isEphemeralSession());
|
||||||
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode());
|
const [hasPasscode, setHasPasscode] = useState(application.hasPasscode());
|
||||||
|
|
||||||
|
|
||||||
const handleAddPassCode = () => {
|
const handleAddPassCode = () => {
|
||||||
setShowPasscodeForm(true);
|
setShowPasscodeForm(true);
|
||||||
setIsPasscodeFocused(true);
|
setIsPasscodeFocused(true);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ interface Props {
|
|||||||
left?: ComponentChild[];
|
left?: ComponentChild[];
|
||||||
right?: ComponentChild[];
|
right?: ComponentChild[];
|
||||||
text?: string;
|
text?: string;
|
||||||
|
onChange?: (text: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,6 +18,7 @@ export const DecoratedInput: FunctionalComponent<Props> = ({
|
|||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
text,
|
text,
|
||||||
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const base =
|
const base =
|
||||||
'rounded py-1.5 px-3 text-input my-1 h-8 flex flex-row items-center gap-4';
|
'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"
|
className="w-full no-border color-black focus:shadow-none"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
value={text}
|
value={text}
|
||||||
|
onChange={(e) =>
|
||||||
|
onChange && onChange((e.target as HTMLInputElement).value)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{right}
|
{right}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const NoteTag = observer(({ appState, tag }: Props) => {
|
|||||||
const getTabIndex = () => {
|
const getTabIndex = () => {
|
||||||
if (focusedTagUuid) {
|
if (focusedTagUuid) {
|
||||||
return focusedTagUuid === tag.uuid ? 0 : -1;
|
return focusedTagUuid === tag.uuid ? 0 : -1;
|
||||||
}
|
}
|
||||||
if (autocompleteInputFocused) {
|
if (autocompleteInputFocused) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const NotesContextMenu = observer(({ application, appState }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useCloseOnClickOutside(
|
useCloseOnClickOutside(
|
||||||
contextMenuRef,
|
contextMenuRef,
|
||||||
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
(open: boolean) => appState.notes.setContextMenuOpen(open)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export enum RootScopeMessages {
|
export enum RootScopeMessages {
|
||||||
ReloadExtendedData = 'reload-ext-data',
|
ReloadExtendedData = 'reload-ext-data',
|
||||||
NewUpdateAvailable = 'new-update-available'
|
NewUpdateAvailable = 'new-update-available'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,20 +6,25 @@ import { observer } from 'mobx-react-lite';
|
|||||||
import { PreferencesMenu } from './preferences-menu';
|
import { PreferencesMenu } from './preferences-menu';
|
||||||
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';
|
||||||
|
|
||||||
const PaneSelector: FunctionComponent<{
|
interface PreferencesProps extends MfaProps {
|
||||||
prefs: PreferencesMenu;
|
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
}> = observer(({ prefs: menu, application }) => {
|
closePreferences: () => void;
|
||||||
switch (menu.selectedPaneId) {
|
}
|
||||||
|
|
||||||
|
const PaneSelector: FunctionComponent<
|
||||||
|
PreferencesProps & { menu: PreferencesMenu }
|
||||||
|
> = observer((props) => {
|
||||||
|
switch (props.menu.selectedPaneId) {
|
||||||
case 'general':
|
case 'general':
|
||||||
return null;
|
return null;
|
||||||
case 'account':
|
case 'account':
|
||||||
return <AccountPreferences application={application} />;
|
return <AccountPreferences application={props.application} />;
|
||||||
case 'appearance':
|
case 'appearance':
|
||||||
return null;
|
return null;
|
||||||
case 'security':
|
case 'security':
|
||||||
return <Security />;
|
return <Security mfaGateway={props.mfaGateway} />;
|
||||||
case 'listed':
|
case 'listed':
|
||||||
return null;
|
return null;
|
||||||
case 'shortcuts':
|
case 'shortcuts':
|
||||||
@@ -33,23 +38,18 @@ const PaneSelector: FunctionComponent<{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const PreferencesCanvas: FunctionComponent<{
|
const PreferencesCanvas: FunctionComponent<
|
||||||
preferences: PreferencesMenu;
|
PreferencesProps & { menu: PreferencesMenu }
|
||||||
application: WebApplication;
|
> = observer((props) => (
|
||||||
}> = observer(({ preferences: prefs, application }) => (
|
|
||||||
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
<div className="flex flex-row flex-grow min-h-0 justify-between">
|
||||||
<PreferencesMenuView menu={prefs}></PreferencesMenuView>
|
<PreferencesMenuView menu={props.menu} />
|
||||||
<PaneSelector prefs={prefs} application={application} />
|
<PaneSelector {...props} />
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|
||||||
const PreferencesView: FunctionComponent<{
|
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
||||||
close: () => void;
|
(props) => {
|
||||||
application: WebApplication;
|
const menu = new PreferencesMenu();
|
||||||
}> = observer(
|
|
||||||
({ close, application }) => {
|
|
||||||
const prefs = new PreferencesMenu();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
<div className="sn-full-screen flex flex-col bg-contrast z-index-preferences">
|
||||||
<TitleBar className="items-center justify-between">
|
<TitleBar className="items-center justify-between">
|
||||||
@@ -58,30 +58,14 @@ const PreferencesView: FunctionComponent<{
|
|||||||
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
<Title className="text-lg">Your preferences for Standard Notes</Title>
|
||||||
<RoundIconButton
|
<RoundIconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
close();
|
props.closePreferences();
|
||||||
}}
|
}}
|
||||||
type="normal"
|
type="normal"
|
||||||
icon="close"
|
icon="close"
|
||||||
/>
|
/>
|
||||||
</TitleBar>
|
</TitleBar>
|
||||||
<PreferencesCanvas preferences={prefs} application={application} />
|
<PreferencesCanvas {...props} menu={menu} />
|
||||||
</div>
|
</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>
|
<h4 className="font-medium text-sm m-0 mb-1">{children}</h4>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Text: FunctionComponent = ({ children }) => (
|
export const Text: FunctionComponent<{ className?: string }> = ({
|
||||||
<p className="text-xs">{children}</p>
|
children,
|
||||||
);
|
className = '',
|
||||||
|
}) => <p className={`${className} text-xs`}>{children}</p>;
|
||||||
|
|
||||||
const buttonClasses = `block bg-default color-text rounded border-solid \
|
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 \
|
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 }) => (
|
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
|
||||||
: children.map((c, i, arr) => (
|
.filter((child) => child != undefined && child !== '')
|
||||||
<>
|
.map((child, i, arr) => (
|
||||||
{c}
|
<>
|
||||||
<HorizontalLine index={i} length={arr.length} />
|
{child}
|
||||||
</>
|
<HorizontalLine index={i} length={arr.length} />
|
||||||
))}
|
</>
|
||||||
|
))
|
||||||
|
: children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
export * from './Content';
|
export * from './Content';
|
||||||
export * from './MenuItem';
|
export * from './MenuItem';
|
||||||
export * from './Pane';
|
export * from './PreferencesPane';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { toDirective } from '../components/utils';
|
import { toDirective } from '../components/utils';
|
||||||
import {
|
import {
|
||||||
PreferencesViewWrapper,
|
PreferencesViewWrapper,
|
||||||
PreferencesWrapperProps,
|
PreferencesViewWrapperProps,
|
||||||
} from './PreferencesView';
|
} from './PreferencesViewWrapper';
|
||||||
|
|
||||||
export const PreferencesDirective = toDirective<PreferencesWrapperProps>(
|
export const PreferencesDirective = toDirective<PreferencesViewWrapperProps>(
|
||||||
PreferencesViewWrapper
|
PreferencesViewWrapper
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { PreferencesPane } from '../components';
|
import { PreferencesPane } from '../components';
|
||||||
import { TwoFactorAuthWrapper } from './two-factor-auth';
|
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>
|
<PreferencesPane>
|
||||||
<TwoFactorAuthWrapper />
|
<TwoFactorAuthWrapper mfaGateway={props.mfaGateway} />
|
||||||
</PreferencesPane>
|
</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 { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { downloadSecretKey } from './download-secret-key';
|
import { downloadSecretKey } from './download-secret-key';
|
||||||
import { TwoFactorActivation } from './model';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
import {
|
||||||
TwoFactorDialog,
|
TwoFactorDialog,
|
||||||
TwoFactorDialogLabel,
|
TwoFactorDialogLabel,
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
|
import QRCode from 'qrcode.react';
|
||||||
|
|
||||||
import { DecoratedInput } from '../../../components/DecoratedInput';
|
import { DecoratedInput } from '../../../components/DecoratedInput';
|
||||||
import { IconButton } from '../../../components/IconButton';
|
import { IconButton } from '../../../components/IconButton';
|
||||||
import { Button } from '@/components/Button';
|
import { Button } from '@/components/Button';
|
||||||
import { TwoFactorActivation } from './model';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
import {
|
||||||
TwoFactorDialog,
|
TwoFactorDialog,
|
||||||
TwoFactorDialogLabel,
|
TwoFactorDialogLabel,
|
||||||
@@ -35,7 +38,7 @@ export const ScanQRCode: FunctionComponent<{
|
|||||||
<TwoFactorDialogDescription>
|
<TwoFactorDialogDescription>
|
||||||
<div className="flex flex-row gap-3 items-center">
|
<div className="flex flex-row gap-3 items-center">
|
||||||
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
<div className="w-25 h-25 flex items-center justify-center bg-info">
|
||||||
QR code
|
<QRCode value={act.qrCode} size={100} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-grow flex flex-col gap-2">
|
<div className="flex-grow flex flex-col gap-2">
|
||||||
<div className="flex flex-row gap-1 items-center">
|
<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 { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { TwoFactorActivation } from './model';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import { SaveSecretKey } from './SaveSecretKey';
|
import { SaveSecretKey } from './SaveSecretKey';
|
||||||
import { ScanQRCode } from './ScanQRCode';
|
import { ScanQRCode } from './ScanQRCode';
|
||||||
import { Verification } from './Verification';
|
import { Verification } from './Verification';
|
||||||
@@ -9,10 +9,12 @@ export const TwoFactorActivationView: FunctionComponent<{
|
|||||||
activation: TwoFactorActivation;
|
activation: TwoFactorActivation;
|
||||||
}> = observer(({ activation: act }) => (
|
}> = 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,
|
is2FADisabled,
|
||||||
is2FAEnabled,
|
is2FAEnabled,
|
||||||
TwoFactorAuth,
|
TwoFactorAuth,
|
||||||
} from './model';
|
} from './TwoFactorAuth';
|
||||||
import { TwoFactorDisabledView } from './TwoFactorDisabledView';
|
import { TwoFactorDisabledView } from './TwoFactorDisabledView';
|
||||||
import { TwoFactorEnabledView } from './TwoFactorEnabledView';
|
|
||||||
import { TwoFactorActivationView } from './TwoFactorActivationView';
|
import { TwoFactorActivationView } from './TwoFactorActivationView';
|
||||||
|
|
||||||
export const TwoFactorAuthView: FunctionComponent<{
|
export const TwoFactorAuthView: FunctionComponent<{
|
||||||
@@ -28,6 +27,9 @@ export const TwoFactorAuthView: FunctionComponent<{
|
|||||||
<Text>
|
<Text>
|
||||||
An extra layer of security when logging in to your account.
|
An extra layer of security when logging in to your account.
|
||||||
</Text>
|
</Text>
|
||||||
|
{auth.errorMessage != null && (
|
||||||
|
<Text className="color-danger">{auth.errorMessage}</Text>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Switch
|
<Switch
|
||||||
checked={!is2FADisabled(auth.status)}
|
checked={!is2FADisabled(auth.status)}
|
||||||
@@ -35,19 +37,15 @@ export const TwoFactorAuthView: FunctionComponent<{
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<PreferencesSegment>
|
|
||||||
{is2FAEnabled(auth.status) && (
|
|
||||||
<TwoFactorEnabledView
|
|
||||||
secretKey={auth.status.secretKey}
|
|
||||||
authCode={auth.status.authCode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{is2FAActivation(auth.status) && (
|
{is2FAActivation(auth.status) ? (
|
||||||
<TwoFactorActivationView activation={auth.status} />
|
<TwoFactorActivationView activation={auth.status} />
|
||||||
)}
|
) : null}
|
||||||
|
|
||||||
{!is2FAEnabled(auth.status) && <TwoFactorDisabledView />}
|
{!is2FAEnabled(auth.status) ? (
|
||||||
</PreferencesSegment>
|
<PreferencesSegment>
|
||||||
|
<TwoFactorDisabledView />
|
||||||
|
</PreferencesSegment>
|
||||||
|
) : null}
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ export const TwoFactorDialog: FunctionComponent<{
|
|||||||
return (
|
return (
|
||||||
<AlertDialog leastDestructiveRef={ldRef}>
|
<AlertDialog leastDestructiveRef={ldRef}>
|
||||||
{/* sn-component is focusable by default, but doesn't stretch to child width
|
{/* sn-component is focusable by default, but doesn't stretch to child width
|
||||||
resulting in a badly focused dialog. Utility classes are not available
|
resulting in a badly focused dialog. Utility classes are not available
|
||||||
at the sn-component level, only below it. tabIndex -1 disables focus
|
at the sn-component level, only below it. tabIndex -1 disables focus
|
||||||
and enables it on the child component */}
|
and enables it on the child component */}
|
||||||
<div tabIndex={-1} className="sn-component">
|
<div tabIndex={-1} className="sn-component">
|
||||||
<div
|
<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 { DecoratedInput } from '@/components/DecoratedInput';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { TwoFactorActivation } from './model';
|
import { TwoFactorActivation } from './TwoFactorActivation';
|
||||||
import {
|
import {
|
||||||
TwoFactorDialog,
|
TwoFactorDialog,
|
||||||
TwoFactorDialogLabel,
|
TwoFactorDialogLabel,
|
||||||
@@ -17,11 +17,7 @@ export const Verification: FunctionComponent<{
|
|||||||
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
act.verificationStatus === 'invalid' ? 'border-dark-red' : '';
|
||||||
return (
|
return (
|
||||||
<TwoFactorDialog>
|
<TwoFactorDialog>
|
||||||
<TwoFactorDialogLabel
|
<TwoFactorDialogLabel closeDialog={act.cancelActivation}>
|
||||||
closeDialog={() => {
|
|
||||||
act.cancelActivation();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Step 3 of 3 - Verification
|
Step 3 of 3 - Verification
|
||||||
</TwoFactorDialogLabel>
|
</TwoFactorDialogLabel>
|
||||||
<TwoFactorDialogDescription>
|
<TwoFactorDialogDescription>
|
||||||
@@ -30,20 +26,26 @@ export const Verification: FunctionComponent<{
|
|||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
・Enter your <b>secret key</b>:
|
・Enter your <b>secret key</b>:
|
||||||
</div>
|
</div>
|
||||||
<DecoratedInput className={borderInv} />
|
<DecoratedInput
|
||||||
|
className={borderInv}
|
||||||
|
onChange={act.setInputSecretKey}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center gap-2">
|
<div className="flex flex-row items-center gap-2">
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
・Verify the <b>authentication code</b> generated by your
|
・Verify the <b>authentication code</b> generated by your
|
||||||
authenticator app:
|
authenticator app:
|
||||||
</div>
|
</div>
|
||||||
<DecoratedInput className={`w-30 ${borderInv}`} />
|
<DecoratedInput
|
||||||
|
className={`w-30 ${borderInv}`}
|
||||||
|
onChange={act.setInputOtpToken}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TwoFactorDialogDescription>
|
</TwoFactorDialogDescription>
|
||||||
<TwoFactorDialogButtons>
|
<TwoFactorDialogButtons>
|
||||||
{act.verificationStatus === 'invalid' && (
|
{act.verificationStatus === 'invalid' && (
|
||||||
<div className="text-sm color-dark-red">
|
<div className="text-sm color-danger">
|
||||||
Incorrect credentials, please try again.
|
Incorrect credentials, please try again.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -51,13 +53,13 @@ export const Verification: FunctionComponent<{
|
|||||||
className="min-w-20"
|
className="min-w-20"
|
||||||
type="normal"
|
type="normal"
|
||||||
label="Back"
|
label="Back"
|
||||||
onClick={() => act.openSaveSecretKey()}
|
onClick={act.openSaveSecretKey}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-20"
|
className="min-w-20"
|
||||||
type="primary"
|
type="primary"
|
||||||
label="Next"
|
label="Next"
|
||||||
onClick={() => act.enable2FA('X', 'X')}
|
onClick={act.enable2FA}
|
||||||
/>
|
/>
|
||||||
</TwoFactorDialogButtons>
|
</TwoFactorDialogButtons>
|
||||||
</TwoFactorDialog>
|
</TwoFactorDialog>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { observer } from 'mobx-react-lite';
|
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { useState } from 'preact/hooks';
|
import { useState } from 'preact/hooks';
|
||||||
import { TwoFactorAuth } from './model';
|
import { MfaProps } from './MfaProps';
|
||||||
|
import { TwoFactorAuth } from './TwoFactorAuth';
|
||||||
import { TwoFactorAuthView } from './TwoFactorAuthView';
|
import { TwoFactorAuthView } from './TwoFactorAuthView';
|
||||||
|
|
||||||
export const TwoFactorAuthWrapper: FunctionComponent = () => {
|
export const TwoFactorAuthWrapper: FunctionComponent<MfaProps> = ({
|
||||||
const [auth] = useState(() => new TwoFactorAuth());
|
mfaGateway,
|
||||||
|
}) => {
|
||||||
|
const [auth] = useState(() => new TwoFactorAuth(mfaGateway));
|
||||||
|
auth.fetchStatus();
|
||||||
return <TwoFactorAuthView auth={auth} />;
|
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';
|
import { compileTemplate } from 'pug';
|
||||||
const content: compileTemplate;
|
const content: compileTemplate;
|
||||||
export default content;
|
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 {
|
} else {
|
||||||
this.activeEditor.setNote(note);
|
this.activeEditor.setNote(note);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.appState.noteTags.reloadTags();
|
this.appState.noteTags.reloadTags();
|
||||||
await this.onActiveEditorChanged();
|
await this.onActiveEditorChanged();
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
|
import { SNComponent, ComponentArea, removeFromArray, addIfUnique , UuidString } from '@standardnotes/snjs';
|
||||||
import { WebApplication } from './application';
|
import { WebApplication } from './application';
|
||||||
|
|
||||||
|
|
||||||
/** Areas that only allow a single component to be active */
|
/** Areas that only allow a single component to be active */
|
||||||
const SingleComponentAreas = [
|
const SingleComponentAreas = [
|
||||||
ComponentArea.Editor,
|
ComponentArea.Editor,
|
||||||
@@ -82,7 +81,6 @@ export class ComponentGroup {
|
|||||||
return this.application.getAll(this.activeComponents) as SNComponent[];
|
return this.application.getAll(this.activeComponents) as SNComponent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies observer when the active editor has changed.
|
* Notifies observer when the active editor has changed.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ class ChallengeModalCtrl extends PureViewCtrl<unknown, ChallengeModalState> {
|
|||||||
if (!this.validate()) {
|
if (!this.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.submitting) {
|
if (this.submitting || this.state.processing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.submitting = true;
|
this.submitting = true;
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ export { EditorView } from './editor/editor_view';
|
|||||||
export { FooterView } from './footer/footer_view';
|
export { FooterView } from './footer/footer_view';
|
||||||
export { NotesView } from './notes/notes_view';
|
export { NotesView } from './notes/notes_view';
|
||||||
export { TagsView } from './tags/tags_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 */
|
/** @override */
|
||||||
async onAppEvent(eventName: ApplicationEvent) {
|
async onAppEvent(eventName: ApplicationEvent) {
|
||||||
super.onAppEvent(eventName);
|
super.onAppEvent(eventName);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "standard-notes-web",
|
"name": "standard-notes-web",
|
||||||
"version": "3.8.21",
|
"version": "3.8.22",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -74,6 +74,7 @@
|
|||||||
"@standardnotes/snjs": "2.12.1",
|
"@standardnotes/snjs": "2.12.1",
|
||||||
"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",
|
||||||
|
"qrcode.react": "^1.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -7039,7 +7039,7 @@ promise@^7.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
asap "~2.0.3"
|
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"
|
version "15.7.2"
|
||||||
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
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"
|
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||||
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
|
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:
|
qs@6.7.0:
|
||||||
version "6.7.0"
|
version "6.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
|
||||||
|
|||||||
Reference in New Issue
Block a user