Merge pull request #474 from standardnotes/custom-challenges

feat: custom challenges
This commit is contained in:
Mo Bitar
2020-09-24 07:20:21 -05:00
committed by GitHub
24 changed files with 210 additions and 296 deletions

View File

@@ -1,5 +1,5 @@
import { WebDirective } from './../../types'; import { WebDirective } from './../../types';
import { isDesktopApplication, isNullOrUndefined, preventRefreshing } from '@/utils'; import { isDesktopApplication, preventRefreshing } from '@/utils';
import template from '%/directives/account-menu.pug'; import template from '%/directives/account-menu.pug';
import { ProtectedAction, ContentType } from 'snjs'; import { ProtectedAction, ContentType } from 'snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
@@ -26,7 +26,6 @@ import { SyncOpStatus } from 'snjs/dist/@types/services/sync/sync_op_status';
import { PasswordWizardType } from '@/types'; import { PasswordWizardType } from '@/types';
import { BackupFile } from 'snjs/dist/@types/services/protocol_service'; import { BackupFile } from 'snjs/dist/@types/services/protocol_service';
import { confirmDialog, alertDialog } from '@/services/alertService'; import { confirmDialog, alertDialog } from '@/services/alertService';
import { HttpResponse } from 'snjs/dist/@types/services/api/http_service';
const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request'; const ELEMENT_ID_IMPORT_PASSWORD_INPUT = 'import-password-request';
@@ -44,8 +43,6 @@ type FormData = {
showPasscodeForm: boolean showPasscodeForm: boolean
strictSignin?: boolean strictSignin?: boolean
ephemeral: boolean ephemeral: boolean
mfa: HttpResponse
userMfaCode?: string
mergeLocal?: boolean mergeLocal?: boolean
url: string url: string
authenticating: boolean authenticating: boolean
@@ -220,12 +217,10 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
formData.user_password!, formData.user_password!,
formData.strictSignin, formData.strictSignin,
formData.ephemeral, formData.ephemeral,
formData.mfa && formData.mfa.payload.mfa_key,
formData.userMfaCode,
formData.mergeLocal formData.mergeLocal
); );
const hasError = !response || response.error; const error = response.error;
if (!hasError) { if (!error) {
await this.setFormDataState({ await this.setFormDataState({
authenticating: false, authenticating: false,
user_password: undefined user_password: undefined
@@ -233,29 +228,13 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.close(); this.close();
return; return;
} }
const error = response await this.setFormDataState({
? response.error! showLogin: true,
: { status: undefined,
message: "An unknown error occured.", user_password: undefined
tag: undefined, });
status: 500 if (error.message) {
} as HttpResponse; this.application!.alertService!.alert(error.message);
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({ await this.setFormDataState({
authenticating: false authenticating: false
@@ -281,17 +260,11 @@ class AccountMenuCtrl extends PureViewCtrl<{}, AccountMenuState> {
this.getState().formData.ephemeral, this.getState().formData.ephemeral,
this.getState().formData.mergeLocal this.getState().formData.mergeLocal
); );
if (!response || response.error) { const error = response.error;
if (error) {
await this.setFormDataState({ await this.setFormDataState({
status: undefined status: undefined
}); });
const error = response
? response.error!
: {
message: "An unknown error occured.",
tag: undefined,
status: 500
} as HttpResponse;
await this.setFormDataState({ await this.setFormDataState({
authenticating: false authenticating: false
}); });

View File

@@ -3,7 +3,7 @@ import { WebDirective } from './../../types';
import template from '%/directives/actions-menu.pug'; import template from '%/directives/actions-menu.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { SNItem, Action, SNActionsExtension, UuidString } from 'snjs/dist/@types'; import { SNItem, Action, SNActionsExtension, UuidString } from 'snjs/dist/@types';
import { ActionResponse } from 'snjs/dist/@types/services/actions_service'; import { ActionResponse } from 'snjs';
import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension'; import { ActionsExtensionMutator } from 'snjs/dist/@types/models/app/extension';
type ActionsMenuScope = { type ActionsMenuScope = {
@@ -144,8 +144,8 @@ class ActionsMenuCtrl extends PureViewCtrl<{}, ActionsMenuState> implements Acti
} }
private async updateAction( private async updateAction(
action: Action, action: Action,
extension: SNActionsExtension, extension: SNActionsExtension,
params: UpdateActionParams params: UpdateActionParams
) { ) {
const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => { const updatedExtension = await this.application.changeItem(extension.uuid, (mutator) => {

View File

@@ -4,12 +4,35 @@ import template from '%/directives/password-wizard.pug';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
const DEFAULT_CONTINUE_TITLE = "Continue"; const DEFAULT_CONTINUE_TITLE = "Continue";
const Steps = { enum Steps {
PasswordStep: 1, PasswordStep = 1,
FinishStep: 2 FinishStep = 2
}; };
class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope { type FormData = {
currentPassword?: string,
newPassword?: string,
newPasswordConfirmation?: string,
status?: string
}
type State = {
lockContinue: boolean
formData: FormData,
continueTitle: string,
step: Steps,
title: string,
showSpinner: boolean
processing: boolean
}
type Props = {
type: PasswordWizardType,
changePassword: boolean,
securityUpdate: boolean
}
class PasswordWizardCtrl extends PureViewCtrl<Props, State> implements PasswordWizardScope {
$element: JQLite $element: JQLite
application!: WebApplication application!: WebApplication
type!: PasswordWizardType type!: PasswordWizardType
@@ -70,7 +93,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
} }
this.isContinuing = true; this.isContinuing = true;
this.setState({ await this.setState({
showSpinner: true, showSpinner: true,
continueTitle: "Generating Keys..." continueTitle: "Generating Keys..."
}); });
@@ -92,7 +115,7 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
}); });
} }
async setFormDataState(formData: any) { async setFormDataState(formData: Partial<FormData>) {
return this.setState({ return this.setState({
formData: { formData: {
...this.state.formData, ...this.state.formData,
@@ -121,7 +144,9 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
this.application.alertService!.alert( this.application.alertService!.alert(
"Your new password does not match its confirmation." "Your new password does not match its confirmation."
); );
this.state.formData.status = null; this.setFormDataState({
status: undefined
});
return false; return false;
} }
} }
@@ -129,13 +154,15 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
this.application.alertService!.alert( this.application.alertService!.alert(
"We don't have your email stored. Please log out then log back in to fix this issue." "We don't have your email stored. Please log out then log back in to fix this issue."
); );
this.state.formData.status = null; this.setFormDataState({
status: undefined
});
return false; return false;
} }
/** Validate current password */ /** Validate current password */
const success = await this.application.validateAccountPassword( const success = await this.application.validateAccountPassword(
this.state.formData.currentPassword this.state.formData.currentPassword!
); );
if (!success) { if (!success) {
this.application.alertService!.alert( this.application.alertService!.alert(
@@ -146,37 +173,31 @@ class PasswordWizardCtrl extends PureViewCtrl implements PasswordWizardScope {
} }
async processPasswordChange() { async processPasswordChange() {
this.setState({ await this.setState({
lockContinue: true, lockContinue: true,
processing: true processing: true
}); });
this.setFormDataState({ await this.setFormDataState({
status: "Processing encryption keys..." status: "Processing encryption keys..."
}); });
const newPassword = this.props.securityUpdate const newPassword = this.props.securityUpdate
? this.state.formData.currentPassword ? this.state.formData.currentPassword
: this.state.formData.newPassword; : this.state.formData.newPassword;
const response = await this.application.changePassword( const response = await this.application.changePassword(
this.state.formData.currentPassword, this.state.formData.currentPassword!,
newPassword newPassword!
); );
const success = !response || !response.error; const success = !response.error;
this.setFormDataState({ await this.setState({
statusError: !success, processing: false,
processing: success lockContinue: false,
}); });
if (!success) { if (!success) {
this.application.alertService!.alert(
response?.error?.message
? response.error.message
: "There was an error changing your password. Please try again."
);
this.setFormDataState({ this.setFormDataState({
status: "Unable to process your password. Please try again." status: "Unable to process your password. Please try again."
}); });
} else { } else {
this.setState({ this.setState({
lockContinue: false,
formData: { formData: {
...this.state.formData, ...this.state.formData,
status: this.props.changePassword status: this.props.changePassword

View File

@@ -1,5 +1,5 @@
/* eslint-disable prefer-promise-reject-errors */ /* eslint-disable prefer-promise-reject-errors */
import { SNAlertService, ButtonType, DismissBlockingDialog } from 'snjs'; import { SNAlertService, ButtonType } from 'snjs';
import { SKAlert } from 'sn-stylekit'; import { SKAlert } from 'sn-stylekit';
/** @returns a promise resolving to true if the user confirmed, false if they canceled */ /** @returns a promise resolving to true if the user confirmed, false if they canceled */

View File

@@ -1,4 +1,4 @@
import { ApplicationGroup } from './../ui_models/application_group'; import { ApplicationService } from 'snjs';
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import { isDesktopApplication } from '@/utils'; import { isDesktopApplication } from '@/utils';
import { AppStateEvent } from '@/ui_models/app_state'; import { AppStateEvent } from '@/ui_models/app_state';
@@ -13,26 +13,21 @@ const LOCK_INTERVAL_ONE_HOUR = 3600 * MILLISECONDS_PER_SECOND;
const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey"; const STORAGE_KEY_AUTOLOCK_INTERVAL = "AutoLockIntervalKey";
export class AutolockService { export class AutolockService extends ApplicationService {
private application: WebApplication
private unsubState: any private unsubState: any
private pollFocusInterval: any private pollFocusInterval: any
private lastFocusState?: 'hidden' | 'visible' private lastFocusState?: 'hidden' | 'visible'
private lockAfterDate?: Date private lockAfterDate?: Date
private lockTimeout?: any private lockTimeout?: any
constructor( onAppLaunch() {
application: WebApplication this.observeVisibility();
) { return super.onAppLaunch();
this.application = application;
setTimeout(() => {
this.observeVisibility();
}, 0);
} }
observeVisibility() { observeVisibility() {
this.unsubState = this.application.getAppState().addObserver( this.unsubState = (this.application as WebApplication).getAppState().addObserver(
async (eventName) => { async (eventName) => {
if (eventName === AppStateEvent.WindowDidBlur) { if (eventName === AppStateEvent.WindowDidBlur) {
this.documentVisibilityChanged(false); this.documentVisibilityChanged(false);

View File

@@ -1,5 +1,4 @@
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import _ from 'lodash';
import { import {
StorageValueModes, StorageValueModes,
EncryptionIntent, EncryptionIntent,

View File

@@ -1,5 +1,5 @@
/** @generic */ /** @generic */
export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."; export const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.";
export const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe."; export const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.";
export const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in."; export const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in.";
export function StringSyncException(data: any) { export function StringSyncException(data: any) {
@@ -46,12 +46,6 @@ export const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a p
export function StringImportError(errorCount: number) { export function StringImportError(errorCount: number) {
return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`; return `Import complete. ${errorCount} items were not imported because there was an error decrypting them. Make sure the password is correct and try again.`;
} }
export const STRING_ENTER_ACCOUNT_PASSCODE = 'Enter your application passcode to unlock the application';
export const STRING_ENTER_ACCOUNT_PASSWORD = 'Enter your account password';
export const STRING_ENTER_PASSCODE_FOR_MIGRATION = 'Your application passcode is required to perform an upgrade of your local data storage structure.';
export const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = 'Enter your application passcode before signing in or registering';
export const STRING_STORAGE_UPDATE = 'Storage Update';
export const STRING_AUTHENTICATION_REQUIRED = 'Authentication Required';
export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.'; export const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = 'This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.';
/** @password_change */ /** @password_change */
@@ -71,8 +65,8 @@ export const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL =
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available'; export const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = 'Encryption upgrade available';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT = export const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT =
'Encryption version 004 is available for your account and local data storage. ' + 'Encryption version 004 is available. ' +
'This version strengthens the encryption algorithms for your account and ' + 'This version strengthens the encryption algorithms your account and ' +
'disk use. To learn more about this upgrade, visit our ' + 'local storage use. To learn more about this upgrade, visit our ' +
'<a href="https://standardnotes.org/help/security" target="_blank">Security Upgrade page.</a>'; '<a href="https://standardnotes.org/help/security" target="_blank">Security Upgrade page.</a>';
export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade'; export const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = 'Upgrade';

View File

@@ -4,9 +4,7 @@ import { EditorGroup } from '@/ui_models/editor_group';
import { InputModalScope } from '@/directives/views/inputModal'; import { InputModalScope } from '@/directives/views/inputModal';
import { PasswordWizardType, PasswordWizardScope } from '@/types'; import { PasswordWizardType, PasswordWizardScope } from '@/types';
import { import {
Environment,
SNApplication, SNApplication,
SNAlertService,
platformFromString, platformFromString,
Challenge, Challenge,
ProtectedAction ProtectedAction
@@ -167,21 +165,6 @@ export class WebApplication extends SNApplication {
angular.element(document.body).append(el); angular.element(document.body).append(el);
} }
async performProtocolUpgrade() {
const result = await this.upgradeProtocolVersion();
if (result.success) {
this.alertService!.alert(
"Success! Your encryption version has been upgraded." +
" You'll be asked to enter your credentials again on other devices you're signed into."
);
} else if (result.error) {
console.error(result.error);
this.alertService!.alert(
"Unable to upgrade encryption version. Please try again."
);
}
}
async presentPrivilegesModal( async presentPrivilegesModal(
action: ProtectedAction, action: ProtectedAction,
onSuccess?: any, onSuccess?: any,

View File

@@ -12,6 +12,11 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
private unsubApp: any private unsubApp: any
private unsubState: any private unsubState: any
private stateTimeout?: ng.IPromise<void> private stateTimeout?: ng.IPromise<void>
/**
* Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that
* no Angular handlebars/syntax render in the UI before display data is ready.
*/
protected templateReady = false
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -28,6 +33,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
} }
this.addAppEventObserver(); this.addAppEventObserver();
this.addAppStateObserver(); this.addAppStateObserver();
this.templateReady = true;
} }
deinit() { deinit() {
@@ -114,7 +120,7 @@ export class PureViewCtrl<P = CtrlProps, S = CtrlState> {
await this.onAppLaunch(); await this.onAppLaunch();
} else if (eventName === ApplicationEvent.CompletedIncrementalSync) { } else if (eventName === ApplicationEvent.CompletedIncrementalSync) {
this.onAppIncrementalSync(); this.onAppIncrementalSync();
} else if (eventName === ApplicationEvent.CompletedFullSync) { } else if (eventName === ApplicationEvent.CompletedFullSync) {
this.onAppFullSync(); this.onAppFullSync();
} else if (eventName === ApplicationEvent.KeyStatusChanged) { } else if (eventName === ApplicationEvent.KeyStatusChanged) {
this.onAppKeyChange(); this.onAppKeyChange();

View File

@@ -10,12 +10,10 @@ import {
PANEL_NAME_TAGS PANEL_NAME_TAGS
} from '@/views/constants'; } from '@/views/constants';
import { import {
STRING_SESSION_EXPIRED,
STRING_DEFAULT_FILE_ERROR STRING_DEFAULT_FILE_ERROR
} from '@/strings'; } from '@/strings';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { PermissionDialog } from 'snjs/dist/@types/services/component_manager'; import { PermissionDialog } from 'snjs/dist/@types/services/component_manager';
import { alertDialog } from '@/services/alertService';
class ApplicationViewCtrl extends PureViewCtrl { class ApplicationViewCtrl extends PureViewCtrl {
private $compile?: ng.ICompileService private $compile?: ng.ICompileService
@@ -29,7 +27,6 @@ class ApplicationViewCtrl extends PureViewCtrl {
private showingDownloadStatus = false private showingDownloadStatus = false
private uploadSyncStatus: any private uploadSyncStatus: any
private lastAlertShownTimeStamp = 0; private lastAlertShownTimeStamp = 0;
private showingInvalidSessionAlert = false;
/* @ngInject */ /* @ngInject */
constructor( constructor(
@@ -126,8 +123,6 @@ class ApplicationViewCtrl extends PureViewCtrl {
this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus); this.syncStatus = this.application!.getStatusService().removeStatus(this.syncStatus);
this.completedInitialSync = true; this.completedInitialSync = true;
} }
} else if (eventName === ApplicationEvent.InvalidSyncSession) {
this.showInvalidSessionAlert();
} else if (eventName === ApplicationEvent.LocalDatabaseReadError) { } else if (eventName === ApplicationEvent.LocalDatabaseReadError) {
this.application!.alertService!.alert( this.application!.alertService!.alert(
'Unable to load local database. Please restart the app and try again.' 'Unable to load local database. Please restart the app and try again.'
@@ -258,24 +253,6 @@ class ApplicationViewCtrl extends PureViewCtrl {
this.application!.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog; this.application!.componentManager!.presentPermissionsDialog = this.presentPermissionsDialog;
} }
showInvalidSessionAlert() {
/** Don't show repeatedly; at most 30 seconds in between */
const SHOW_INTERVAL = 30 * 1000;
if (
!this.showingInvalidSessionAlert &&
(Date.now() - this.lastAlertShownTimeStamp) > SHOW_INTERVAL
) {
this.lastAlertShownTimeStamp = Date.now();
this.showingInvalidSessionAlert = true;
setTimeout(async () => {
await alertDialog({
text: STRING_SESSION_EXPIRED
});
this.showingInvalidSessionAlert = false;
}, 500);
}
}
addDragDropHandlers() { addDragDropHandlers() {
/** /**
* Disable dragging and dropping of files (but allow text) into main SN interface. * Disable dragging and dropping of files (but allow text) into main SN interface.

View File

@@ -1,26 +1,30 @@
.sk-modal-background(ng-click="ctrl.cancel()") .sk-modal-background(ng-click="ctrl.cancel()")
.challenge-modal.sk-modal-content .challenge-modal.sk-modal-content(ng-if='ctrl.templateReady')
.sn-component .sn-component
.sk-panel .sk-panel
.sk-panel-header .sk-panel-header
.sk-panel-header-title {{ctrl.title}} .sk-panel-header-title {{ctrl.challenge.modalTitle}}
.sk-panel-content .sk-panel-content
.sk-panel-section .sk-panel-section
div(ng-repeat="type in ctrl.state.types") .sk-p.sk-panel-row.centered.prompt
.sk-p.sk-panel-row.centered.prompt strong {{ctrl.challenge.heading}}
strong {{ctrl.promptForChallenge(type)}} .sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.challenge.subheading')
| {{ctrl.challenge.subheading}}
.sk-panel-section
div(ng-repeat="prompt in ctrl.state.prompts track by prompt.id")
.sk-panel-row .sk-panel-row
input.sk-input.contrast( input.sk-input.contrast(
ng-model="ctrl.state.values[type].value" ng-model="ctrl.state.values[prompt.id].value"
should-focus="$index == 0" should-focus="$index == 0"
sn-autofocus="true" sn-autofocus="true"
sn-enter="ctrl.submit()" , sn-enter="ctrl.submit()" ,
ng-change="ctrl.onTextValueChange(type)" ng-change="ctrl.onTextValueChange(prompt)"
type="password" ng-attr-type="{{prompt.secureTextEntry ? 'password' : 'text'}}",
ng-attr-placeholder="{{prompt.placeholder}}"
) )
.sk-panel-row.centered .sk-panel-row.centered
label.sk-label.danger( label.sk-label.danger(
ng-if="ctrl.state.values[type].invalid" ng-if="ctrl.state.values[prompt.id].invalid"
) Invalid authentication. Please try again. ) Invalid authentication. Please try again.
.sk-panel-footer.extra-padding .sk-panel-footer.extra-padding
.sk-button.info.big.block.bold( .sk-button.info.big.block.bold(

View File

@@ -1,43 +1,39 @@
import { WebApplication } from '@/ui_models/application'; import { WebApplication } from '@/ui_models/application';
import template from './challenge-modal.pug'; import template from './challenge-modal.pug';
import { import {
ChallengeType,
ChallengeValue, ChallengeValue,
removeFromArray, removeFromArray,
Challenge, Challenge,
ChallengeReason, ChallengeReason,
ChallengePrompt
} from 'snjs'; } from 'snjs';
import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl'; import { PureViewCtrl } from '@Views/abstract/pure_view_ctrl';
import { WebDirective } from '@/types'; import { WebDirective } from '@/types';
import { confirmDialog } from '@/services/alertService'; import { confirmDialog } from '@/services/alertService';
import { import {
STRING_SIGN_OUT_CONFIRMATION, STRING_SIGN_OUT_CONFIRMATION,
STRING_ENTER_ACCOUNT_PASSCODE,
STRING_ENTER_ACCOUNT_PASSWORD,
STRING_ENTER_PASSCODE_FOR_MIGRATION,
STRING_STORAGE_UPDATE,
STRING_AUTHENTICATION_REQUIRED,
STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER,
} from '@/strings'; } from '@/strings';
type InputValue = { type InputValue = {
prompt: ChallengePrompt
value: string value: string
invalid: boolean invalid: boolean
} }
type Values = Record<ChallengeType, InputValue> type Values = Record<number, InputValue>
type ChallengeModalState = { type ChallengeModalState = {
types: ChallengeType[] prompts: ChallengePrompt[]
values: Partial<Values> values: Partial<Values>
processing: boolean, processing: boolean,
forgotPasscode: boolean, forgotPasscode: boolean,
showForgotPasscodeLink: boolean, showForgotPasscodeLink: boolean,
processingPrompts: ChallengePrompt[],
hasAccount: boolean,
} }
class ChallengeModalCtrl extends PureViewCtrl { class ChallengeModalCtrl extends PureViewCtrl<{}, ChallengeModalState> {
private $element: JQLite private $element: JQLite
private processingTypes: ChallengeType[] = []
application!: WebApplication application!: WebApplication
challenge!: Challenge challenge!: Challenge
private cancelable = false private cancelable = false
@@ -58,14 +54,15 @@ class ChallengeModalCtrl extends PureViewCtrl {
$onInit() { $onInit() {
super.$onInit(); super.$onInit();
const values = {} as Values; const values = {} as Values;
const types = this.challenge.types; const prompts = this.challenge.prompts;
for (const type of types) { for (const prompt of prompts) {
values[type] = { values[prompt.id] = {
prompt,
value: '', value: '',
invalid: false invalid: false
}; };
} }
let showForgotPasscodeLink: boolean; let showForgotPasscodeLink = false;
switch (this.challenge.reason) { switch (this.challenge.reason) {
case ChallengeReason.ApplicationUnlock: case ChallengeReason.ApplicationUnlock:
showForgotPasscodeLink = true; showForgotPasscodeLink = true;
@@ -86,29 +83,40 @@ class ChallengeModalCtrl extends PureViewCtrl {
} }
this.cancelable = !showForgotPasscodeLink this.cancelable = !showForgotPasscodeLink
this.setState({ this.setState({
types, prompts,
values, values,
processing: false, processing: false,
forgotPasscode: false, forgotPasscode: false,
showForgotPasscodeLink, showForgotPasscodeLink,
hasAccount: this.application.hasAccount(), hasAccount: this.application.hasAccount(),
processingPrompts: []
}); });
this.application.setChallengeCallbacks({ this.application.addChallengeObserver(
challenge: this.challenge, this.challenge,
onValidValue: (value) => { {
this.getState().values[value.type]!.invalid = false; onValidValue: (value) => {
removeFromArray(this.processingTypes, value.type); this.getState().values[value.prompt.id]!.invalid = false;
this.reloadProcessingStatus(); removeFromArray(this.state.processingPrompts, value.prompt);
}, this.reloadProcessingStatus();
onInvalidValue: (value) => { },
this.getState().values[value.type]!.invalid = true; onInvalidValue: (value) => {
removeFromArray(this.processingTypes, value.type); this.getState().values[value.prompt.id]!.invalid = true;
this.reloadProcessingStatus(); /** If custom validation, treat all values together and not individually */
}, if (!value.prompt.validates) {
onComplete: () => { this.setState({ processingPrompts: [], processing: false });
this.dismiss(); } else {
}, removeFromArray(this.state.processingPrompts, value.prompt);
}); this.reloadProcessingStatus();
}
},
onComplete: () => {
this.dismiss();
},
onCancel: () => {
this.dismiss();
},
}
);
} }
deinit() { deinit() {
@@ -118,34 +126,11 @@ class ChallengeModalCtrl extends PureViewCtrl {
} }
reloadProcessingStatus() { reloadProcessingStatus() {
this.setState({ return this.setState({
processing: this.processingTypes.length > 0 processing: this.state.processingPrompts.length > 0
}); });
} }
get title(): string {
if (this.challenge.reason === ChallengeReason.Migration) {
return STRING_STORAGE_UPDATE;
} else {
return STRING_AUTHENTICATION_REQUIRED;
}
}
promptForChallenge(challenge: ChallengeType): string {
if (challenge === ChallengeType.LocalPasscode) {
switch (this.challenge.reason) {
case ChallengeReason.Migration:
return STRING_ENTER_PASSCODE_FOR_MIGRATION;
case ChallengeReason.ResaveRootKey:
return STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER;
default:
return STRING_ENTER_ACCOUNT_PASSCODE;
}
} else {
return STRING_ENTER_ACCOUNT_PASSWORD;
}
}
async destroyLocalData() { async destroyLocalData() {
if (await confirmDialog({ if (await confirmDialog({
text: STRING_SIGN_OUT_CONFIRMATION, text: STRING_SIGN_OUT_CONFIRMATION,
@@ -156,10 +141,10 @@ class ChallengeModalCtrl extends PureViewCtrl {
}; };
} }
/** @template */
cancel() { cancel() {
if (this.cancelable) { if (this.cancelable) {
this.application!.cancelChallenge(this.challenge); this.application!.cancelChallenge(this.challenge);
this.dismiss();
} }
} }
@@ -169,18 +154,18 @@ class ChallengeModalCtrl extends PureViewCtrl {
}); });
} }
onTextValueChange(challenge: ChallengeType) { onTextValueChange(prompt: ChallengePrompt) {
const values = this.getState().values; const values = this.getState().values;
values[challenge]!.invalid = false; values[prompt.id]!.invalid = false;
this.setState({ values }); this.setState({ values });
} }
validate() { validate() {
const failed = []; const failed = [];
for (const type of this.getState().types) { for (const prompt of this.getState().prompts) {
const value = this.getState().values[type]; const value = this.getState().values[prompt.id];
if (!value || value.value.length === 0) { if (!value || value.value.length === 0) {
this.getState().values[type]!.invalid = true; this.getState().values[prompt.id]!.invalid = true;
} }
} }
return failed.length === 0; return failed.length === 0;
@@ -191,22 +176,29 @@ class ChallengeModalCtrl extends PureViewCtrl {
return; return;
} }
await this.setState({ processing: true }); await this.setState({ processing: true });
const values = []; const values: ChallengeValue[] = [];
for (const key of Object.keys(this.getState().values)) { for (const inputValue of Object.values(this.getState().values)) {
const type = Number(key) as ChallengeType; const rawValue = inputValue!!.value;
if (this.getState().values[type]!.invalid) { const value = new ChallengeValue(inputValue!.prompt, rawValue);
continue;
}
const rawValue = this.getState().values[type]!.value;
const value = new ChallengeValue(type, rawValue);
values.push(value); values.push(value);
} }
this.processingTypes = values.map((v) => v.type); const processingPrompts = values.map((v) => v.prompt);
if (values.length > 0) { await this.setState({
this.application.submitValuesForChallenge(this.challenge, values); processingPrompts: processingPrompts,
} else { processing: processingPrompts.length > 0
this.setState({ processing: false }); })
} /**
* Unfortunately neccessary to wait 50ms so that the above setState call completely
* updates the UI to change processing state, before we enter into UI blocking operation
* (crypto key generation)
*/
this.$timeout(() => {
if (values.length > 0) {
this.application.submitValuesForChallenge(this.challenge, values);
} else {
this.setState({ processing: false });
}
}, 50)
} }
dismiss() { dismiss() {

View File

@@ -347,7 +347,7 @@ class FooterViewCtrl extends PureViewCtrl<{}, {
confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON, confirmButtonText: STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON,
})) { })) {
preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => { preventRefreshing(STRING_CONFIRM_APP_QUIT_DURING_UPGRADE, async () => {
await this.application.performProtocolUpgrade(); await this.application.upgradeProtocolVersion();
}); });
} }
} }

View File

@@ -1,4 +1,4 @@
import { SNNote, SNTag } from 'snjs'; import { SNNote } from 'snjs';
export enum NoteSortKey { export enum NoteSortKey {
CreatedAt = 'created_at', CreatedAt = 'created_at',

View File

@@ -13,8 +13,9 @@
.challenge-modal { .challenge-modal {
max-width: 480px; max-width: 480px;
min-width: 400px !important;
.prompt { .prompt, .subprompt {
text-align: center; text-align: center;
} }
.sk-panel .sk-panel-header { .sk-panel .sk-panel-header {
@@ -90,10 +91,6 @@
} }
} }
#password-wizard {
font-size: 16px;
}
#item-preview-modal { #item-preview-modal {
> .sk-modal-content { > .sk-modal-content {
width: 800px; width: 800px;

View File

@@ -8,8 +8,7 @@
ng-if=` ng-if=`
!self.state.user && !self.state.user &&
!self.state.formData.showLogin && !self.state.formData.showLogin &&
!self.state.formData.showRegister && !self.state.formData.showRegister`
!self.state.formData.mfa`
) )
.sk-panel-row .sk-panel-row
.sk-h1 Sign in or register to enable sync and end-to-end encryption. .sk-h1 Sign in or register to enable sync and end-to-end encryption.
@@ -131,35 +130,10 @@
type='checkbox' type='checkbox'
) )
p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags) p.sk-p Merge local data ({{self.notesAndTagsCount()}} notes and tags)
.sk-panel-section(ng-if='self.state.formData.mfa')
form.sk-panel-form(ng-submit='self.submitMfaForm()')
.sk-p.sk-panel-row {{self.state.formData.mfa.message}}
.sk-panel-row
input.sk-input.contrast(
autofocus='true',
name='mfa',
ng-model='self.state.formData.userMfaCode',
placeholder='Enter Code',
required='',
should-focus='true',
sn-autofocus='true'
)
.sk-button-group.stretch.sk-panel-row.form-submit(
ng-if='!self.state.formData.status'
)
button.sk-button.info.featured(type='submit')
.sk-label Sign In
.sk-panel-section.no-bottom-pad(ng-if='self.state.formData.status')
.sk-panel-row
.sk-panel-row
.sk-horizontal-group
.sk-spinner.small.neutral
.sk-label {{self.state.formData.status}}
div( div(
ng-if=` ng-if=`
!self.state.formData.showLogin && !self.state.formData.showLogin &&
!self.state.formData.showRegister && !self.state.formData.showRegister`
!self.state.formData.mfa`
) )
.sk-panel-section(ng-if='self.state.user') .sk-panel-section(ng-if='self.state.user')
.sk-notification.danger(ng-if='self.syncStatus.error') .sk-notification.danger(ng-if='self.syncStatus.error')

View File

@@ -13,37 +13,36 @@
.sk-panel-column.stretch .sk-panel-column.stretch
form.sk-panel-form form.sk-panel-form
input.sk-input.contrast( input.sk-input.contrast(
ng-model='ctrl.state.formData.currentPassword', ng-model='ctrl.state.formData.currentPassword',
placeholder='Current Password', placeholder='Current Password',
should-focus='true', should-focus='true',
sn-autofocus='true', sn-autofocus='true',
type='password'
)
.sk-panel-row
input.sk-input.contrast(
ng-if='ctrl.props.changePassword',
ng-model='ctrl.state.formData.newPassword',
placeholder='New Password',
type='password' type='password'
) )
input.sk-input.contrast( input.sk-input.contrast(
ng-if='ctrl.props.changePassword', ng-if='ctrl.props.changePassword',
ng-model='ctrl.state.formData.newPassword',
placeholder='New Password',
type='password'
)
input.sk-input.contrast(
ng-if='ctrl.props.changePassword',
ng-model='ctrl.state.formData.newPasswordConfirmation', ng-model='ctrl.state.formData.newPasswordConfirmation',
placeholder='Confirm New Password', placeholder='Confirm New Password',
type='password' type='password'
) )
.sk-panel-section(ng-if='ctrl.state.step == 2') .sk-panel-section(ng-if='ctrl.state.step == 2')
div(ng-if='ctrl.props.changePassword') .sk-label.sk-bold.info(ng-if='ctrl.props.changePassword')
p.sk-p.sk-panel-row.info-i Your password has been successfully changed. | Your password has been successfully changed.
div(ng-if='ctrl.props.securityUpdate') p.sk-p.info-i(ng-if='ctrl.props.securityUpdate')
p.sk-p.sk-panel-row.info-i | The account update has been successfully applied to your account.
| The account update has been successfully applied to your account. p.sk-p
p.sk-p.sk-panel-row | Please ensure you are running the latest version of Standard Notes
| Please ensure you are running the latest version of Standard Notes
| on all platforms to ensure maximum compatibility. | on all platforms to ensure maximum compatibility.
.sk-panel-footer .sk-panel-footer
.empty .sk-button.info(
a.sk-a.info.right( ng-click='ctrl.nextStep()',
ng-click='ctrl.nextStep()', ng-disabled='ctrl.state.lockContinue'
ng-disabled='ctrl.state.lockContinue') )
.sk-spinner.small.inline.info.mr-5(ng-if='ctrl.state.showSpinner') .sk-label {{ctrl.state.continueTitle}}
| {{ctrl.state.continueTitle}}

View File

@@ -1,12 +1,11 @@
import { WebApplication } from '@/ui_models/application'; import { ApplicationService } from 'snjs';
export declare class AutolockService { export declare class AutolockService extends ApplicationService {
private application;
private unsubState; private unsubState;
private pollFocusInterval; private pollFocusInterval;
private lastFocusState?; private lastFocusState?;
private lockAfterDate?; private lockAfterDate?;
private lockTimeout?; private lockTimeout?;
constructor(application: WebApplication); onAppLaunch(): Promise<void>;
observeVisibility(): void; observeVisibility(): void;
deinit(): void; deinit(): void;
private lockApplication; private lockApplication;

View File

@@ -3,15 +3,15 @@ declare type StatusCallback = (string: string) => void;
export declare class StatusManager { export declare class StatusManager {
private statuses; private statuses;
private observers; private observers;
statusFromString(string: string): { replaceStatusWithString(status: FooterStatus, string: string): {
string: string;
};
addStatusFromString(string: string): {
string: string; string: string;
}; };
replaceStatusWithString(status: FooterStatus, string: string): FooterStatus;
addStatusFromString(string: string): FooterStatus;
addStatus(status: FooterStatus): FooterStatus;
removeStatus(status: FooterStatus): undefined; removeStatus(status: FooterStatus): undefined;
getStatusString(): string;
notifyObservers(): void;
addStatusObserver(callback: StatusCallback): () => void; addStatusObserver(callback: StatusCallback): () => void;
private notifyObservers;
private getStatusString;
} }
export {}; export {};

View File

@@ -1,5 +1,5 @@
/** @generic */ /** @generic */
export declare const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign out and sign back in to refresh your session."; export declare const STRING_SESSION_EXPIRED = "Your session has expired. New changes will not be pulled in. Please sign in to refresh your session.";
export declare const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe."; export declare const STRING_DEFAULT_FILE_ERROR = "Please use FileSafe or the Bold Editor to attach images and files. Learn more at standardnotes.org/filesafe.";
export declare const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in."; export declare const STRING_GENERIC_SYNC_ERROR = "There was an error syncing. Please try again. If all else fails, try signing out and signing back in.";
export declare function StringSyncException(data: any): string; export declare function StringSyncException(data: any): string;
@@ -32,15 +32,12 @@ export declare const STRING_GENERATING_LOGIN_KEYS = "Generating Login Keys...";
export declare const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys..."; export declare const STRING_GENERATING_REGISTER_KEYS = "Generating Account Keys...";
export declare const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again."; export declare const STRING_INVALID_IMPORT_FILE = "Unable to open file. Ensure it is a proper JSON file and try again.";
export declare function StringImportError(errorCount: number): string; export declare function StringImportError(errorCount: number): string;
export declare const STRING_ENTER_ACCOUNT_PASSCODE = "Enter your application passcode to unlock the application";
export declare const STRING_ENTER_ACCOUNT_PASSWORD = "Enter your account password";
export declare const STRING_ENTER_PASSCODE_FOR_MIGRATION = "Your application passcode is required to perform an upgrade of your local data storage structure.";
export declare const STRING_ENTER_PASSCODE_FOR_LOGIN_REGISTER = "Enter your application passcode before signing in or registering";
export declare const STRING_STORAGE_UPDATE = "Storage Update";
export declare const STRING_AUTHENTICATION_REQUIRED = "Authentication Required";
export declare const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = "This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again."; export declare const STRING_UNSUPPORTED_BACKUP_FILE_VERSION = "This backup file was created using an unsupported version of the application and cannot be imported here. Please update your application and try again.";
/** @password_change */ /** @password_change */
export declare const STRING_FAILED_PASSWORD_CHANGE = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup."; export declare const STRING_FAILED_PASSWORD_CHANGE = "There was an error re-encrypting your items. Your password was changed, but not all your items were properly re-encrypted and synced. You should try syncing again. If all else fails, you should restore your notes from backup.";
export declare const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE: string; export declare const STRING_CONFIRM_APP_QUIT_DURING_UPGRADE: string;
export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE: string; export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_CHANGE: string;
export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL: string; export declare const STRING_CONFIRM_APP_QUIT_DURING_PASSCODE_REMOVAL: string;
export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_TITLE = "Encryption upgrade available";
export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_TEXT: string;
export declare const STRING_UPGRADE_ACCOUNT_CONFIRM_BUTTON = "Upgrade";

View File

@@ -42,7 +42,6 @@ export declare class WebApplication extends SNApplication {
checkForSecurityUpdate(): Promise<boolean>; checkForSecurityUpdate(): Promise<boolean>;
presentPasswordWizard(type: PasswordWizardType): void; presentPasswordWizard(type: PasswordWizardType): void;
promptForChallenge(challenge: Challenge): void; promptForChallenge(challenge: Challenge): void;
performProtocolUpgrade(): Promise<void>;
presentPrivilegesModal(action: ProtectedAction, onSuccess?: any, onCancel?: any): Promise<void>; presentPrivilegesModal(action: ProtectedAction, onSuccess?: any, onCancel?: any): Promise<void>;
presentPrivilegesManagementModal(): void; presentPrivilegesManagementModal(): void;
authenticationInProgress(): boolean; authenticationInProgress(): boolean;

View File

@@ -12,6 +12,11 @@ export declare class PureViewCtrl<P = CtrlProps, S = CtrlState> {
private unsubApp; private unsubApp;
private unsubState; private unsubState;
private stateTimeout?; private stateTimeout?;
/**
* Subclasses can optionally add an ng-if=ctrl.templateReady to make sure that
* no Angular handlebars/syntax render in the UI before display data is ready.
*/
protected templateReady: boolean;
constructor($timeout: ng.ITimeoutService, props?: P); constructor($timeout: ng.ITimeoutService, props?: P);
$onInit(): void; $onInit(): void;
deinit(): void; deinit(): void;

4
package-lock.json generated
View File

@@ -10956,8 +10956,8 @@
"from": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad" "from": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad"
}, },
"snjs": { "snjs": {
"version": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790", "version": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9",
"from": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790" "from": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9"
}, },
"sockjs": { "sockjs": {
"version": "0.3.20", "version": "0.3.20",

View File

@@ -68,6 +68,6 @@
}, },
"dependencies": { "dependencies": {
"sncrypto": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad", "sncrypto": "github:standardnotes/sncrypto#8794c88daa967eaae493cd5fdec7506d52b257ad",
"snjs": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790" "snjs": "github:standardnotes/snjs#99d73922326b20e58f914ec9dd666efd6e5e4ac9"
} }
} }