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>
This commit is contained in:
@@ -22,7 +22,7 @@ export function clickOutside($document: ng.IDocumentService) {
|
||||
$scope.$apply(attrs.clickOutside);
|
||||
didApplyClickOutside = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$scope.$on('$destroy', () => {
|
||||
attrs.clickOutside = undefined;
|
||||
|
||||
@@ -16,23 +16,23 @@ export function delayHide($timeout: ng.ITimeoutService) {
|
||||
scopeAny.hidePromise = null;
|
||||
}
|
||||
showElement(true);
|
||||
}
|
||||
};
|
||||
|
||||
const hideSpinner = () => {
|
||||
scopeAny.hidePromise = $timeout(
|
||||
showElement.bind(this as any, false),
|
||||
getDelay()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const showElement = (show: boolean) => {
|
||||
show ? elem.css({ display: '' }) : elem.css({ display: 'none' });
|
||||
}
|
||||
};
|
||||
|
||||
const getDelay = () => {
|
||||
const delay = parseInt(scopeAny.delay);
|
||||
return angular.isNumber(delay) ? delay : 200;
|
||||
}
|
||||
};
|
||||
|
||||
showElement(false);
|
||||
// Whenever the scope variable updates we simply
|
||||
|
||||
@@ -5,7 +5,7 @@ export function elemReady($parse: ng.IParseService) {
|
||||
link: function($scope: ng.IScope, elem: JQLite, attrs: any) {
|
||||
elem.ready(function() {
|
||||
$scope.$apply(function() {
|
||||
var func = $parse(attrs.elemReady);
|
||||
const func = $parse(attrs.elemReady);
|
||||
func($scope);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export function infiniteScroll() {
|
||||
};
|
||||
elem.on('scroll', scopeAny.onScroll);
|
||||
scope.$on('$destroy', () => {
|
||||
elem.off('scroll', scopeAny.onScroll);;
|
||||
elem.off('scroll', scopeAny.onScroll);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { isDesktopApplication, preventRefreshing } from '@/utils';
|
||||
import { isDesktopApplication, isSameDay, preventRefreshing } from '@/utils';
|
||||
import template from '%/directives/account-menu.pug';
|
||||
import { ProtectedAction, ContentType } from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import {
|
||||
STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
@@ -10,8 +9,6 @@ import {
|
||||
STRING_LOCAL_ENC_ENABLED,
|
||||
STRING_ENC_NOT_ENABLED,
|
||||
STRING_IMPORT_SUCCESS,
|
||||
STRING_REMOVE_PASSCODE_CONFIRMATION,
|
||||
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
|
||||
STRING_NON_MATCHING_PASSCODES,
|
||||
STRING_NON_MATCHING_PASSWORDS,
|
||||
STRING_INVALID_IMPORT_FILE,
|
||||
@@ -20,38 +17,47 @@ import {
|
||||
StringImportError,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE,
|
||||
STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL,
|
||||
STRING_UNSUPPORTED_BACKUP_FILE_VERSION
|
||||
STRING_UNSUPPORTED_BACKUP_FILE_VERSION,
|
||||
Strings,
|
||||
} from '@/strings';
|
||||
import { PasswordWizardType } from '@/types';
|
||||
import { BackupFile } 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';
|
||||
|
||||
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
||||
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
|
||||
}
|
||||
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>;
|
||||
@@ -60,30 +66,30 @@ type AccountMenuState = {
|
||||
user: any;
|
||||
mutable: any;
|
||||
importData: any;
|
||||
encryptionStatusString: string;
|
||||
server: string;
|
||||
encryptionEnabled: boolean;
|
||||
selectedAutoLockInterval: any;
|
||||
encryptionStatusString?: string;
|
||||
server?: string;
|
||||
encryptionEnabled?: boolean;
|
||||
selectedAutoLockInterval?: unknown;
|
||||
showBetaWarning: boolean;
|
||||
errorReportingEnabled: boolean;
|
||||
syncInProgress: boolean;
|
||||
syncError: string;
|
||||
syncError?: string;
|
||||
showSessions: boolean;
|
||||
}
|
||||
errorReportingId: string | null;
|
||||
keyStorageInfo: string | null;
|
||||
protectionsDisabledUntil: string | null;
|
||||
};
|
||||
|
||||
class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
|
||||
public appVersion: string
|
||||
class AccountMenuCtrl extends PureViewCtrl<unknown, AccountMenuState> {
|
||||
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;
|
||||
}
|
||||
@@ -92,17 +98,25 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
getInitialState() {
|
||||
return {
|
||||
appVersion: 'v' + ((window as any).electronAppVersion || this.appVersion),
|
||||
passcodeAutoLockOptions: this.application!.getAutolockService().getAutoLockIntervalOptions(),
|
||||
user: this.application!.getUser(),
|
||||
passcodeAutoLockOptions: this.application
|
||||
.getAutolockService()
|
||||
.getAutoLockIntervalOptions(),
|
||||
user: this.application.getUser(),
|
||||
formData: {
|
||||
mergeLocal: true,
|
||||
ephemeral: false,
|
||||
},
|
||||
mutable: {},
|
||||
showBetaWarning: false,
|
||||
errorReportingEnabled: storage.get(StorageKey.DisableErrorReporting) === false,
|
||||
errorReportingEnabled:
|
||||
storage.get(StorageKey.DisableErrorReporting) === false,
|
||||
showSessions: false,
|
||||
} as AccountMenuState;
|
||||
errorReportingId: errorReportingId(),
|
||||
keyStorageInfo: Strings.keyStorageInfo(this.application),
|
||||
importData: null,
|
||||
syncInProgress: false,
|
||||
protectionsDisabledUntil: this.getProtectionsDisabledUntil(),
|
||||
};
|
||||
}
|
||||
|
||||
getState() {
|
||||
@@ -124,17 +138,17 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
|
||||
refreshedCredentialState() {
|
||||
return {
|
||||
user: this.application!.getUser(),
|
||||
canAddPasscode: !this.application!.isEphemeralSession(),
|
||||
hasPasscode: this.application!.hasPasscode(),
|
||||
showPasscodeForm: false
|
||||
user: this.application.getUser(),
|
||||
canAddPasscode: !this.application.isEphemeralSession(),
|
||||
hasPasscode: this.application.hasPasscode(),
|
||||
showPasscodeForm: false,
|
||||
};
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
super.$onInit();
|
||||
this.setState({
|
||||
showSessions: this.appState.enableUnfinishedFeatures && await this.application.userCanManageSessions()
|
||||
showSessions: await this.application.userCanManageSessions()
|
||||
});
|
||||
|
||||
const sync = this.appState.sync;
|
||||
@@ -143,17 +157,27 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
syncInProgress: sync.inProgress,
|
||||
syncError: sync.errorMessage,
|
||||
});
|
||||
})
|
||||
});
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -163,17 +187,50 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
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);
|
||||
@@ -188,13 +245,13 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -206,7 +263,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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];
|
||||
@@ -217,7 +274,10 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
}
|
||||
|
||||
submitAuthForm() {
|
||||
if (!this.getState().formData.email || !this.getState().formData.user_password) {
|
||||
if (
|
||||
!this.getState().formData.email ||
|
||||
!this.getState().formData.user_password
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.blurAuthFields();
|
||||
@@ -232,15 +292,15 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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(
|
||||
@@ -254,7 +314,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
if (!error) {
|
||||
await this.setFormDataState({
|
||||
authenticating: false,
|
||||
user_password: undefined
|
||||
user_password: undefined,
|
||||
});
|
||||
this.close();
|
||||
return;
|
||||
@@ -262,28 +322,26 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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!,
|
||||
@@ -294,14 +352,12 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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();
|
||||
@@ -313,8 +369,8 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
this.setFormDataState({
|
||||
mergeLocal: !(await confirmDialog({
|
||||
text: STRING_ACCOUNT_MENU_UNCHECK_MERGE,
|
||||
confirmButtonStyle: 'danger'
|
||||
}))
|
||||
confirmButtonStyle: 'danger',
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -329,45 +385,20 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
this.appState.openSessionsModal();
|
||||
}
|
||||
|
||||
async openPrivilegesModal() {
|
||||
const run = () => {
|
||||
this.application!.presentPrivilegesManagementModal();
|
||||
this.close();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePrivileges
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePrivileges,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
async submitImportPassword() {
|
||||
await this.performImport(
|
||||
this.getState().importData.data,
|
||||
this.getState().importData.password
|
||||
);
|
||||
}
|
||||
|
||||
showRegister() {
|
||||
this.setFormDataState({
|
||||
showRegister: true
|
||||
showRegister: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -379,9 +410,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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);
|
||||
@@ -392,128 +421,84 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
* @template
|
||||
*/
|
||||
async importFileSelected(files: File[]) {
|
||||
const run = async () => {
|
||||
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.setState({ importData: null });
|
||||
alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
|
||||
return;
|
||||
}
|
||||
if (data.keyParams || data.auth_params) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.getState().importData,
|
||||
requestPassword: true,
|
||||
data,
|
||||
}
|
||||
});
|
||||
const element = document.getElementById(
|
||||
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
||||
);
|
||||
if (element) {
|
||||
element.scrollIntoView(false);
|
||||
}
|
||||
} else {
|
||||
await this.performImport(data, undefined);
|
||||
}
|
||||
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.performImport(data, undefined);
|
||||
await this.setState({ importData: null });
|
||||
void alertDialog({ text: STRING_UNSUPPORTED_BACKUP_FILE_VERSION });
|
||||
}
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManageBackups
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManageBackups,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
await this.performImport(data);
|
||||
}
|
||||
}
|
||||
|
||||
async performImport(data: BackupFile, password?: string) {
|
||||
async performImport(data: BackupFile) {
|
||||
await this.setState({
|
||||
importData: {
|
||||
...this.getState().importData,
|
||||
loading: true
|
||||
}
|
||||
loading: true,
|
||||
},
|
||||
});
|
||||
const result = await this.application!.importData(
|
||||
data,
|
||||
password
|
||||
);
|
||||
const result = await this.application.importData(data);
|
||||
this.setState({
|
||||
importData: null
|
||||
importData: null,
|
||||
});
|
||||
if ('error' in result) {
|
||||
this.application!.alertService!.alert(
|
||||
result.error
|
||||
);
|
||||
if (!result) {
|
||||
return;
|
||||
} else if ('error' in result) {
|
||||
void alertDialog({
|
||||
text: result.error,
|
||||
});
|
||||
} else if (result.errorCount) {
|
||||
const message = StringImportError(result.errorCount);
|
||||
this.application!.alertService!.alert(
|
||||
message
|
||||
);
|
||||
void alertDialog({
|
||||
text: StringImportError(result.errorCount),
|
||||
});
|
||||
} else {
|
||||
this.application!.alertService!.alert(
|
||||
STRING_IMPORT_SUCCESS
|
||||
);
|
||||
void alertDialog({
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
async selectAutoLockInterval(interval: number) {
|
||||
const run = async () => {
|
||||
await this.application!.getAutolockService().setAutoLockInterval(interval);
|
||||
this.reloadAutoLockInterval();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
() => {
|
||||
run();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
if (!(await this.application.authorizeAutolockIntervalChange())) {
|
||||
return;
|
||||
}
|
||||
await this.application!.getAutolockService().setAutoLockInterval(interval);
|
||||
this.reloadAutoLockInterval();
|
||||
}
|
||||
|
||||
hidePasswordForm() {
|
||||
@@ -521,7 +506,7 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
showLogin: false,
|
||||
showRegister: false,
|
||||
user_password: undefined,
|
||||
password_conf: undefined
|
||||
password_conf: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -531,91 +516,62 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
async changePasscodePressed() {
|
||||
const run = () => {
|
||||
this.getState().formData.changingPasscode = true;
|
||||
this.addPasscodeClicked();
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
this.getState().formData.changingPasscode = true;
|
||||
this.addPasscodeClicked();
|
||||
}
|
||||
|
||||
async removePasscodePressed() {
|
||||
const run = async () => {
|
||||
const signedIn = this.application!.hasAccount();
|
||||
let message = STRING_REMOVE_PASSCODE_CONFIRMATION;
|
||||
if (!signedIn) {
|
||||
message += STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM;
|
||||
}
|
||||
if (await confirmDialog({
|
||||
text: message,
|
||||
confirmButtonStyle: 'danger'
|
||||
})) {
|
||||
await preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL, async () => {
|
||||
await this.application.getAutolockService().deleteAutolockPreference();
|
||||
await this.application!.removePasscode();
|
||||
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();
|
||||
this.refreshEncryptionStatus();
|
||||
}
|
||||
}
|
||||
};
|
||||
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
||||
ProtectedAction.ManagePasscode
|
||||
);
|
||||
if (needsPrivilege) {
|
||||
this.application!.presentPrivilegesModal(
|
||||
ProtectedAction.ManagePasscode,
|
||||
run
|
||||
);
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
openErrorReportingDialog() {
|
||||
alertDialog({
|
||||
title: 'Data sent during automatic error reporting',
|
||||
text: `
|
||||
We use <a target="_blank" href="https://www.bugsnag.com/">Bugsnag</a>
|
||||
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" href="https://docs.bugsnag.com/platforms/javascript/#sending-diagnostic-data">
|
||||
<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.
|
||||
@@ -624,15 +580,15 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
|
||||
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) {
|
||||
storage.set(StorageKey.DisableErrorReporting, true);
|
||||
disableErrorReporting();
|
||||
} else {
|
||||
storage.set(StorageKey.DisableErrorReporting, false);
|
||||
enableErrorReporting();
|
||||
}
|
||||
if (!this.state.syncInProgress) {
|
||||
window.location.reload();
|
||||
@@ -654,7 +610,7 @@ export class AccountMenu extends WebDirective {
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
closeFunction: '&',
|
||||
application: '='
|
||||
application: '=',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ type ActionsMenuState = {
|
||||
}[]
|
||||
}
|
||||
|
||||
class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements ActionsMenuScope {
|
||||
class ActionsMenuCtrl extends PureViewCtrl<unknown, ActionsMenuState> implements ActionsMenuScope {
|
||||
application!: WebApplication
|
||||
item!: SNItem
|
||||
private removeHiddenExtensionsListener?: IReactionDisposer;
|
||||
@@ -63,7 +63,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
|
||||
hiddenExtensions: this.appState.actionsMenu.hiddenExtensions
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
deinit() {
|
||||
this.removeHiddenExtensionsListener?.();
|
||||
@@ -74,7 +74,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
|
||||
const extensions = this.application.actionsManager!.getExtensions().sort((a, b) => {
|
||||
return a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1;
|
||||
});
|
||||
let extensionsState: Record<UuidString, ExtensionState> = {};
|
||||
const extensionsState: Record<UuidString, ExtensionState> = {};
|
||||
extensions.map((extension) => {
|
||||
extensionsState[extension.uuid] = {
|
||||
loading: false,
|
||||
@@ -114,7 +114,7 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
|
||||
return {
|
||||
...action,
|
||||
subrows: this.subRowsForAction(action, extension)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ class ComponentViewCtrl implements ComponentViewScope {
|
||||
this.$timeout(() => {
|
||||
this.reloading = false;
|
||||
});
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
private onVisibilityChange() {
|
||||
@@ -228,6 +228,7 @@ class ComponentViewCtrl implements ComponentViewScope {
|
||||
if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') {
|
||||
desktopError = true;
|
||||
}
|
||||
// eslint-disable-next-line no-empty
|
||||
} catch (e) { }
|
||||
}
|
||||
this.$timeout.cancel(this.loadTimeout);
|
||||
|
||||
@@ -52,14 +52,14 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
editors: editors,
|
||||
defaultEditor: defaultEditor
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
selectComponent(component: SNComponent) {
|
||||
if (component) {
|
||||
if (component.conflictOf) {
|
||||
this.application.changeAndSaveItem(component.uuid, (mutator) => {
|
||||
mutator.conflictOf = undefined;
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
this.$timeout(() => {
|
||||
@@ -87,7 +87,7 @@ class EditorMenuCtrl extends PureViewCtrl implements EditorMenuScope {
|
||||
this.application.changeItem(currentDefault.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
mutator.defaultEditor = false;
|
||||
})
|
||||
});
|
||||
}
|
||||
this.application.changeAndSaveItem(component.uuid, (m) => {
|
||||
const mutator = m as ComponentMutator;
|
||||
|
||||
@@ -16,7 +16,7 @@ interface HistoryScope {
|
||||
item: SNItem
|
||||
}
|
||||
|
||||
class HistoryMenuCtrl extends PureViewCtrl<{}, HistoryState> implements HistoryScope {
|
||||
class HistoryMenuCtrl extends PureViewCtrl<unknown, HistoryState> implements HistoryScope {
|
||||
|
||||
diskEnabled = false
|
||||
autoOptimize = false
|
||||
|
||||
@@ -8,8 +8,6 @@ export { MenuRow } from './menuRow';
|
||||
export { PanelResizer } from './panelResizer';
|
||||
export { PasswordWizard } from './passwordWizard';
|
||||
export { PermissionsModal } from './permissionsModal';
|
||||
export { PrivilegesAuthModal } from './privilegesAuthModal';
|
||||
export { PrivilegesManagementModal } from './privilegesManagementModal';
|
||||
export { RevisionPreviewModal } from './revisionPreviewModal';
|
||||
export { HistoryMenu } from './historyMenu';
|
||||
export { SyncResolutionMenu } from './syncResolutionMenu';
|
||||
|
||||
@@ -6,12 +6,12 @@ import { debounce } from '@/utils';
|
||||
enum PanelSide {
|
||||
Right = 'right',
|
||||
Left = 'left'
|
||||
};
|
||||
}
|
||||
enum MouseEventType {
|
||||
Move = 'mousemove',
|
||||
Down = 'mousedown',
|
||||
Up = 'mouseup'
|
||||
};
|
||||
}
|
||||
enum CssClass {
|
||||
Hoverable = 'hoverable',
|
||||
AlwaysVisible = 'always-visible',
|
||||
@@ -19,7 +19,7 @@ enum CssClass {
|
||||
NoSelection = 'no-selection',
|
||||
Collapsed = 'collapsed',
|
||||
AnimateOpacity = 'animate-opacity',
|
||||
};
|
||||
}
|
||||
const WINDOW_EVENT_RESIZE = 'resize';
|
||||
|
||||
type ResizeFinishCallback = (
|
||||
|
||||
@@ -7,7 +7,7 @@ const DEFAULT_CONTINUE_TITLE = "Continue";
|
||||
enum Steps {
|
||||
PasswordStep = 1,
|
||||
FinishStep = 2
|
||||
};
|
||||
}
|
||||
|
||||
type FormData = {
|
||||
currentPassword?: string,
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import { ProtectedAction, PrivilegeCredential, PrivilegeSessionLength } from '@standardnotes/snjs';
|
||||
import template from '%/directives/privileges-auth-modal.pug';
|
||||
|
||||
type PrivilegesAuthModalScope = {
|
||||
application: WebApplication
|
||||
action: ProtectedAction
|
||||
onSuccess: () => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
class PrivilegesAuthModalCtrl implements PrivilegesAuthModalScope {
|
||||
$element: JQLite
|
||||
$timeout: ng.ITimeoutService
|
||||
application!: WebApplication
|
||||
action!: ProtectedAction
|
||||
onSuccess!: () => void
|
||||
onCancel!: () => void
|
||||
authParameters: Partial<Record<PrivilegeCredential, string>> = {}
|
||||
sessionLengthOptions!: { value: PrivilegeSessionLength, label: string }[]
|
||||
selectedSessionLength!: PrivilegeSessionLength
|
||||
requiredCredentials!: PrivilegeCredential[]
|
||||
failedCredentials!: PrivilegeCredential[]
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$element: JQLite,
|
||||
$timeout: ng.ITimeoutService
|
||||
) {
|
||||
this.$element = $element;
|
||||
this.$timeout = $timeout;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.sessionLengthOptions = this.application!.privilegesService!
|
||||
.getSessionLengthOptions();
|
||||
this.application.privilegesService!.getSelectedSessionLength()
|
||||
.then((length) => {
|
||||
this.$timeout(() => {
|
||||
this.selectedSessionLength = length;
|
||||
});
|
||||
});
|
||||
this.application.privilegesService!.netCredentialsForAction(this.action)
|
||||
.then((credentials) => {
|
||||
this.$timeout(() => {
|
||||
this.requiredCredentials = credentials.sort();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectSessionLength(length: PrivilegeSessionLength) {
|
||||
this.selectedSessionLength = length;
|
||||
}
|
||||
|
||||
promptForCredential(credential: PrivilegeCredential) {
|
||||
return this.application.privilegesService!.displayInfoForCredential(credential).prompt;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
isCredentialInFailureState(credential: PrivilegeCredential) {
|
||||
if (!this.failedCredentials) {
|
||||
return false;
|
||||
}
|
||||
return this.failedCredentials.find((candidate) => {
|
||||
return candidate === credential;
|
||||
}) != null;
|
||||
}
|
||||
|
||||
validate() {
|
||||
const failed = [];
|
||||
for (const cred of this.requiredCredentials) {
|
||||
const value = this.authParameters[cred];
|
||||
if (!value || value.length === 0) {
|
||||
failed.push(cred);
|
||||
}
|
||||
}
|
||||
this.failedCredentials = failed;
|
||||
return failed.length === 0;
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.validate()) {
|
||||
return;
|
||||
}
|
||||
const result = await this.application.privilegesService!.authenticateAction(
|
||||
this.action,
|
||||
this.authParameters
|
||||
);
|
||||
this.$timeout(() => {
|
||||
if (result.success) {
|
||||
this.application.privilegesService!.setSessionLength(this.selectedSessionLength);
|
||||
this.onSuccess();
|
||||
this.dismiss();
|
||||
} else {
|
||||
this.failedCredentials = result.failedCredentials;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesAuthModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesAuthModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
action: '=',
|
||||
onSuccess: '=',
|
||||
onCancel: '=',
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
import { WebDirective } from './../../types';
|
||||
import { WebApplication } from '@/ui_models/application';
|
||||
import template from '%/directives/privileges-management-modal.pug';
|
||||
import { PrivilegeCredential, ProtectedAction, SNPrivileges, PrivilegeSessionLength } from '@standardnotes/snjs';
|
||||
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
|
||||
import { PrivilegeMutator } from '@standardnotes/snjs';
|
||||
|
||||
type DisplayInfo = {
|
||||
label: string
|
||||
prompt: string
|
||||
}
|
||||
|
||||
class PrivilegesManagementModalCtrl extends PureViewCtrl {
|
||||
|
||||
hasPasscode = false
|
||||
hasAccount = false
|
||||
$element: JQLite
|
||||
application!: WebApplication
|
||||
privileges!: SNPrivileges
|
||||
availableActions!: ProtectedAction[]
|
||||
availableCredentials!: PrivilegeCredential[]
|
||||
sessionExpirey!: string
|
||||
sessionExpired = true
|
||||
credentialDisplayInfo: Partial<Record<PrivilegeCredential, DisplayInfo>> = {}
|
||||
onCancel!: () => void
|
||||
|
||||
/* @ngInject */
|
||||
constructor(
|
||||
$timeout: ng.ITimeoutService,
|
||||
$element: JQLite
|
||||
) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
}
|
||||
|
||||
async onAppLaunch() {
|
||||
super.onAppLaunch();
|
||||
this.hasPasscode = this.application.hasPasscode();
|
||||
this.hasAccount = !this.application.noAccount();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
displayInfoForCredential(credential: PrivilegeCredential) {
|
||||
const info: any = this.application.privilegesService!.displayInfoForCredential(credential);
|
||||
if (credential === PrivilegeCredential.LocalPasscode) {
|
||||
info.availability = this.hasPasscode;
|
||||
} else if (credential === PrivilegeCredential.AccountPassword) {
|
||||
info.availability = this.hasAccount;
|
||||
} else {
|
||||
info.availability = true;
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
displayInfoForAction(action: ProtectedAction) {
|
||||
return this.application.privilegesService!.displayInfoForAction(action).label;
|
||||
}
|
||||
|
||||
isCredentialRequiredForAction(action: ProtectedAction, credential: PrivilegeCredential) {
|
||||
if (!this.privileges) {
|
||||
return false;
|
||||
}
|
||||
return this.privileges.isCredentialRequiredForAction(action, credential);
|
||||
}
|
||||
|
||||
async clearSession() {
|
||||
await this.application.privilegesService!.clearSession();
|
||||
this.reloadPrivileges();
|
||||
}
|
||||
|
||||
async reloadPrivileges() {
|
||||
this.availableActions = this.application.privilegesService!.getAvailableActions();
|
||||
this.availableCredentials = this.application.privilegesService!.getAvailableCredentials();
|
||||
const sessionEndDate = await this.application.privilegesService!.getSessionExpirey();
|
||||
this.sessionExpirey = sessionEndDate.toLocaleString();
|
||||
this.sessionExpired = new Date() >= sessionEndDate;
|
||||
for (const cred of this.availableCredentials) {
|
||||
this.credentialDisplayInfo[cred] = this.displayInfoForCredential(cred);
|
||||
}
|
||||
const privs = await this.application.privilegesService!.getPrivileges();
|
||||
this.$timeout(() => {
|
||||
this.privileges = privs;
|
||||
});
|
||||
}
|
||||
|
||||
checkboxValueChanged(action: ProtectedAction, credential: PrivilegeCredential) {
|
||||
this.application.changeAndSaveItem(this.privileges.uuid, (m) => {
|
||||
const mutator = m as PrivilegeMutator;
|
||||
mutator.toggleCredentialForAction(action, credential);
|
||||
})
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.dismiss();
|
||||
this.onCancel && this.onCancel();
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
const elem = this.$element;
|
||||
const scope = elem.scope();
|
||||
scope.$destroy();
|
||||
elem.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export class PrivilegesManagementModal extends WebDirective {
|
||||
constructor() {
|
||||
super();
|
||||
this.restrict = 'E';
|
||||
this.template = template;
|
||||
this.controller = PrivilegesManagementModalCtrl;
|
||||
this.controllerAs = 'ctrl';
|
||||
this.bindToController = true;
|
||||
this.scope = {
|
||||
application: '='
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,272 +0,0 @@
|
||||
import { AppState } from '@/ui_models/app_state';
|
||||
import { PureViewCtrl } from '@/views';
|
||||
import {
|
||||
SNApplication,
|
||||
RemoteSession,
|
||||
SessionStrings,
|
||||
UuidString,
|
||||
} from '@standardnotes/snjs';
|
||||
import { autorun, IAutorunOptions, IReactionPublic } from 'mobx';
|
||||
import { render, FunctionComponent } from 'preact';
|
||||
import { useState, useEffect, useRef } from 'preact/hooks';
|
||||
import { Dialog } from '@reach/dialog';
|
||||
import { Alert } from '@reach/alert';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogDescription,
|
||||
AlertDialogLabel,
|
||||
} from '@reach/alert-dialog';
|
||||
|
||||
function useAutorun(view: (r: IReactionPublic) => any, opts?: IAutorunOptions) {
|
||||
useEffect(() => autorun(view, opts), []);
|
||||
}
|
||||
|
||||
type Session = RemoteSession & {
|
||||
revoking?: true;
|
||||
};
|
||||
|
||||
function useSessions(
|
||||
application: SNApplication
|
||||
): [
|
||||
Session[],
|
||||
() => void,
|
||||
boolean,
|
||||
(uuid: UuidString) => Promise<void>,
|
||||
string
|
||||
] {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [lastRefreshDate, setLastRefreshDate] = useState(Date.now());
|
||||
const [refreshing, setRefreshing] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
setRefreshing(true);
|
||||
const response = await application.getSessions();
|
||||
if ('error' in response) {
|
||||
if (response.error?.message) {
|
||||
setErrorMessage(response.error.message);
|
||||
} else {
|
||||
setErrorMessage('An unknown error occured while loading sessions.');
|
||||
}
|
||||
} else {
|
||||
const sessions = response as Session[];
|
||||
setSessions(sessions);
|
||||
setErrorMessage('');
|
||||
}
|
||||
setRefreshing(false);
|
||||
})();
|
||||
}, [lastRefreshDate]);
|
||||
|
||||
function refresh() {
|
||||
setLastRefreshDate(Date.now());
|
||||
}
|
||||
|
||||
async function revokeSession(uuid: UuidString) {
|
||||
const responsePromise = application.revokeSession(uuid);
|
||||
|
||||
let sessionsBeforeRevoke = sessions;
|
||||
|
||||
const sessionsDuringRevoke = sessions.slice();
|
||||
const toRemoveIndex = sessions.findIndex(
|
||||
(session) => session.uuid === uuid
|
||||
);
|
||||
sessionsDuringRevoke[toRemoveIndex] = {
|
||||
...sessionsDuringRevoke[toRemoveIndex],
|
||||
revoking: true,
|
||||
};
|
||||
setSessions(sessionsDuringRevoke);
|
||||
|
||||
const response = await responsePromise;
|
||||
if ('error' in response) {
|
||||
if (response.error?.message) {
|
||||
setErrorMessage(response.error?.message);
|
||||
} else {
|
||||
setErrorMessage('An unknown error occured while revoking the session.');
|
||||
}
|
||||
setSessions(sessionsBeforeRevoke);
|
||||
} else {
|
||||
setSessions(sessions.filter((session) => session.uuid !== uuid));
|
||||
}
|
||||
}
|
||||
|
||||
return [sessions, refresh, refreshing, revokeSession, errorMessage];
|
||||
}
|
||||
|
||||
const SessionsModal: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: SNApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const close = () => appState.closeSessionsModal();
|
||||
|
||||
const [
|
||||
sessions,
|
||||
refresh,
|
||||
refreshing,
|
||||
revokeSession,
|
||||
errorMessage,
|
||||
] = useSessions(application);
|
||||
|
||||
const [confirmRevokingSessionUuid, setRevokingSessionUuid] = useState('');
|
||||
const closeRevokeSessionAlert = () => setRevokingSessionUuid('');
|
||||
const cancelRevokeRef = useRef<HTMLButtonElement>();
|
||||
|
||||
const formatter = new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog onDismiss={close}>
|
||||
<div className="sk-modal-content sessions-modal">
|
||||
<div class="sn-component">
|
||||
<div class="sk-panel">
|
||||
<div class="sk-panel-header">
|
||||
<div class="sk-panel-header-title">Active Sessions</div>
|
||||
<div className="buttons">
|
||||
<button
|
||||
class="sk-a close-button info"
|
||||
disabled={refreshing}
|
||||
onClick={refresh}
|
||||
>
|
||||
Refresh
|
||||
</button>
|
||||
<button class="sk-a close-button info" onClick={close}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sk-panel-content">
|
||||
{refreshing ? (
|
||||
<>
|
||||
<div class="sk-spinner small info"></div>
|
||||
<h2 className="sk-p sessions-modal-refreshing">
|
||||
Loading sessions
|
||||
</h2>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{errorMessage && (
|
||||
<Alert className="sk-p bold">{errorMessage}</Alert>
|
||||
)}
|
||||
{sessions.length > 0 && (
|
||||
<ul>
|
||||
{sessions.map((session) => (
|
||||
<li>
|
||||
<h2>{session.device_info}</h2>
|
||||
{session.current ? (
|
||||
<span className="info bold">Current session</span>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
Signed in on{' '}
|
||||
{formatter.format(session.updated_at)}
|
||||
</p>
|
||||
<button
|
||||
className="sk-button danger sk-label"
|
||||
disabled={session.revoking}
|
||||
onClick={() =>
|
||||
setRevokingSessionUuid(session.uuid)
|
||||
}
|
||||
>
|
||||
<span>Revoke</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
{confirmRevokingSessionUuid && (
|
||||
<AlertDialog leastDestructiveRef={cancelRevokeRef}>
|
||||
<div className="sk-modal-content">
|
||||
<div className="sn-component">
|
||||
<div className="sk-panel">
|
||||
<div className="sk-panel-content">
|
||||
<div className="sk-panel-section">
|
||||
<AlertDialogLabel className="sk-h3 sk-panel-section-title">
|
||||
{SessionStrings.RevokeTitle}
|
||||
</AlertDialogLabel>
|
||||
<AlertDialogDescription className="sk-panel-row">
|
||||
<p>{SessionStrings.RevokeText}</p>
|
||||
</AlertDialogDescription>
|
||||
<div className="sk-panel-row">
|
||||
<div className="sk-button-group">
|
||||
<button
|
||||
className="sk-button neutral sk-label"
|
||||
ref={cancelRevokeRef}
|
||||
onClick={closeRevokeSessionAlert}
|
||||
>
|
||||
<span>{SessionStrings.RevokeCancelButton}</span>
|
||||
</button>
|
||||
<button
|
||||
className="sk-button danger sk-label"
|
||||
onClick={() => {
|
||||
closeRevokeSessionAlert();
|
||||
revokeSession(confirmRevokingSessionUuid);
|
||||
}}
|
||||
>
|
||||
<span>{SessionStrings.RevokeConfirmButton}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AlertDialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const Sessions: FunctionComponent<{
|
||||
appState: AppState;
|
||||
application: SNApplication;
|
||||
}> = ({ appState, application }) => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
useAutorun(() => setShowModal(appState.isSessionsModalVisible));
|
||||
|
||||
if (showModal) {
|
||||
return <SessionsModal application={application} appState={appState} />;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
class SessionsModalCtrl extends PureViewCtrl<{}, {}> {
|
||||
/* @ngInject */
|
||||
constructor(private $element: JQLite, $timeout: ng.ITimeoutService) {
|
||||
super($timeout);
|
||||
this.$element = $element;
|
||||
}
|
||||
$onChanges() {
|
||||
render(
|
||||
<Sessions appState={this.appState} application={this.application} />,
|
||||
this.$element[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function SessionsModalDirective() {
|
||||
return {
|
||||
controller: SessionsModalCtrl,
|
||||
bindToController: true,
|
||||
scope: {
|
||||
application: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user