604 lines
15 KiB
TypeScript
604 lines
15 KiB
TypeScript
import { WebDirective } from './../../types';
|
|
import { isDesktopApplication, isNullOrUndefined } from '@/utils';
|
|
import template from '%/directives/account-menu.pug';
|
|
import { ProtectedAction, ContentType } from 'snjs';
|
|
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_REMOVE_PASSCODE_CONFIRMATION,
|
|
STRING_REMOVE_PASSCODE_OFFLINE_ADDENDUM,
|
|
STRING_NON_MATCHING_PASSCODES,
|
|
STRING_NON_MATCHING_PASSWORDS,
|
|
STRING_INVALID_IMPORT_FILE,
|
|
STRING_GENERATING_LOGIN_KEYS,
|
|
STRING_GENERATING_REGISTER_KEYS,
|
|
StringImportError
|
|
} from '@/strings';
|
|
import { SyncOpStatus } from '@node_modules/snjs/dist/@types/services/sync/sync_op_status';
|
|
import { PasswordWizardType } from '@/types';
|
|
import { BackupFile } from '@node_modules/snjs/dist/@types/services/protocol_service';
|
|
|
|
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
|
|
|
|
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<FormData>
|
|
appVersion: string
|
|
passcodeAutoLockOptions: any
|
|
user: any
|
|
mutable: any
|
|
importData: any
|
|
}
|
|
|
|
class AccountMenuCtrl extends PureViewCtrl {
|
|
|
|
public appVersion: string
|
|
private syncStatus?: SyncOpStatus
|
|
private closeFunction?: () => 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!.getLockService().getAutoLockIntervalOptions(),
|
|
user: this.application!.getUser(),
|
|
formData: {
|
|
mergeLocal: true,
|
|
ephemeral: false
|
|
},
|
|
mutable: {}
|
|
} as AccountMenuState;
|
|
}
|
|
|
|
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.loadBackupsAvailability();
|
|
}
|
|
|
|
refreshedCredentialState() {
|
|
return {
|
|
user: this.application!.getUser(),
|
|
canAddPasscode: !this.application!.isEphemeralSession(),
|
|
hasPasscode: this.application!.hasPasscode(),
|
|
showPasscodeForm: false
|
|
};
|
|
}
|
|
|
|
$onInit() {
|
|
super.$onInit();
|
|
this.initProps({
|
|
closeFunction: this.closeFunction
|
|
});
|
|
this.syncStatus = this.application!.getSyncStatus();
|
|
}
|
|
|
|
close() {
|
|
this.$timeout(() => {
|
|
this.props.closeFunction();
|
|
});
|
|
}
|
|
|
|
async loadHost() {
|
|
const host = await this.application!.getHost();
|
|
this.setState({
|
|
server: host,
|
|
formData: {
|
|
...this.getState().formData,
|
|
url: host
|
|
}
|
|
});
|
|
}
|
|
|
|
onHostInputChange() {
|
|
const url = this.getState().formData.url!;
|
|
this.application!.setHost(url);
|
|
}
|
|
|
|
async loadBackupsAvailability() {
|
|
const hasUser = !isNullOrUndefined(this.application!.getUser());
|
|
const hasPasscode = this.application!.hasPasscode();
|
|
const encryptedAvailable = hasUser || hasPasscode;
|
|
|
|
function encryptionStatusString() {
|
|
if (hasUser) {
|
|
return STRING_E2E_ENABLED;
|
|
} else if (hasPasscode) {
|
|
return STRING_LOCAL_ENC_ENABLED;
|
|
} else {
|
|
return STRING_ENC_NOT_ENABLED;
|
|
}
|
|
}
|
|
|
|
this.setState({
|
|
encryptionStatusString: encryptionStatusString(),
|
|
encryptionEnabled: encryptedAvailable,
|
|
mutable: {
|
|
...this.getState().mutable,
|
|
backupEncrypted: encryptedAvailable
|
|
}
|
|
});
|
|
}
|
|
|
|
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.mfa && formData.mfa.payload.mfa_key,
|
|
formData.userMfaCode,
|
|
formData.mergeLocal
|
|
);
|
|
const hasError = !response || response.error;
|
|
if (!hasError) {
|
|
await this.setFormDataState({
|
|
authenticating: false,
|
|
user_password: undefined
|
|
});
|
|
this.close();
|
|
return;
|
|
}
|
|
const error = response
|
|
? response.error
|
|
: { message: "An unknown error occured." };
|
|
if (error.tag === 'mfa-required' || error.tag === 'mfa-invalid') {
|
|
await this.setFormDataState({
|
|
showLogin: false,
|
|
mfa: error,
|
|
status: undefined
|
|
});
|
|
} else {
|
|
await this.setFormDataState({
|
|
showLogin: true,
|
|
mfa: undefined,
|
|
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
|
|
);
|
|
if (!response || response.error) {
|
|
await this.setFormDataState({
|
|
status: undefined
|
|
});
|
|
const error = response
|
|
? response.error
|
|
: { message: "An unknown error occured." };
|
|
await this.setFormDataState({
|
|
authenticating: false
|
|
});
|
|
this.application!.alertService!.alert(
|
|
error.message
|
|
);
|
|
} else {
|
|
await this.setFormDataState({ authenticating: false });
|
|
this.close();
|
|
}
|
|
}
|
|
|
|
mergeLocalChanged() {
|
|
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(PasswordWizardType.ChangePassword);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
destroyLocalData() {
|
|
this.application!.alertService!.confirm(
|
|
STRING_SIGN_OUT_CONFIRMATION,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
async () => {
|
|
await this.application!.signOut();
|
|
},
|
|
undefined,
|
|
true,
|
|
);
|
|
}
|
|
|
|
async submitImportPassword() {
|
|
await this.performImport(
|
|
this.getState().importData.data,
|
|
this.getState().importData.password
|
|
);
|
|
}
|
|
|
|
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 run = async () => {
|
|
const file = files[0];
|
|
const data = await this.readFile(file);
|
|
if (!data) {
|
|
return;
|
|
}
|
|
if (data.auth_params) {
|
|
await this.setState({
|
|
importData: {
|
|
...this.getState().importData,
|
|
requestPassword: true,
|
|
data: data
|
|
}
|
|
});
|
|
const element = document.getElementById(
|
|
ELEMENT_ID_IMPORT_PASSWORD_INPUT
|
|
);
|
|
if (element) {
|
|
element.scrollIntoView(false);
|
|
}
|
|
} else {
|
|
await this.performImport(data, undefined);
|
|
}
|
|
};
|
|
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
|
ProtectedAction.ManageBackups
|
|
);
|
|
if (needsPrivilege) {
|
|
this.application!.presentPrivilegesModal(
|
|
ProtectedAction.ManageBackups,
|
|
run
|
|
);
|
|
} else {
|
|
run();
|
|
}
|
|
}
|
|
|
|
async performImport(data: any, password?: string) {
|
|
await this.setState({
|
|
importData: {
|
|
...this.getState().importData,
|
|
loading: true
|
|
}
|
|
});
|
|
const errorCount = await this.importJSONData(data, password);
|
|
this.setState({
|
|
importData: null
|
|
});
|
|
if (errorCount > 0) {
|
|
const message = StringImportError(errorCount);
|
|
this.application!.alertService!.alert(
|
|
message
|
|
);
|
|
} else {
|
|
this.application!.alertService!.alert(
|
|
STRING_IMPORT_SUCCESS
|
|
);
|
|
}
|
|
}
|
|
|
|
async importJSONData(data: BackupFile, password?: string) {
|
|
const { errorCount } = await this.application!.importData(
|
|
data,
|
|
password
|
|
);
|
|
return errorCount;
|
|
}
|
|
|
|
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!.getLockService().getAutoLockInterval();
|
|
this.setState({
|
|
selectedAutoLockInterval: interval
|
|
});
|
|
}
|
|
|
|
async selectAutoLockInterval(interval: number) {
|
|
const run = async () => {
|
|
await this.application!.getLockService().setAutoLockInterval(interval);
|
|
this.reloadAutoLockInterval();
|
|
};
|
|
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
|
ProtectedAction.ManagePasscode
|
|
);
|
|
if (needsPrivilege) {
|
|
this.application!.presentPrivilegesModal(
|
|
ProtectedAction.ManagePasscode,
|
|
() => {
|
|
run();
|
|
}
|
|
);
|
|
} else {
|
|
run();
|
|
}
|
|
}
|
|
|
|
hidePasswordForm() {
|
|
this.setFormDataState({
|
|
showLogin: false,
|
|
showRegister: false,
|
|
user_password: undefined,
|
|
password_conf: undefined
|
|
});
|
|
}
|
|
|
|
hasPasscode() {
|
|
return this.application!.hasPasscode();
|
|
}
|
|
|
|
addPasscodeClicked() {
|
|
this.setFormDataState({
|
|
showPasscodeForm: true
|
|
});
|
|
}
|
|
|
|
submitPasscodeForm() {
|
|
const passcode = this.getState().formData.passcode!;
|
|
if (passcode !== this.getState().formData.confirmPasscode!) {
|
|
this.application!.alertService!.alert(
|
|
STRING_NON_MATCHING_PASSCODES
|
|
);
|
|
return;
|
|
}
|
|
(this.getState().formData.changingPasscode
|
|
? this.application!.changePasscode(passcode)
|
|
: this.application!.setPasscode(passcode)
|
|
).then(() => {
|
|
this.setFormDataState({
|
|
passcode: undefined,
|
|
confirmPasscode: undefined,
|
|
showPasscodeForm: false
|
|
});
|
|
});
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
async removePasscodePressed() {
|
|
const run = async () => {
|
|
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(
|
|
message,
|
|
undefined,
|
|
undefined,
|
|
undefined,
|
|
() => {
|
|
this.application!.removePasscode();
|
|
},
|
|
undefined,
|
|
true,
|
|
);
|
|
};
|
|
const needsPrivilege = await this.application!.privilegesService!.actionRequiresPrivilege(
|
|
ProtectedAction.ManagePasscode
|
|
);
|
|
if (needsPrivilege) {
|
|
this.application!.presentPrivilegesModal(
|
|
ProtectedAction.ManagePasscode,
|
|
run
|
|
);
|
|
} else {
|
|
run();
|
|
}
|
|
}
|
|
|
|
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: '='
|
|
};
|
|
}
|
|
}
|