diff --git a/app/assets/javascripts/directives/views/accountMenu.js b/app/assets/javascripts/directives/views/accountMenu.ts similarity index 53% rename from app/assets/javascripts/directives/views/accountMenu.js rename to app/assets/javascripts/directives/views/accountMenu.ts index daf9581cc..95dd9bae7 100644 --- a/app/assets/javascripts/directives/views/accountMenu.js +++ b/app/assets/javascripts/directives/views/accountMenu.ts @@ -1,7 +1,8 @@ +import { WebDirective } from './../../types'; import { isDesktopApplication, isNullOrUndefined } from '@/utils'; import template from '%/directives/account-menu.pug'; -import { ProtectedAction } from 'snjs'; -import { PureCtrl } from '@Controllers'; +import { ProtectedAction, ContentType, SNComponent } from 'snjs'; +import { PureCtrl } from '@Controllers/abstract/pure_ctrl'; import { STRING_ACCOUNT_MENU_UNCHECK_MERGE, STRING_SIGN_OUT_CONFIRMATION, @@ -18,6 +19,9 @@ import { STRING_GENERATING_REGISTER_KEYS, StringImportError } from '@/strings'; +import { SyncOpStatus } from '@/../../../../snjs/dist/@types/services/sync/sync_op_status'; +import { PasswordWizardType } from '@/types'; +import { BackupFile } from '@/../../../../snjs/dist/@types/services/protocol_service'; const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request'; @@ -25,11 +29,46 @@ const ELEMENT_NAME_AUTH_EMAIL = 'email'; const ELEMENT_NAME_AUTH_PASSWORD = 'password'; const ELEMENT_NAME_AUTH_PASSWORD_CONF = 'password_conf'; +type FormData = { + email: string + user_password: string + password_conf: string + confirmPassword: boolean + showLogin: boolean + showRegister: boolean + showPasscodeForm: boolean + strictSignin?: boolean + ephemeral: boolean + mfa: { payload: any } + userMfaCode?: string + mergeLocal?: boolean + url: string + authenticating: boolean + status: string + passcode: string + confirmPasscode: string + changingPasscode: boolean +} + +type AccountMenuState = { + formData: Partial + appVersion: string + passcodeAutoLockOptions: any + user: any + mutable: any + importData: any +} + class AccountMenuCtrl extends PureCtrl { + + public appVersion: string + private syncStatus?: SyncOpStatus + private closeFunction?: () => void + /* @ngInject */ constructor( - $timeout, - appVersion, + $timeout: ng.ITimeoutService, + appVersion: string, ) { super($timeout); this.appVersion = appVersion; @@ -38,15 +77,19 @@ class AccountMenuCtrl extends PureCtrl { /** @override */ getInitialState() { return { - appVersion: 'v' + (window.electronAppVersion || this.appVersion), - passcodeAutoLockOptions: this.application.getLockService().getAutoLockIntervalOptions(), - user: this.application.getUser(), + appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion), + passcodeAutoLockOptions: this.application!.getLockService().getAutoLockIntervalOptions(), + user: this.application!.getUser(), formData: { mergeLocal: true, ephemeral: false }, mutable: {} - }; + } as AccountMenuState; + } + + getState() { + return this.state as AccountMenuState; } async onAppKeyChange() { @@ -64,9 +107,9 @@ class AccountMenuCtrl extends PureCtrl { refreshedCredentialState() { return { - user: this.application.getUser(), - canAddPasscode: !this.application.isEphemeralSession(), - hasPasscode: this.application.hasPasscode(), + user: this.application!.getUser(), + canAddPasscode: !this.application!.isEphemeralSession(), + hasPasscode: this.application!.hasPasscode(), showPasscodeForm: false }; } @@ -76,7 +119,7 @@ class AccountMenuCtrl extends PureCtrl { this.initProps({ closeFunction: this.closeFunction }); - this.syncStatus = this.application.getSyncStatus(); + this.syncStatus = this.application!.getSyncStatus(); } close() { @@ -86,24 +129,24 @@ class AccountMenuCtrl extends PureCtrl { } async loadHost() { - const host = await this.application.getHost(); + const host = await this.application!.getHost(); this.setState({ server: host, formData: { - ...this.state.formData, + ...this.getState().formData, url: host } }); } onHostInputChange() { - const url = this.state.formData.url; - this.application.setHost(url); + const url = this.getState().formData.url!; + this.application!.setHost(url); } async loadBackupsAvailability() { - const hasUser = !isNullOrUndefined(this.application.getUser()); - const hasPasscode = this.application.hasPasscode(); + const hasUser = !isNullOrUndefined(this.application!.getUser()); + const hasPasscode = this.application!.hasPasscode(); const encryptedAvailable = hasUser || hasPasscode; function encryptionStatusString() { @@ -120,7 +163,7 @@ class AccountMenuCtrl extends PureCtrl { encryptionStatusString: encryptionStatusString(), encryptionEnabled: encryptedAvailable, mutable: { - ...this.state.mutable, + ...this.getState().mutable, backupEncrypted: encryptedAvailable } }); @@ -145,21 +188,21 @@ class AccountMenuCtrl extends PureCtrl { } submitAuthForm() { - if (!this.state.formData.email || !this.state.formData.user_password) { + if (!this.getState().formData.email || !this.getState().formData.user_password) { return; } this.blurAuthFields(); - if (this.state.formData.showLogin) { + if (this.getState().formData.showLogin) { this.login(); } else { this.register(); } } - async setFormDataState(formData) { + async setFormDataState(formData: Partial) { return this.setState({ formData: { - ...this.state.formData, + ...this.getState().formData, ...formData } }); @@ -170,20 +213,21 @@ class AccountMenuCtrl extends PureCtrl { status: STRING_GENERATING_LOGIN_KEYS, authenticating: true }); - const response = await this.application.signIn({ - email: this.state.formData.email, - password: this.state.formData.user_password, - strict: this.state.formData.strictSignin, - ephemeral: this.state.formData.ephemeral, - mfaKeyPath: this.state.formData.mfa && this.state.formData.mfa.payload.mfa_key, - mfaCode: this.state.formData.userMfaCode, - mergeLocal: this.state.formData.mergeLocal - }); + const formData = this.getState().formData; + const response = await this.application!.signIn( + formData.email!, + formData.user_password!, + formData.strictSignin, + formData.ephemeral, + formData.mfa && formData.mfa.payload.mfa_key, + formData.userMfaCode, + formData.mergeLocal + ); const hasError = !response || response.error; if (!hasError) { await this.setFormDataState({ authenticating: false, - user_password: null + user_password: undefined }); this.close(); return; @@ -195,19 +239,17 @@ class AccountMenuCtrl extends PureCtrl { await this.setFormDataState({ showLogin: false, mfa: error, - status: null + status: undefined }); } else { await this.setFormDataState({ showLogin: true, - mfa: null, - status: null, - user_password: null + mfa: undefined, + status: undefined, + user_password: undefined }); if (error.message) { - this.application.alertService.alert({ - text: error.message - }); + this.application!.alertService!.alert(error.message); } } await this.setFormDataState({ @@ -216,11 +258,11 @@ class AccountMenuCtrl extends PureCtrl { } async register() { - const confirmation = this.state.formData.password_conf; - if (confirmation !== this.state.formData.user_password) { - this.application.alertService.alert({ - text: STRING_NON_MATCHING_PASSWORDS - }); + const confirmation = this.getState().formData.password_conf; + if (confirmation !== this.getState().formData.user_password) { + this.application!.alertService!.alert( + STRING_NON_MATCHING_PASSWORDS + ); return; } await this.setFormDataState({ @@ -228,15 +270,15 @@ class AccountMenuCtrl extends PureCtrl { status: STRING_GENERATING_REGISTER_KEYS, authenticating: true }); - const response = await this.application.register({ - email: this.state.formData.email, - password: this.state.formData.user_password, - ephemeral: this.state.formData.ephemeral, - mergeLocal: this.state.formData.mergeLocal - }); + const response = await this.application!.register( + this.getState().formData.email!, + this.getState().formData.user_password!, + this.getState().formData.ephemeral, + this.getState().formData.mergeLocal + ); if (!response || response.error) { await this.setFormDataState({ - status: null + status: undefined }); const error = response ? response.error @@ -244,9 +286,9 @@ class AccountMenuCtrl extends PureCtrl { await this.setFormDataState({ authenticating: false }); - this.application.alertService.alert({ - text: error.message - }); + this.application!.alertService!.alert( + error.message + ); } else { await this.setFormDataState({ authenticating: false }); this.close(); @@ -254,34 +296,38 @@ class AccountMenuCtrl extends PureCtrl { } mergeLocalChanged() { - if (!this.state.formData.mergeLocal) { - this.application.alertService.confirm({ - text: STRING_ACCOUNT_MENU_UNCHECK_MERGE, - destructive: true, - onCancel: () => { + if (!this.getState().formData.mergeLocal) { + this.application!.alertService!.confirm( + STRING_ACCOUNT_MENU_UNCHECK_MERGE, + undefined, + undefined, + undefined, + undefined, + () => { this.setFormDataState({ mergeLocal: true }); - } - }); + }, + true, + ); } } openPasswordWizard() { this.close(); - this.application.presentPasswordWizard(); + this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword); } async openPrivilegesModal() { this.close(); const run = () => { - this.application.presentPrivilegesManagementModal(); + this.application!.presentPrivilegesManagementModal(); }; - const needsPrivilege = await this.application.privilegesService.actionRequiresPrivilege( + const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( ProtectedAction.ManagePrivileges ); if (needsPrivilege) { - this.application.presentPrivilegesModal( + this.application!.presentPrivilegesModal( ProtectedAction.ManagePrivileges, () => { run(); @@ -293,33 +339,37 @@ class AccountMenuCtrl extends PureCtrl { } destroyLocalData() { - this.application.alertService.confirm({ - text: STRING_SIGN_OUT_CONFIRMATION, - destructive: true, - onConfirm: async () => { - await this.application.signOut(); - } - }); + this.application!.alertService!.confirm( + STRING_SIGN_OUT_CONFIRMATION, + undefined, + undefined, + undefined, + async () => { + await this.application!.signOut(); + }, + undefined, + true, + ); } async submitImportPassword() { await this.performImport( - this.state.importData.data, - this.state.importData.password + this.getState().importData.data, + this.getState().importData.password ); } - async readFile(file) { - return new Promise((resolve, reject) => { + async readFile(file: File): Promise { + return new Promise((resolve) => { const reader = new FileReader(); - reader.onload = function (e) { + reader.onload = (e) => { try { - const data = JSON.parse(e.target.result); + const data = JSON.parse(e.target!.result as string); resolve(data); } catch (e) { - this.application.alertService.alert({ - text: STRING_INVALID_IMPORT_FILE - }); + this.application!.alertService!.alert( + STRING_INVALID_IMPORT_FILE + ); } }; reader.readAsText(file); @@ -329,7 +379,7 @@ class AccountMenuCtrl extends PureCtrl { /** * @template */ - async importFileSelected(files) { + async importFileSelected(files: File[]) { const run = async () => { const file = files[0]; const data = await this.readFile(file); @@ -339,7 +389,7 @@ class AccountMenuCtrl extends PureCtrl { if (data.auth_params) { await this.setState({ importData: { - ...this.state.importData, + ...this.getState().importData, requestPassword: true, data: data } @@ -351,14 +401,14 @@ class AccountMenuCtrl extends PureCtrl { element.scrollIntoView(false); } } else { - await this.performImport(data, null); + await this.performImport(data, undefined); } }; - const needsPrivilege = await this.application.privilegesService.actionRequiresPrivilege( + const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( ProtectedAction.ManageBackups ); if (needsPrivilege) { - this.application.presentPrivilegesModal( + this.application!.presentPrivilegesModal( ProtectedAction.ManageBackups, run ); @@ -367,10 +417,10 @@ class AccountMenuCtrl extends PureCtrl { } } - async performImport(data, password) { + async performImport(data: any, password?: string) { await this.setState({ importData: { - ...this.state.importData, + ...this.getState().importData, loading: true } }); @@ -380,44 +430,35 @@ class AccountMenuCtrl extends PureCtrl { }); if (errorCount > 0) { const message = StringImportError(errorCount); - this.application.alertService.alert({ - text: message - }); + this.application!.alertService!.alert( + message + ); } else { - this.application.alertService.alert({ - text: STRING_IMPORT_SUCCESS - }); + this.application!.alertService!.alert( + STRING_IMPORT_SUCCESS + ); } } - async importJSONData(data, password) { - const { affectedItems, errorCount } = await this.application.importData({ - data: data.items, - password: password - }); - for (const item of affectedItems) { - /** - * Don't want to activate any components during import process in - * case of exceptions breaking up the import proccess - */ - if (item.content_type === 'SN|Component') { - item.active = false; - } - } + async importJSONData(data: BackupFile, password?: string) { + const { errorCount } = await this.application!.importData( + data, + password + ); return errorCount; } async downloadDataArchive() { - this.application.getArchiveService().downloadBackup(this.state.mutable.backupEncrypted); + this.application!.getArchiveService().downloadBackup(this.getState().mutable.backupEncrypted); } notesAndTagsCount() { - return this.application.getItems({ - contentType: [ - 'Note', - 'Tag' + return this.application!.getItems( + [ + ContentType.Note, + ContentType.Tag ] - }).length; + ).length; } encryptionStatusForNotes() { @@ -426,22 +467,22 @@ class AccountMenuCtrl extends PureCtrl { } async reloadAutoLockInterval() { - const interval = await this.application.getLockService().getAutoLockInterval(); + const interval = await this.application!.getLockService().getAutoLockInterval(); this.setState({ selectedAutoLockInterval: interval }); } - async selectAutoLockInterval(interval) { + async selectAutoLockInterval(interval: number) { const run = async () => { - await this.application.getLockService().setAutoLockInterval(interval); + await this.application!.getLockService().setAutoLockInterval(interval); this.reloadAutoLockInterval(); }; - const needsPrivilege = await this.application.privilegesService.actionRequiresPrivilege( + const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( ProtectedAction.ManagePasscode ); if (needsPrivilege) { - this.application.presentPrivilegesModal( + this.application!.presentPrivilegesModal( ProtectedAction.ManagePasscode, () => { run(); @@ -456,13 +497,13 @@ class AccountMenuCtrl extends PureCtrl { this.setFormDataState({ showLogin: false, showRegister: false, - user_password: null, - password_conf: null + user_password: undefined, + password_conf: undefined }); } hasPasscode() { - return this.passcodeManager.hasPasscode(); + return this.application!.hasPasscode(); } addPasscodeClicked() { @@ -472,20 +513,20 @@ class AccountMenuCtrl extends PureCtrl { } submitPasscodeForm() { - const passcode = this.state.formData.passcode; - if (passcode !== this.state.formData.confirmPasscode) { - this.application.alertService.alert({ - text: STRING_NON_MATCHING_PASSCODES - }); + const passcode = this.getState().formData.passcode!; + if (passcode !== this.getState().formData.confirmPasscode!) { + this.application!.alertService!.alert( + STRING_NON_MATCHING_PASSCODES + ); return; } - (this.state.formData.changingPasscode - ? this.application.changePasscode(passcode) - : this.application.setPasscode(passcode) + (this.getState().formData.changingPasscode + ? this.application!.changePasscode(passcode) + : this.application!.setPasscode(passcode) ).then(() => { this.setFormDataState({ - passcode: null, - confirmPasscode: null, + passcode: undefined, + confirmPasscode: undefined, showPasscodeForm: false }); }); @@ -493,14 +534,14 @@ class AccountMenuCtrl extends PureCtrl { async changePasscodePressed() { const run = () => { - this.state.formData.changingPasscode = true; + this.getState().formData.changingPasscode = true; this.addPasscodeClicked(); }; - const needsPrivilege = await this.application.privilegesService.actionRequiresPrivilege( + const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( ProtectedAction.ManagePasscode ); if (needsPrivilege) { - this.application.presentPrivilegesModal( + this.application!.presentPrivilegesModal( ProtectedAction.ManagePasscode, run ); @@ -511,24 +552,28 @@ class AccountMenuCtrl extends PureCtrl { async removePasscodePressed() { const run = async () => { - const signedIn = !isNullOrUndefined(await this.application.getUser()); + const signedIn = !isNullOrUndefined(await this.application!.getUser()); let message = STRING_REMOVE_PASSCODE_CONFIRMATION; if (!signedIn) { message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM; } - this.application.alertService.confirm({ - text: message, - destructive: true, - onConfirm: () => { - this.application.removePasscode(); - } - }); + this.application!.alertService!.confirm( + message, + undefined, + undefined, + undefined, + () => { + this.application!.removePasscode(); + }, + undefined, + true, + ); }; - const needsPrivilege = await this.application.privilegesService.actionRequiresPrivilege( + const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege( ProtectedAction.ManagePasscode ); if (needsPrivilege) { - this.application.presentPrivilegesModal( + this.application!.presentPrivilegesModal( ProtectedAction.ManagePasscode, run ); @@ -542,8 +587,9 @@ class AccountMenuCtrl extends PureCtrl { } } -export class AccountMenu { +export class AccountMenu extends WebDirective { constructor() { + super(); this.restrict = 'E'; this.template = template; this.controller = AccountMenuCtrl; diff --git a/app/assets/javascripts/services/lockManager.js b/app/assets/javascripts/services/lockManager.ts similarity index 71% rename from app/assets/javascripts/services/lockManager.js rename to app/assets/javascripts/services/lockManager.ts index 1da7d4123..a8a8f1ae7 100644 --- a/app/assets/javascripts/services/lockManager.js +++ b/app/assets/javascripts/services/lockManager.ts @@ -1,3 +1,4 @@ +import { WebApplication } from './../application'; import { isDesktopApplication } from '@/utils'; import { AppStateEvent } from '@/services/state'; @@ -7,12 +8,20 @@ const LOCK_INTERVAL_NONE = 0; const LOCK_INTERVAL_IMMEDIATE = 1; const LOCK_INTERVAL_ONE_MINUTE = 60 * MILLISECONDS_PER_SECOND; const LOCK_INTERVAL_FIVE_MINUTES = 300 * MILLISECONDS_PER_SECOND; -const LOCK_INTERVAL_ONE_HOUR= 3600 * MILLISECONDS_PER_SECOND; +const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND; const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey"; export class LockManager { - constructor(application) { + + private application: WebApplication + private unsubState: any + private pollFocusInterval: any + private lastFocusState?: 'hidden' | 'visible' + private lockAfterDate?: Date + private lockTimeout?: any + + constructor(application: WebApplication) { this.application = application; setImmediate(() => { this.observeVisibility(); @@ -20,13 +29,15 @@ export class LockManager { } observeVisibility() { - this.unsubState = this.application.getAppState().addObserver((eventName) => { - if(eventName === AppStateEvent.WindowDidBlur) { - this.documentVisibilityChanged(false); - } else if(eventName === AppStateEvent.WindowDidFocus) { - this.documentVisibilityChanged(true); + this.unsubState = this.application.getAppState().addObserver( + async (eventName) => { + if (eventName === AppStateEvent.WindowDidBlur) { + this.documentVisibilityChanged(false); + } else if (eventName === AppStateEvent.WindowDidFocus) { + this.documentVisibilityChanged(true); + } } - }); + ); if (!isDesktopApplication()) { this.beginWebFocusPolling(); } @@ -39,21 +50,24 @@ export class LockManager { } } - async setAutoLockInterval(interval) { - return this.application.setValue(STORAGE_KEY_AUTOLOCK_INTERVAL, interval); + async setAutoLockInterval(interval: number) { + return this.application!.setValue( + STORAGE_KEY_AUTOLOCK_INTERVAL, + interval + ); } async getAutoLockInterval() { - const interval = await this.application.getValue( - STORAGE_KEY_AUTOLOCK_INTERVAL, + const interval = await this.application!.getValue( + STORAGE_KEY_AUTOLOCK_INTERVAL ); - if(interval) { + if (interval) { return interval; } else { return LOCK_INTERVAL_NONE; } } - + /** * Verify document is in focus every so often as visibilitychange event is * not triggered on a typical window blur event but rather on tab changes. @@ -61,9 +75,9 @@ export class LockManager { beginWebFocusPolling() { this.pollFocusInterval = setInterval(() => { const hasFocus = document.hasFocus(); - if(hasFocus && this.lastFocusState === 'hidden') { + if (hasFocus && this.lastFocusState === 'hidden') { this.documentVisibilityChanged(true); - } else if(!hasFocus && this.lastFocusState === 'visible') { + } else if (!hasFocus && this.lastFocusState === 'visible') { this.documentVisibilityChanged(false); } /* Save this to compare against next time around */ @@ -96,12 +110,12 @@ export class LockManager { ]; } - async documentVisibilityChanged(visible) { - if(visible) { + async documentVisibilityChanged(visible: boolean) { + if (visible) { const locked = await this.application.isLocked(); - if( + if ( !locked && - this.lockAfterDate && + this.lockAfterDate && new Date() > this.lockAfterDate ) { this.application.lock(); @@ -114,7 +128,7 @@ export class LockManager { async beginAutoLockTimer() { var interval = await this.getAutoLockInterval(); - if(interval === LOCK_INTERVAL_NONE) { + if (interval === LOCK_INTERVAL_NONE) { return; } /** @@ -123,7 +137,7 @@ export class LockManager { * persisted, as living in memory is sufficient. If memory is cleared, then the * application will lock anyway. */ - const addToNow = (seconds) => { + const addToNow = (seconds: number) => { const date = new Date(); date.setSeconds(date.getSeconds() + seconds); return date; @@ -132,12 +146,12 @@ export class LockManager { this.lockTimeout = setTimeout(() => { this.cancelAutoLockTimer(); this.application.lock(); - this.lockAfterDate = null; + this.lockAfterDate = undefined; }, interval); } cancelAutoLockTimer() { clearTimeout(this.lockTimeout); - this.lockAfterDate = null; + this.lockAfterDate = undefined; } }