From badff1568d6181909caa06c4546c93b75dcba83b Mon Sep 17 00:00:00 2001 From: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com> Date: Mon, 15 Feb 2021 15:36:36 +0100 Subject: [PATCH] feat: clear protection session --- .../directives/views/accountMenu.ts | 232 +++++++++++------- app/assets/javascripts/utils.ts | 36 ++- .../templates/directives/account-menu.pug | 18 +- package.json | 2 +- yarn.lock | 8 +- 5 files changed, 187 insertions(+), 109 deletions(-) diff --git a/app/assets/javascripts/directives/views/accountMenu.ts b/app/assets/javascripts/directives/views/accountMenu.ts index 944a317a0..0e31abf5d 100644 --- a/app/assets/javascripts/directives/views/accountMenu.ts +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -1,5 +1,5 @@ import { WebDirective } from './../../types'; -import { isDesktopApplication, preventRefreshing } from '@/utils'; +import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils'; import template from '%/directives/account-menu.pug'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { @@ -18,17 +18,22 @@ import { STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, STRING_UNSUPPORTED_BACKUP_FILE_VERSION, - Strings + Strings, } from '@/strings'; import { PasswordWizardType } from '@/types'; -import { BackupFile, ContentType, Platform } from '@standardnotes/snjs'; +import { + ApplicationEvent, + BackupFile, + ContentType, + Platform, +} from '@standardnotes/snjs'; import { confirmDialog, alertDialog } from '@/services/alertService'; import { autorun, IReactionDisposer } from 'mobx'; import { storage, StorageKey } from '@/services/localStorage'; import { disableErrorReporting, enableErrorReporting, - errorReportingId + errorReportingId, } from '@/services/errorReporting'; const ELEMENT_NAME_AUTH_EMAIL = 'email'; @@ -52,7 +57,7 @@ type FormData = { passcode: string; confirmPasscode: string; changingPasscode: boolean; -} +}; type AccountMenuState = { formData: Partial; @@ -72,21 +77,19 @@ type AccountMenuState = { showSessions: boolean; errorReportingId: string | null; keyStorageInfo: string | null; -} + protectionsDisabledUntil: string | null; +}; class AccountMenuCtrl extends PureViewCtrl { - - public appVersion: string + public appVersion: string; /** @template */ - private closeFunction?: () => void - private removeBetaWarningListener?: IReactionDisposer - private removeSyncObserver?: IReactionDisposer + private closeFunction?: () => void; + private removeBetaWarningListener?: IReactionDisposer; + private removeSyncObserver?: IReactionDisposer; + private removeProtectionLengthObserver?: () => void; /* @ngInject */ - constructor( - $timeout: ng.ITimeoutService, - appVersion: string, - ) { + constructor($timeout: ng.ITimeoutService, appVersion: string) { super($timeout); this.appVersion = appVersion; } @@ -95,7 +98,9 @@ class AccountMenuCtrl extends PureViewCtrl { getInitialState() { return { appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion), - passcodeAutoLockOptions: this.application.getAutolockService().getAutoLockIntervalOptions(), + passcodeAutoLockOptions: this.application + .getAutolockService() + .getAutoLockIntervalOptions(), user: this.application.getUser(), formData: { mergeLocal: true, @@ -103,12 +108,14 @@ class AccountMenuCtrl extends PureViewCtrl { }, mutable: {}, showBetaWarning: false, - errorReportingEnabled: storage.get(StorageKey.DisableErrorReporting) === false, + errorReportingEnabled: + storage.get(StorageKey.DisableErrorReporting) === false, showSessions: false, errorReportingId: errorReportingId(), keyStorageInfo: Strings.keyStorageInfo(this.application), importData: null, syncInProgress: false, + protectionsDisabledUntil: this.getProtectionsDisabledUntil(), }; } @@ -134,14 +141,16 @@ class AccountMenuCtrl extends PureViewCtrl { user: this.application.getUser(), canAddPasscode: !this.application.isEphemeralSession(), hasPasscode: this.application.hasPasscode(), - showPasscodeForm: false + showPasscodeForm: false, }; } async $onInit() { super.$onInit(); this.setState({ - showSessions: this.appState.enableUnfinishedFeatures && await this.application.userCanManageSessions() + showSessions: + this.appState.enableUnfinishedFeatures && + (await this.application.userCanManageSessions()), }); const sync = this.appState.sync; @@ -153,14 +162,24 @@ class AccountMenuCtrl extends PureViewCtrl { }); this.removeBetaWarningListener = autorun(() => { this.setState({ - showBetaWarning: this.appState.showBetaWarning + showBetaWarning: this.appState.showBetaWarning, }); }); + + this.removeProtectionLengthObserver = this.application.addEventObserver( + async () => { + this.setState({ + protectionsDisabledUntil: this.getProtectionsDisabledUntil(), + }); + }, + ApplicationEvent.ProtectionSessionExpiryDateChanged + ); } deinit() { this.removeSyncObserver?.(); this.removeBetaWarningListener?.(); + this.removeProtectionLengthObserver?.(); super.deinit(); } @@ -170,17 +189,46 @@ class AccountMenuCtrl extends PureViewCtrl { }); } + private getProtectionsDisabledUntil(): string | null { + const protectionExpiry = this.application.getProtectionSessionExpiryDate(); + const now = new Date(); + if (protectionExpiry > now) { + let f: Intl.DateTimeFormat; + if (isSameDay(protectionExpiry, now)) { + f = new Intl.DateTimeFormat(undefined, { + hour: 'numeric', + minute: 'numeric', + }); + } else { + f = new Intl.DateTimeFormat(undefined, { + weekday: 'long', + day: 'numeric', + month: 'short', + hour: 'numeric', + minute: 'numeric', + }); + } + + return f.format(protectionExpiry); + } + return null; + } + async loadHost() { - const host = await this.application!.getHost(); + const host = await this.application.getHost(); this.setState({ server: host, formData: { ...this.getState().formData, - url: host - } + url: host, + }, }); } + enableProtections() { + this.application.clearProtectionSession(); + } + onHostInputChange() { const url = this.getState().formData.url!; this.application!.setHost(url); @@ -195,13 +243,13 @@ class AccountMenuCtrl extends PureViewCtrl { encryptionStatusString: hasUser ? STRING_E2E_ENABLED : hasPasscode - ? STRING_LOCAL_ENC_ENABLED - : STRING_ENC_NOT_ENABLED, + ? STRING_LOCAL_ENC_ENABLED + : STRING_ENC_NOT_ENABLED, encryptionEnabled, mutable: { ...this.getState().mutable, - backupEncrypted: encryptionEnabled - } + backupEncrypted: encryptionEnabled, + }, }); } @@ -213,7 +261,7 @@ class AccountMenuCtrl extends PureViewCtrl { const names = [ ELEMENT_NAME_AUTH_EMAIL, ELEMENT_NAME_AUTH_PASSWORD, - ELEMENT_NAME_AUTH_PASSWORD_CONF + ELEMENT_NAME_AUTH_PASSWORD_CONF, ]; for (const name of names) { const element = document.getElementsByName(name)[0]; @@ -224,7 +272,10 @@ class AccountMenuCtrl extends PureViewCtrl { } submitAuthForm() { - if (!this.getState().formData.email || !this.getState().formData.user_password) { + if ( + !this.getState().formData.email || + !this.getState().formData.user_password + ) { return; } this.blurAuthFields(); @@ -239,15 +290,15 @@ class AccountMenuCtrl extends PureViewCtrl { return this.setState({ formData: { ...this.getState().formData, - ...formData - } + ...formData, + }, }); } async login() { await this.setFormDataState({ status: STRING_GENERATING_LOGIN_KEYS, - authenticating: true + authenticating: true, }); const formData = this.getState().formData; const response = await this.application!.signIn( @@ -261,7 +312,7 @@ class AccountMenuCtrl extends PureViewCtrl { if (!error) { await this.setFormDataState({ authenticating: false, - user_password: undefined + user_password: undefined, }); this.close(); return; @@ -269,28 +320,26 @@ class AccountMenuCtrl extends PureViewCtrl { await this.setFormDataState({ showLogin: true, status: undefined, - user_password: undefined + user_password: undefined, }); if (error.message) { this.application!.alertService!.alert(error.message); } await this.setFormDataState({ - authenticating: false + authenticating: false, }); } async register() { const confirmation = this.getState().formData.password_conf; if (confirmation !== this.getState().formData.user_password) { - this.application!.alertService!.alert( - STRING_NON_MATCHING_PASSWORDS - ); + this.application!.alertService!.alert(STRING_NON_MATCHING_PASSWORDS); return; } await this.setFormDataState({ confirmPassword: false, status: STRING_GENERATING_REGISTER_KEYS, - authenticating: true + authenticating: true, }); const response = await this.application!.register( this.getState().formData.email!, @@ -301,14 +350,12 @@ class AccountMenuCtrl extends PureViewCtrl { const error = response.error; if (error) { await this.setFormDataState({ - status: undefined + status: undefined, }); await this.setFormDataState({ - authenticating: false + authenticating: false, }); - this.application!.alertService!.alert( - error.message - ); + this.application!.alertService!.alert(error.message); } else { await this.setFormDataState({ authenticating: false }); this.close(); @@ -320,8 +367,8 @@ class AccountMenuCtrl extends PureViewCtrl { this.setFormDataState({ mergeLocal: !(await confirmDialog({ text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, - confirmButtonStyle: 'danger' - })) + confirmButtonStyle: 'danger', + })), }); } } @@ -337,17 +384,19 @@ class AccountMenuCtrl extends PureViewCtrl { } async destroyLocalData() { - if (await confirmDialog({ - text: STRING_SIGN_OUT_CONFIRMATION, - confirmButtonStyle: "danger" - })) { + if ( + await confirmDialog({ + text: STRING_SIGN_OUT_CONFIRMATION, + confirmButtonStyle: 'danger', + }) + ) { this.application.signOut(); } } showRegister() { this.setFormDataState({ - showRegister: true + showRegister: true, }); } @@ -359,9 +408,7 @@ class AccountMenuCtrl extends PureViewCtrl { const data = JSON.parse(e.target!.result as string); resolve(data); } catch (e) { - this.application!.alertService!.alert( - STRING_INVALID_IMPORT_FILE - ); + this.application!.alertService!.alert(STRING_INVALID_IMPORT_FILE); } }; reader.readAsText(file); @@ -378,7 +425,8 @@ class AccountMenuCtrl extends PureViewCtrl { return; } if (data.version || data.auth_params || data.keyParams) { - const version = data.version || data.keyParams?.version || data.auth_params?.version; + const version = + data.version || data.keyParams?.version || data.auth_params?.version; if ( this.application.protocolService.supportedVersions().includes(version) ) { @@ -396,52 +444,50 @@ class AccountMenuCtrl extends PureViewCtrl { await this.setState({ importData: { ...this.getState().importData, - loading: true - } + loading: true, + }, }); const result = await this.application.importData(data); this.setState({ - importData: null + importData: null, }); if (!result) { return; } else if ('error' in result) { void alertDialog({ - text: result.error + text: result.error, }); } else if (result.errorCount) { void alertDialog({ - text: StringImportError(result.errorCount) + text: StringImportError(result.errorCount), }); } else { void alertDialog({ - text: STRING_IMPORT_SUCCESS + text: STRING_IMPORT_SUCCESS, }); } } async downloadDataArchive() { - this.application.getArchiveService().downloadBackup(this.getState().mutable.backupEncrypted); + this.application + .getArchiveService() + .downloadBackup(this.getState().mutable.backupEncrypted); } notesAndTagsCount() { - return this.application.getItems( - [ - ContentType.Note, - ContentType.Tag - ] - ).length; + return this.application.getItems([ContentType.Note, ContentType.Tag]) + .length; } encryptionStatusForNotes() { const length = this.notesAndTagsCount(); - return length + "/" + length + " notes and tags encrypted"; + return length + '/' + length + ' notes and tags encrypted'; } async reloadAutoLockInterval() { const interval = await this.application!.getAutolockService().getAutoLockInterval(); this.setState({ - selectedAutoLockInterval: interval + selectedAutoLockInterval: interval, }); } @@ -458,7 +504,7 @@ class AccountMenuCtrl extends PureViewCtrl { showLogin: false, showRegister: false, user_password: undefined, - password_conf: undefined + password_conf: undefined, }); } @@ -468,30 +514,31 @@ class AccountMenuCtrl extends PureViewCtrl { addPasscodeClicked() { this.setFormDataState({ - showPasscodeForm: true + showPasscodeForm: true, }); } async submitPasscodeForm() { const passcode = this.getState().formData.passcode!; if (passcode !== this.getState().formData.confirmPasscode!) { - this.application!.alertService!.alert( - STRING_NON_MATCHING_PASSCODES - ); + this.application!.alertService!.alert(STRING_NON_MATCHING_PASSCODES); return; } - await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, async () => { - if (this.application!.hasPasscode()) { - await this.application!.changePasscode(passcode); - } else { - await this.application!.setPasscode(passcode); + await preventRefreshing( + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE, + async () => { + if (this.application!.hasPasscode()) { + await this.application!.changePasscode(passcode); + } else { + await this.application!.setPasscode(passcode); + } } - }); + ); this.setFormDataState({ passcode: undefined, confirmPasscode: undefined, - showPasscodeForm: false + showPasscodeForm: false, }); this.refreshEncryptionStatus(); } @@ -502,13 +549,18 @@ class AccountMenuCtrl extends PureViewCtrl { } async removePasscodePressed() { - await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => { - if (await this.application!.removePasscode()) { - await this.application.getAutolockService().deleteAutolockPreference(); - await this.reloadAutoLockInterval(); - this.refreshEncryptionStatus(); + await preventRefreshing( + STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, + async () => { + if (await this.application!.removePasscode()) { + await this.application + .getAutolockService() + .deleteAutolockPreference(); + await this.reloadAutoLockInterval(); + this.refreshEncryptionStatus(); + } } - }); + ); } openErrorReportingDialog() { @@ -526,7 +578,7 @@ class AccountMenuCtrl extends PureViewCtrl { anonymized. We use error reports to be alerted when something in our code is causing unexpected errors and crashes in your application experience. - ` + `, }); } @@ -556,7 +608,7 @@ export class AccountMenu extends WebDirective { this.bindToController = true; this.scope = { closeFunction: '&', - application: '=' + application: '=', }; } } diff --git a/app/assets/javascripts/utils.ts b/app/assets/javascripts/utils.ts index 06b08284b..c5f9b6be6 100644 --- a/app/assets/javascripts/utils.ts +++ b/app/assets/javascripts/utils.ts @@ -1,9 +1,9 @@ -import { Platform, platformFromString } from "@standardnotes/snjs"; +import { Platform, platformFromString } from '@standardnotes/snjs'; -declare const process : { +declare const process: { env: { - NODE_ENV: string | null | undefined - } + NODE_ENV: string | null | undefined; + }; }; export const isDev = process.env.NODE_ENV === 'development'; @@ -36,11 +36,10 @@ let sharedDateFormatter: Intl.DateTimeFormat; export function dateToLocalizedString(date: Date) { if (typeof Intl !== 'undefined' && Intl.DateTimeFormat) { if (!sharedDateFormatter) { - const locale = ( - (navigator.languages && navigator.languages.length) + const locale = + navigator.languages && navigator.languages.length ? navigator.languages[0] - : navigator.language - ); + : navigator.language; sharedDateFormatter = new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'numeric', @@ -58,8 +57,21 @@ export function dateToLocalizedString(date: Date) { } } +export function isSameDay(dateA: Date, dateB: Date): boolean { + return ( + dateA.getFullYear() === dateB.getFullYear() && + dateA.getMonth() === dateB.getMonth() && + dateA.getDate() === dateB.getDate() + ); +} + /** Via https://davidwalsh.name/javascript-debounce-function */ -export function debounce(this: any, func: any, wait: number, immediate = false) { +export function debounce( + this: any, + func: any, + wait: number, + immediate = false +) { let timeout: any; return () => { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -131,7 +143,7 @@ if (!Array.prototype.includes) { // 8. Return false return false; - } + }, }); } @@ -153,7 +165,9 @@ declare const __WEB__: boolean; declare const __DESKTOP__: boolean; if (!__WEB__ && !__DESKTOP__) { - throw Error('Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.'); + throw Error( + 'Neither __WEB__ nor __DESKTOP__ is true. Check your configuration files.' + ); } export function isDesktopApplication() { diff --git a/app/assets/templates/directives/account-menu.pug b/app/assets/templates/directives/account-menu.pug index 5461ce92c..ceb885d15 100644 --- a/app/assets/templates/directives/account-menu.pug +++ b/app/assets/templates/directives/account-menu.pug @@ -164,6 +164,19 @@ | {{self.encryptionStatusForNotes()}} p.sk-p | {{self.state.encryptionStatusString}} + .sk-panel-section + .sk-panel-section-title Protections + .sk-panel-section-subtitle.info(ng-if="self.state.protectionsDisabledUntil") + | Protections are disabled until {{self.state.protectionsDisabledUntil}} + .sk-panel-section-subtitle.info(ng-if="!self.state.protectionsDisabledUntil") + | Protections are enabled + p.sk-p + | Actions like viewing protected notes, exporting decrypted backups, + | or revoking an active session, require additional authentication + | like entering your account password or application passcode. + .sk-panel-row(ng-if="self.state.protectionsDisabledUntil") + button.sk-button.info(ng-click="self.enableProtections()") + span.sk-label Enable protections .sk-panel-section .sk-panel-section-title Passcode Lock div(ng-if='!self.state.hasPasscode') @@ -206,8 +219,7 @@ ng-click='self.state.formData.showPasscodeForm = false' ) Cancel div(ng-if='self.state.hasPasscode && !self.state.formData.showPasscodeForm') - .sk-p - | Passcode lock is enabled. + .sk-panel-section-subtitle.info Passcode lock is enabled .sk-notification.contrast .sk-notification-title Options .sk-notification-text @@ -273,7 +285,7 @@ .sk-panel-section .sk-panel-section-title Error Reporting .sk-panel-section-subtitle.info - | Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled.' : 'disabled.' }} + | Automatic error reporting is {{ self.state.errorReportingEnabled ? 'enabled' : 'disabled' }} p.sk-p | Help us improve Standard Notes by automatically submitting | anonymized error reports. diff --git a/package.json b/package.json index 5d1966cd7..008797b11 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "@reach/alert-dialog": "^0.13.0", "@reach/dialog": "^0.13.0", "@standardnotes/sncrypto-web": "^1.2.10", - "@standardnotes/snjs": "^2.0.53", + "@standardnotes/snjs": "^2.0.54", "mobx": "^6.1.6", "preact": "^10.5.12" } diff --git a/yarn.lock b/yarn.lock index 67d8ac19a..549f70f34 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1845,10 +1845,10 @@ "@standardnotes/sncrypto-common" "^1.2.7" libsodium-wrappers "^0.7.8" -"@standardnotes/snjs@^2.0.53": - version "2.0.53" - resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.53.tgz#ec89668fef57bf154dc0e145e4bc883cbe6be241" - integrity sha512-9mlSitWXCBnQtMwhHMIV6/BaLIUXzWuQIMKkqWn43XYojnF33avEJteu0ciffZMAW9A2S7ORerRBVBbJxlqtdg== +"@standardnotes/snjs@^2.0.54": + version "2.0.54" + resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.0.54.tgz#dab1dbf0405c2671aa73e4dbb944863bd434f629" + integrity sha512-q1FErsVthiLpOarpKohoZhvXb8BGvgCBpXzbDZ64/15L2lErFUTYbXxyklBTy5DCbULwUh7wBvkm74JF10Ghlg== dependencies: "@standardnotes/sncrypto-common" "^1.2.9"