Files
standardnotes-app-web/app/assets/javascripts/directives/views/accountMenu.ts
Baptiste Grob bef17ef534 Release/3.6.0 (#527)
* feat: (wip) authorize note access

* fix: remove multiEditorEnabled

* refactor: update SNJS + eslint

* refactor: remove privileges in favor of SNJS protections

* fix: do not close editor when editing an archived note

* chore: remove progress indicator for webpack dev server

* fix: add rel="noreferrer" to bugsnag links

* chore(deps): upgrade snjs

* chore(deps): upgrade snjs

* feat: batch manager protection + react challenge modal + eslint fix

* fix: lint errors

* fix: launch state error

* fix: challenge modal: cancel instead of dismiss when pressing escape

* feat: improve focus styles

* fix: cancel session revoking when pressing escape on confirm dialog

* fix: lint warning

* chore(deps): upgrade minor versions

* feat: make SNWebCrypto a constant

* feat: add random identifier to bugsnag reports

* fix: check onKeyUp instead of onKeyDown

* feat: implement SNJS backup file password retrieval

* chore(deps): upgrade snjs

* feat: display warning banner when using the app with no account

* fix: properly color svg button

* fix: wording

* fix: hide account warning after login + improve key storage wording

* chore(deps): upgrade stylekit

* feat: use stylekit fonts for the editor

* chore(deps): bump nokogiri from 1.10.8 to 1.11.1 (#511)

Bumps [nokogiri](https://github.com/sparklemotion/nokogiri) from 1.10.8 to 1.11.1.
- [Release notes](https://github.com/sparklemotion/nokogiri/releases)
- [Changelog](https://github.com/sparklemotion/nokogiri/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sparklemotion/nokogiri/compare/v1.10.8...v1.11.1)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* chore(deps): bump ini from 1.3.5 to 1.3.8 (#504)

Bumps [ini](https://github.com/isaacs/ini) from 1.3.5 to 1.3.8.
- [Release notes](https://github.com/isaacs/ini/releases)
- [Commits](https://github.com/isaacs/ini/compare/v1.3.5...v1.3.8)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* fix: rename master branch to main

* fix: add missing placeholders for submodules (#516)

Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

* chore(deps): upgrade snjs, babel, typescript, reach, mobx, preact

* feat: clear protection session

* fix: use correct close icon size

* fix: hide protections paragraph when no account or passcode exist

* chore(deps): remove unused dependencies

* fix: button casing

* feat: implement SNApplication.hasProtectionSources

* chore(version): 3.6.0

* feat: enable sessions management for every build

* feat: make "Protected" flag more subtle

* fix: only match protected note title

* fix: remove inconsistencies between protected note label and date

* feat: show warning when protecting a note with no protection source

* feat: make unprotecting a note a protected action

* chore(deps): upgrade snjs

* chore(version): 3.6.0-beta01

* fix: run docker with root to fix crashing on Linux (undoes 62da387d3a) (#525)

* feat: make encrypted backups protected (#524)

Co-authored-by: Baptiste Grob <60621355+baptiste-grob@users.noreply.github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: proletarius101 <54175165+proletarius101@users.noreply.github.com>
Co-authored-by: Darius JJ Chuck <79410894+standarius@users.noreply.github.com>
Co-authored-by: Antonella Sgarlatta <antonella@standardnotes.org>
2021-03-02 15:44:40 +01:00

617 lines
16 KiB
TypeScript

import { WebDirective } from './../../types';
import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils';
import template from '%/directives/account-menu.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import {
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
STRING_SIGN_OUT_CONFIRMATION,
STRING_E2E_ENABLED,
STRING_LOCAL_ENC_ENABLED,
STRING_ENC_NOT_ENABLED,
STRING_IMPORT_SUCCESS,
STRING_NON_MATCHING_PASSCODES,
STRING_NON_MATCHING_PASSWORDS,
STRING_INVALID_IMPORT_FILE,
STRING_GENERATING_LOGIN_KEYS,
STRING_GENERATING_REGISTER_KEYS,
StringImportError,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
Strings,
} from '@/strings';
import { PasswordWizardType } from '@/types';
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,
} from '@/services/errorReporting';
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;
mergeLocal?: boolean;
url: string;
authenticating: boolean;
status: string;
passcode: string;
confirmPasscode: string;
changingPasscode: boolean;
};
type AccountMenuState = {
formData: Partial<FormData>;
appVersion: string;
passcodeAutoLockOptions: any;
user: any;
mutable: any;
importData: any;
encryptionStatusString?: string;
server?: string;
encryptionEnabled?: boolean;
selectedAutoLockInterval?: unknown;
showBetaWarning: boolean;
errorReportingEnabled: boolean;
syncInProgress: boolean;
syncError?: string;
showSessions: boolean;
errorReportingId: string | null;
keyStorageInfo: string | null;
protectionsDisabledUntil: string | null;
};
class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
public appVersion: string;
/** @template */
private closeFunction?: () => void;
private removeBetaWarningListener?: IReactionDisposer;
private removeSyncObserver?: IReactionDisposer;
private removeProtectionLengthObserver?: () => void;
/* @ngInject */
constructor($timeout: ng.ITimeoutService, appVersion: string) {
super($timeout);
this.appVersion = appVersion;
}
/** @override */
getInitialState() {
return {
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
passcodeAutoLockOptions: this.application
.getAutolockService()
.getAutoLockIntervalOptions(),
user: this.application.getUser(),
formData: {
mergeLocal: true,
ephemeral: false,
},
mutable: {},
showBetaWarning: false,
errorReportingEnabled:
storage.get(StorageKey.DisableErrorReporting) === false,
showSessions: false,
errorReportingId: errorReportingId(),
keyStorageInfo: Strings.keyStorageInfo(this.application),
importData: null,
syncInProgress: false,
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
};
}
getState() {
return this.state as AccountMenuState;
}
async onAppKeyChange() {
super.onAppKeyChange();
this.setState(this.refreshedCredentialState());
}
async onAppLaunch() {
super.onAppLaunch();
this.setState(this.refreshedCredentialState());
this.loadHost();
this.reloadAutoLockInterval();
this.refreshEncryptionStatus();
}
refreshedCredentialState() {
return {
user: this.application.getUser(),
canAddPasscode: !this.application.isEphemeralSession(),
hasPasscode: this.application.hasPasscode(),
showPasscodeForm: false,
};
}
async $onInit() {
super.$onInit();
this.setState({
showSessions: await this.application.userCanManageSessions()
});
const sync = this.appState.sync;
this.removeSyncObserver = autorun(() => {
this.setState({
syncInProgress: sync.inProgress,
syncError: sync.errorMessage,
});
});
this.removeBetaWarningListener = autorun(() => {
this.setState({
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();
}
close() {
this.$timeout(() => {
this.closeFunction?.();
});
}
hasProtections() {
return this.application.hasProtectionSources();
}
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();
this.setState({
server: host,
formData: {
...this.getState().formData,
url: host,
},
});
}
enableProtections() {
this.application.clearProtectionSession();
}
onHostInputChange() {
const url = this.getState().formData.url!;
this.application!.setHost(url);
}
refreshEncryptionStatus() {
const hasUser = this.application!.hasAccount();
const hasPasscode = this.application!.hasPasscode();
const encryptionEnabled = hasUser || hasPasscode;
this.setState({
encryptionStatusString: hasUser
? STRING_E2E_ENABLED
: hasPasscode
? STRING_LOCAL_ENC_ENABLED
: STRING_ENC_NOT_ENABLED,
encryptionEnabled,
mutable: {
...this.getState().mutable,
backupEncrypted: encryptionEnabled,
},
});
}
submitMfaForm() {
this.login();
}
blurAuthFields() {
const names = [
ELEMENT_NAME_AUTH_EMAIL,
ELEMENT_NAME_AUTH_PASSWORD,
ELEMENT_NAME_AUTH_PASSWORD_CONF,
];
for (const name of names) {
const element = document.getElementsByName(name)[0];
if (element) {
element.blur();
}
}
}
submitAuthForm() {
if (
!this.getState().formData.email ||
!this.getState().formData.user_password
) {
return;
}
this.blurAuthFields();
if (this.getState().formData.showLogin) {
this.login();
} else {
this.register();
}
}
async setFormDataState(formData: Partial<FormData>) {
return this.setState({
formData: {
...this.getState().formData,
...formData,
},
});
}
async login() {
await this.setFormDataState({
status: STRING_GENERATING_LOGIN_KEYS,
authenticating: true,
});
const formData = this.getState().formData;
const response = await this.application!.signIn(
formData.email!,
formData.user_password!,
formData.strictSignin,
formData.ephemeral,
formData.mergeLocal
);
const error = response.error;
if (!error) {
await this.setFormDataState({
authenticating: false,
user_password: undefined,
});
this.close();
return;
}
await this.setFormDataState({
showLogin: true,
status: undefined,
user_password: undefined,
});
if (error.message) {
this.application!.alertService!.alert(error.message);
}
await this.setFormDataState({
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);
return;
}
await this.setFormDataState({
confirmPassword: false,
status: STRING_GENERATING_REGISTER_KEYS,
authenticating: true,
});
const response = await this.application!.register(
this.getState().formData.email!,
this.getState().formData.user_password!,
this.getState().formData.ephemeral,
this.getState().formData.mergeLocal
);
const error = response.error;
if (error) {
await this.setFormDataState({
status: undefined,
});
await this.setFormDataState({
authenticating: false,
});
this.application!.alertService!.alert(error.message);
} else {
await this.setFormDataState({ authenticating: false });
this.close();
}
}
async mergeLocalChanged() {
if (!this.getState().formData.mergeLocal) {
this.setFormDataState({
mergeLocal: !(await confirmDialog({
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
confirmButtonStyle: 'danger',
})),
});
}
}
openPasswordWizard() {
this.close();
this.application!.presentPasswordWizard(PasswordWizardType.ChangePassword);
}
openSessionsModal() {
this.close();
this.appState.openSessionsModal();
}
async destroyLocalData() {
if (
await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION,
confirmButtonStyle: 'danger',
})
) {
this.application.signOut();
}
}
showRegister() {
this.setFormDataState({
showRegister: true,
});
}
async readFile(file: File): Promise<any> {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target!.result as string);
resolve(data);
} catch (e) {
this.application!.alertService!.alert(STRING_INVALID_IMPORT_FILE);
}
};
reader.readAsText(file);
});
}
/**
* @template
*/
async importFileSelected(files: File[]) {
const file = files[0];
const data = await this.readFile(file);
if (!data) {
return;
}
if (data.version || data.auth_params || data.keyParams) {
const version =
data.version || data.keyParams?.version || data.auth_params?.version;
if (
this.application.protocolService.supportedVersions().includes(version)
) {
await this.performImport(data);
} else {
await this.setState({ importData: null });
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
}
} else {
await this.performImport(data);
}
}
async performImport(data: BackupFile) {
await this.setState({
importData: {
...this.getState().importData,
loading: true,
},
});
const result = await this.application.importData(data);
this.setState({
importData: null,
});
if (!result) {
return;
} else if ('error' in result) {
void alertDialog({
text: result.error,
});
} else if (result.errorCount) {
void alertDialog({
text: StringImportError(result.errorCount),
});
} else {
void alertDialog({
text: STRING_IMPORT_SUCCESS,
});
}
}
async downloadDataArchive() {
this.application
.getArchiveService()
.downloadBackup(this.getState().mutable.backupEncrypted);
}
notesAndTagsCount() {
return this.application.getItems([ContentType.Note, ContentType.Tag])
.length;
}
encryptionStatusForNotes() {
const length = this.notesAndTagsCount();
return length + '/' + length + ' notes and tags encrypted';
}
async reloadAutoLockInterval() {
const interval = await this.application!.getAutolockService().getAutoLockInterval();
this.setState({
selectedAutoLockInterval: interval,
});
}
async selectAutoLockInterval(interval: number) {
if (!(await this.application.authorizeAutolockIntervalChange())) {
return;
}
await this.application!.getAutolockService().setAutoLockInterval(interval);
this.reloadAutoLockInterval();
}
hidePasswordForm() {
this.setFormDataState({
showLogin: false,
showRegister: false,
user_password: undefined,
password_conf: undefined,
});
}
hasPasscode() {
return this.application!.hasPasscode();
}
addPasscodeClicked() {
this.setFormDataState({
showPasscodeForm: true,
});
}
async submitPasscodeForm() {
const passcode = this.getState().formData.passcode!;
if (passcode !== this.getState().formData.confirmPasscode!) {
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);
}
}
);
this.setFormDataState({
passcode: undefined,
confirmPasscode: undefined,
showPasscodeForm: false,
});
this.refreshEncryptionStatus();
}
async changePasscodePressed() {
this.getState().formData.changingPasscode = true;
this.addPasscodeClicked();
}
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();
}
}
);
}
openErrorReportingDialog() {
alertDialog({
title: 'Data sent during automatic error reporting',
text: `
We use <a target="_blank" rel="noreferrer" href="https://www.bugsnag.com/">Bugsnag</a>
to automatically report errors that occur while the app is running. See
<a target="_blank" rel="noreferrer" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
this article, paragraph 'Browser' under 'Sending diagnostic data',
</a>
to see what data is included in error reports.
<br><br>
Error reports never include IP addresses and are fully
anonymized. We use error reports to be alerted when something in our
code is causing unexpected errors and crashes in your application
experience.
`,
});
}
toggleErrorReportingEnabled() {
if (this.state.errorReportingEnabled) {
disableErrorReporting();
} else {
enableErrorReporting();
}
if (!this.state.syncInProgress) {
window.location.reload();
}
}
isDesktopApplication() {
return isDesktopApplication();
}
}
export class AccountMenu extends WebDirective {
constructor() {
super();
this.restrict = 'E';
this.template = template;
this.controller = AccountMenuCtrl;
this.controllerAs = 'self';
this.bindToController = true;
this.scope = {
closeFunction: '&',
application: '=',
};
}
}