feat: handle unprotected session expiration (#747)

* feat: hide note contents if the protection expires when the protected note is open and wasn't edited for a while

* feat: handle session expiration for opened protected note for both plain advanced editors

* fix: if after canceling  session expiry modal only one unprotected note stays selected, show its contents in the editor

* refactor: handle session expiration for opened protected note (move the logic to web client)

* feat: handle the case of selecting "Don't remember" option in session expiry dialog

* test (WIP): add unit tests for protecting opened note after the session has expired

* test: add remaining unit tests

* refactor: move the opened note protection logic to "editor_view"

* refactor: reviewer comments
- don't rely on user signed-in/out status to require authentication for protected note
- remove unnecessary async/awaits
- better wording on ui

* refactor: reviewer's comments:
 - use snjs method to check if "Don't remember" option is selected in authentication modal
 - move the constant to snjs
 - fix eslint error

* refactor: avoid `any` type for `appEvent` payload

* test: add unit tests

* chore: update function name

* refactor: use simpler protection session event types

* refactor: protected access terminology

* refactor: start counting idle timer after every edit (instead of counting by interval in spite of edits)

* test: unit tests

* style: don't give extra brightness to the "View Note"/"Authenticate" button on hover/focus

* chore: bump snjs version

Co-authored-by: Mo Bitar <me@bitar.io>
This commit is contained in:
Vardan Hakobyan
2021-12-14 19:14:01 +04:00
committed by GitHub
parent f494b106b9
commit 8db549f6f6
26 changed files with 418 additions and 755 deletions

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/Button';
import { SyncQueueStrategy, dateToLocalizedString } from '@standardnotes/snjs';
import { STRING_GENERIC_SYNC_ERROR } from '@/strings';
import { useState } from '@node_modules/preact/hooks';
import { observer } from '@node_modules/mobx-react-lite';
import { observer } from 'mobx-react-lite';
import { WebApplication } from '@/ui_models/application';
import { FunctionComponent } from 'preact';

View File

@@ -4,7 +4,12 @@ import { useCallback, useState } from 'preact/hooks';
import { useEffect } from 'preact/hooks';
import { ApplicationEvent } from '@standardnotes/snjs';
import { isSameDay } from '@/utils';
import { PreferencesGroup, PreferencesSegment, Title, Text } from '@/preferences/components';
import {
PreferencesGroup,
PreferencesSegment,
Title,
Text,
} from '@/preferences/components';
import { Button } from '@/components/Button';
type Props = {
@@ -16,7 +21,9 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
application.clearProtectionSession();
};
const [hasProtections, setHasProtections] = useState(() => application.hasProtectionSources());
const [hasProtections, setHasProtections] = useState(() =>
application.hasProtectionSources()
);
const getProtectionsDisabledUntil = useCallback((): string | null => {
const protectionExpiry = application.getProtectionSessionExpiryDate();
@@ -26,7 +33,7 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
if (isSameDay(protectionExpiry, now)) {
f = new Intl.DateTimeFormat(undefined, {
hour: 'numeric',
minute: 'numeric'
minute: 'numeric',
});
} else {
f = new Intl.DateTimeFormat(undefined, {
@@ -34,7 +41,7 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
day: 'numeric',
month: 'short',
hour: 'numeric',
minute: 'numeric'
minute: 'numeric',
});
}
@@ -43,14 +50,23 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
return null;
}, [application]);
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(getProtectionsDisabledUntil());
const [protectionsDisabledUntil, setProtectionsDisabledUntil] = useState(
getProtectionsDisabledUntil()
);
useEffect(() => {
const removeProtectionSessionExpiryDateChangedObserver = application.addEventObserver(
const removeUnprotectedSessionBeginObserver = application.addEventObserver(
async () => {
setProtectionsDisabledUntil(getProtectionsDisabledUntil());
},
ApplicationEvent.ProtectionSessionExpiryDateChanged
ApplicationEvent.UnprotectedSessionBegan
);
const removeUnprotectedSessionEndObserver = application.addEventObserver(
async () => {
setProtectionsDisabledUntil(getProtectionsDisabledUntil());
},
ApplicationEvent.UnprotectedSessionExpired
);
const removeKeyStatusChangedObserver = application.addEventObserver(
@@ -61,7 +77,8 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
);
return () => {
removeProtectionSessionExpiryDateChangedObserver();
removeUnprotectedSessionBeginObserver();
removeUnprotectedSessionEndObserver();
removeKeyStatusChangedObserver();
};
}, [application, getProtectionsDisabledUntil]);
@@ -74,19 +91,28 @@ export const Protections: FunctionalComponent<Props> = ({ application }) => {
<PreferencesGroup>
<PreferencesSegment>
<Title>Protections</Title>
{protectionsDisabledUntil
? <Text className="info">Protections are disabled until {protectionsDisabledUntil}.</Text>
: <Text className="info">Protections are enabled.</Text>
}
{protectionsDisabledUntil ? (
<Text className="info">
Unprotected access expires at {protectionsDisabledUntil}.
</Text>
) : (
<Text className="info">Protections are enabled.</Text>
)}
<Text className="mt-2">
Actions like viewing protected notes, exporting decrypted backups,
or revoking an active session, require additional authentication
like entering your account password or application passcode.
Actions like viewing or searching protected notes, exporting decrypted
backups, or revoking an active session require additional
authentication such as entering your account password or application
passcode.
</Text>
{protectionsDisabledUntil &&
<Button className="mt-3" type="primary" label="Enable Protections" onClick={enableProtections} />
}
{protectionsDisabledUntil && (
<Button
className="mt-3"
type="primary"
label="End Unprotected Access"
onClick={enableProtections}
/>
)}
</PreferencesSegment>
</PreferencesGroup >
</PreferencesGroup>
);
};

View File

@@ -5,7 +5,8 @@ import {
DisclosurePanel,
} from '@reach/disclosure';
import { FunctionComponent } from 'preact';
import { useState, useRef, useEffect, MouseEventHandler } from 'react';
import { MouseEventHandler } from 'react';
import { useState, useRef, useEffect } from 'preact/hooks';
const DisclosureIconButton: FunctionComponent<{
className?: string;