feat: custom challenges

This commit is contained in:
Mo Bitar
2020-09-22 00:10:43 -05:00
parent cd66d769a7
commit d42518fc61
8 changed files with 100 additions and 95 deletions

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,10 +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_STORAGE_UPDATE = 'Storage Update';
export const STRING_AUTHENTICATION_REQUIRED = 'Authentication Required'; 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.';

View File

@@ -268,9 +268,9 @@ class ApplicationViewCtrl extends PureViewCtrl {
this.lastAlertShownTimeStamp = Date.now(); this.lastAlertShownTimeStamp = Date.now();
this.showingInvalidSessionAlert = true; this.showingInvalidSessionAlert = true;
setTimeout(async () => { setTimeout(async () => {
await alertDialog({ // await alertDialog({
text: STRING_SESSION_EXPIRED // text: STRING_SESSION_EXPIRED
}); // });
this.showingInvalidSessionAlert = false; this.showingInvalidSessionAlert = false;
}, 500); }, 500);
} }

View File

@@ -3,24 +3,28 @@
.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.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.state.title}}
strong {{ctrl.promptForChallenge(type)}} .sk-p.sk-panel-row.centered.subprompt(ng-if='ctrl.state.subtitle')
| {{ctrl.state.subtitle}}
.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,43 @@
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_STORAGE_UPDATE,
STRING_AUTHENTICATION_REQUIRED, 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,
title: string,
subtitle: string
} }
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 +58,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 +87,42 @@ 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(),
title: this.challenge.title,
subtitle: this.challenge.subtitle,
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: [] });
this.dismiss(); } else {
}, removeFromArray(this.state.processingPrompts, value.prompt);
}); }
this.reloadProcessingStatus();
},
onComplete: () => {
this.dismiss();
},
onCancel: () => {
this.dismiss();
},
}
);
} }
deinit() { deinit() {
@@ -118,12 +132,12 @@ 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 { get modalTitle(): string {
if (this.challenge.reason === ChallengeReason.Migration) { if (this.challenge.reason === ChallengeReason.Migration) {
return STRING_STORAGE_UPDATE; return STRING_STORAGE_UPDATE;
} else { } else {
@@ -131,21 +145,6 @@ class ChallengeModalCtrl extends PureViewCtrl {
} }
} }
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,
@@ -169,18 +168,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 +190,32 @@ 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; if (inputValue!.invalid) {
if (this.getState().values[type]!.invalid) {
continue; continue;
} }
const rawValue = this.getState().values[type]!.value; const rawValue = inputValue!!.value;
const value = new ChallengeValue(type, rawValue); const value = new ChallengeValue(inputValue!.prompt, 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

@@ -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,10 +32,6 @@ 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_STORAGE_UPDATE = "Storage Update";
export declare const STRING_AUTHENTICATION_REQUIRED = "Authentication Required"; 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.";

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#6a0e03eaca106ffda7bce7d87761a29fd8f8420d",
"from": "github:standardnotes/snjs#f922ede72a3e90984605048854dc20db8a88c790" "from": "github:standardnotes/snjs#6a0e03eaca106ffda7bce7d87761a29fd8f8420d"
}, },
"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#6a0e03eaca106ffda7bce7d87761a29fd8f8420d"
} }
} }