Password lock screen and delete data screen

This commit is contained in:
Vincent 2020-01-28 16:43:39 +11:00
parent 4a6ed12992
commit 4a7e2dece7
18 changed files with 390 additions and 218 deletions

View File

@ -1376,7 +1376,7 @@
"description": "Description of the media permission description"
},
"clearDataHeader": {
"message": "Clear Data",
"message": "Clear All Local Data",
"description":
"Header in the settings dialog for the section dealing with data deletion"
},
@ -2281,7 +2281,7 @@
},
"passwordViewTitle": {
"message": "Type in your password",
"message": "Type In Your Password",
"description":
"The title shown when user needs to type in a password to unlock the messenger"
},
@ -2309,6 +2309,9 @@
"message": "Remove Password",
"description": "Button action that the user can click to remove a password"
},
"maxPasswordAttempts": {
"message": "Invalid Password. Would you like to reset the database?"
},
"typeInOldPassword": {
"message": "Please type in your old password"
},
@ -2316,7 +2319,7 @@
"message": "Old password is invalid"
},
"invalidPassword": {
"message": "Invalid password"
"message": "Invalid Password"
},
"noGivenPassword": {
"message": "Please enter your password"

View File

@ -4,7 +4,7 @@
<meta charset='utf-8'>
<meta content='width=device-width, user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' name='viewport'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Signal</title>
<title>Session</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href='/images/loki/loki_icon_128.png' rel='shortcut icon'>

View File

@ -74,6 +74,9 @@ const {
const {
SessionPasswordModal,
} = require('../../ts/components/session/SessionPasswordModal');
const {
SessionPasswordPrompt,
} = require('../../ts/components/session/SessionPasswordPrompt');
const {
SessionConfirm,
@ -293,6 +296,7 @@ exports.setup = (options = {}) => {
SessionQRModal,
SessionSeedModal,
SessionPasswordModal,
SessionPasswordPrompt,
SessionDropdown,
SessionScrollButton,
MediaGallery,

View File

@ -1,72 +0,0 @@
/* global i18n: false */
/* global Whisper: false */
/* eslint-disable no-new */
// eslint-disable-next-line func-names
(function() {
'use strict';
window.Whisper = window.Whisper || {};
const { Logs } = window.Signal;
const CLEAR_DATA_STEPS = {
CHOICE: 1,
DELETING: 2,
};
window.Whisper.ClearDataView = Whisper.View.extend({
templateName: 'clear-data',
className: 'full-screen-flow overlay',
events: {
'click .cancel': 'onCancel',
'click .delete-all-data': 'onDeleteAllData',
},
initialize(onClear = null) {
this.step = CLEAR_DATA_STEPS.CHOICE;
this.onClear = onClear;
},
onCancel() {
this.remove();
},
async onDeleteAllData() {
window.log.info('Deleting everything!');
this.step = CLEAR_DATA_STEPS.DELETING;
this.render();
await this.clearAllData();
},
async clearAllData() {
if (this.onClear) {
this.onClear();
} else {
try {
await Logs.deleteAll();
await window.Signal.Data.removeAll();
await window.Signal.Data.close();
await window.Signal.Data.removeDB();
await window.Signal.Data.removeOtherData();
} catch (error) {
window.log.error(
'Something went wrong deleting all data:',
error && error.stack ? error.stack : error
);
}
window.restart();
}
},
render_attributes() {
return {
isStep1: this.step === CLEAR_DATA_STEPS.CHOICE,
header: i18n('deleteAllDataHeader'),
body: i18n('deleteAllDataBody'),
cancelButton: i18n('cancel'),
deleteButton: i18n('deleteAllDataButton'),
isStep2: this.step === CLEAR_DATA_STEPS.DELETING,
deleting: i18n('deleteAllDataProgress'),
};
},
});
})();

View File

@ -1,7 +1,4 @@
/* global i18n: false */
/* global Whisper: false */
/* eslint-disable no-new */
/* global Whisper */
// eslint-disable-next-line func-names
(function() {
@ -9,63 +6,20 @@
window.Whisper = window.Whisper || {};
const MIN_LOGIN_TRIES = 3;
Whisper.PasswordView = Whisper.View.extend({
className: 'password full-screen-flow standalone-fullscreen',
templateName: 'password',
events: {
keyup: 'onKeyup',
'click #unlock-button': 'onLogin',
'click #reset-button': 'onReset',
},
initialize() {
this.errorCount = 0;
this.render();
},
render_attributes() {
return {
title: i18n('passwordViewTitle'),
buttonText: i18n('unlock'),
resetText: i18n('resetDatabase'),
showReset: this.errorCount >= MIN_LOGIN_TRIES,
};
},
onKeyup(event) {
switch (event.key) {
case 'Enter':
this.onLogin();
break;
default:
return;
}
event.preventDefault();
},
async onLogin() {
const passPhrase = this.$('#passPhrase').val();
const trimmed = passPhrase ? passPhrase.trim() : passPhrase;
this.setError('');
try {
await window.onLogin(trimmed);
} catch (e) {
// Increment the error counter and show the button if necessary
this.errorCount += 1;
if (this.errorCount >= MIN_LOGIN_TRIES) {
this.render();
}
this.setError(`Error: ${e}`);
}
},
setError(string) {
this.$('.error').text(string);
},
onReset() {
const clearDataView = new window.Whisper.ClearDataView(() => {
window.resetDatabase();
render() {
this.passwordView = new window.Whisper.ReactWrapperView({
className: 'password overlay',
Component: window.Signal.Components.SessionPasswordPrompt,
props: {},
});
clearDataView.render();
this.$el.append(clearDataView.el);
this.$el.append(this.passwordView.el);
return this;
},
});
})();

View File

@ -16,63 +16,12 @@
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
<style>
</style>
<script type='text/x-tmpl-mustache' id='password'>
<div class='content-wrapper standalone'>
<div class='content'>
<h2>{{ title }}</h2>
<div class='inputs'>
<input class='form-control' type='password' id='passPhrase' placeholder='Password' autocomplete='off' spellcheck='false' />
<a class='button session-button brand green' id='unlock-button'>{{ buttonText }}</a>
<div class='error'></div>
{{ #showReset }}
<div class='reset'>
<a id='reset-button'>{{ resetText }}</a>
</div>
{{ /showReset }}
</div>
</div>
</div>
</script>
<script type='text/x-tmpl-mustache' id='clear-data'>
{{#isStep1}}
<div class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon alert-outline-red'></span>
<div class='header'>{{ header }}</div>
<div class='body-text-wide'>{{ body }}</div>
</div>
<div class='nav'>
<div>
<a class='button neutral cancel'>{{ cancelButton }}</a>
<a class='button destructive delete-all-data'>{{ deleteButton }}</a>
</div>
</div>
</div>
</div>
{{/isStep1}}
{{#isStep2}}
<div id='step3' class='step'>
<div class='inner'>
<div class='step-body'>
<span class='banner-icon delete'></span>
<div class='header'>{{ deleting }}</div>
</div>
<div class='progress'>
<div class='bar-container'>
<div class='bar progress-bar progress-bar-striped active'></div>
</div>
</div>
</div>
</div>
{{/isStep2}}
</script>
<script type='text/javascript' src='js/components.js'></script>
<script type='text/javascript' src='js/views/whisper_view.js'></script>
<script type='text/javascript' src='js/views/react_wrapper_view.js'></script>
<script type='text/javascript' src='js/views/password_view.js'></script>
<script type='text/javascript' src='js/views/clear_data_view.js'></script>
</head>
<body>
<div class='app-loading-screen'>

View File

@ -8,6 +8,9 @@ const config = url.parse(window.location.toString(), true).query;
const { locale } = config;
const localeMessages = ipcRenderer.sendSync('locale-data');
window.React = require('react');
window.ReactDOM = require('react-dom');
window.theme = config.theme;
window.i18n = i18n.setup(locale, localeMessages);
@ -17,6 +20,9 @@ window.getAppInstance = () => config.appInstance;
// So far we're only using this for Signal.Types
const Signal = require('./js/modules/signal');
const electron = require('electron');
const ipc = electron.ipcRenderer;
window.Signal = Signal.setup({
Attachments: null,
@ -24,12 +30,32 @@ window.Signal = Signal.setup({
getRegionCode: () => null,
});
window.Signal.Logs = require('./js/modules/logs');
window.CONSTANTS = {
MAX_LOGIN_TRIES: 3,
MAX_PASSWORD_LENGTH: 32,
MAX_USERNAME_LENGTH: 20,
};
window.passwordUtil = require('./app/password_util');
window.Signal.Logs = require('./js/modules/logs');
window.resetDatabase = () => {
window.log.info('reset database');
ipcRenderer.send('resetDatabase');
};
window.restart = () => {
window.log.info('restart');
ipc.send('restart');
};
window.clearLocalData = async () => {
window.resetDatabase();
window.restart();
};
window.onLogin = passPhrase =>
new Promise((resolve, reject) => {
ipcRenderer.once('password-window-login-response', (event, error) => {

View File

@ -60,8 +60,9 @@ window.isBeforeVersion = (toCheck, baseVersion) => {
};
window.CONSTANTS = {
maxPasswordLength: 32,
maxUsernameLength: 20,
MAX_LOGIN_TRIES: 3,
MAX_PASSWORD_LENGTH: 32,
MAX_USERNAME_LENGTH: 20,
};
window.versionInfo = {
@ -134,6 +135,11 @@ window.restart = () => {
ipc.send('restart');
};
window.resetDatabase = () => {
window.log.info('reset database');
ipc.send('resetDatabase');
};
// Events for updating block number states across different windows.
// In this case we need these to update the blocked number
// collection on the main window from the settings window.

View File

@ -225,6 +225,7 @@ $session_message-container-border-radius: 5px;
}
.button-group > div {
display: inline-flex;
margin-left: 5px;
margin-right: 5px;
}
@ -258,6 +259,10 @@ $session_message-container-border-radius: 5px;
&.brand {
color: $session-color-white;
&:hover {
filter: brightness(90%);
}
&.green,
&.white,
&.primary,
@ -1438,3 +1443,89 @@ input {
}
}
}
.clear-data,
.password-prompt {
&-wrapper {
display: flex;
justify-content: center;
align-items: center;
background-color: $session-color-black;
width: 100%;
height: 100%;
padding: 3 * $session-margin-lg;
}
&-error-section {
width: 100%;
color: $session-color-white;
margin: -$session-margin-sm 0px 2 * $session-margin-lg 0px;
.session-label {
&.primary {
background-color: rgba($session-color-primary, 0.3);
}
padding: $session-margin-xs $session-margin-sm;
font-size: $session-font-xs;
text-align: center;
}
}
&-container {
font-family: 'SF Pro Text';
color: $session-color-white;
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 600px;
min-width: 420px;
padding: 3 * $session-margin-lg 2 * $session-margin-lg;
box-sizing: border-box;
background-color: $session-shade-4;
border: 1px solid $session-shade-8;
border-radius: 2px;
.warning-info-area,
.password-info-area {
display: inline-flex;
justify-content: center;
align-items: center;
h1 {
color: $session-color-white;
}
svg {
margin-right: $session-margin-lg;
}
}
p,
input {
margin: $session-margin-lg 0px;
}
.button-group {
display: inline-flex;
}
#password-prompt-input {
width: 100%;
color: #fff;
background-color: #2e2e2e;
margin-top: 2 * $session-margin-lg;
padding: $session-margin-xs $session-margin-lg;
outline: none;
border: none;
border-radius: 2px;
text-align: center;
font-size: 24px;
letter-spacing: 5px;
font-family: 'SF Pro Text';
}
}
}

View File

@ -208,7 +208,7 @@ export class EditProfileDialog extends React.Component<Props, State> {
value={this.state.profileName}
placeholder={placeholderText}
onChange={this.onNameEdited}
maxLength={window.CONSTANTS.maxUsernameLength}
maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH}
tabIndex={0}
required={true}
aria-required={true}
@ -302,7 +302,10 @@ export class EditProfileDialog extends React.Component<Props, State> {
private onClickOK() {
const newName = this.state.profileName.trim();
if (newName.length === 0 || newName.length > window.CONSTANTS.maxUsernameLength) {
if (
newName.length === 0 ||
newName.length > window.CONSTANTS.MAX_USERNAME_LENGTH
) {
return;
}

View File

@ -64,7 +64,8 @@ export class ActionsPanel extends React.Component<Props, State> {
const handleClick = onSelect
? () => {
type === SectionType.Profile
? this.editProfileHandle()
? /* tslint:disable-next-line:no-void-expression */
this.editProfileHandle()
: /* tslint:disable-next-line:no-void-expression */
onSelect(type);
}

View File

@ -449,7 +449,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
type="text"
placeholder={window.i18n('enterDisplayName')}
value={this.state.displayName}
maxLength={window.CONSTANTS.maxUsernameLength}
maxLength={window.CONSTANTS.MAX_USERNAME_LENGTH}
onValueChanged={(val: string) => {
this.onDisplayNameChanged(val);
}}
@ -463,7 +463,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
error={this.state.passwordErrorString}
type="password"
placeholder={window.i18n('enterOptionalPassword')}
maxLength={window.CONSTANTS.maxPasswordLength}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onValueChanged={(val: string) => {
this.onPasswordChanged(val);
}}
@ -477,7 +477,7 @@ export class RegistrationTabs extends React.Component<{}, State> {
error={passwordsDoNotMatch}
type="password"
placeholder={window.i18n('optionalPassword')}
maxLength={window.CONSTANTS.maxPasswordLength}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
onValueChanged={(val: string) => {
this.onPasswordVerifyChanged(val);
}}

View File

@ -34,7 +34,14 @@ export class SessionInput extends React.PureComponent<Props, State> {
}
public render() {
const { placeholder, type, value, maxLength, enableShowHide, error } = this.props;
const {
placeholder,
type,
value,
maxLength,
enableShowHide,
error,
} = this.props;
const { forceShow } = this.state;
const correctType = forceShow ? 'text' : type;

View File

@ -58,14 +58,14 @@ export class SessionPasswordModal extends React.Component<Props, State> {
type="password"
id="password-modal-input"
placeholder={placeholders[0]}
maxLength={window.CONSTANTS.maxPasswordLength}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
/>
{action !== PasswordAction.Remove && (
<input
type="password"
id="password-modal-input-confirm"
placeholder={placeholders[1]}
maxLength={window.CONSTANTS.maxPasswordLength}
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
/>
)}
</div>
@ -120,7 +120,7 @@ export class SessionPasswordModal extends React.Component<Props, State> {
$('#password-modal-input-confirm').val()
);
if (enteredPassword.length === 0 || enteredPasswordConfirm.length === 0){
if (enteredPassword.length === 0 || enteredPasswordConfirm.length === 0) {
return;
}

View File

@ -0,0 +1,196 @@
import React from 'react';
import classNames from 'classnames';
import { SessionIcon, SessionIconType } from './icon';
import {
SessionButton,
SessionButtonColor,
SessionButtonType,
} from './SessionButton';
interface State {
error: string;
errorCount: number;
clearDataView: boolean;
}
export class SessionPasswordPrompt extends React.PureComponent<{}, State> {
constructor(props: any) {
super(props);
this.state = {
error: '',
errorCount: 0,
clearDataView: false,
};
this.onKeyUp = this.onKeyUp.bind(this);
this.initLogin = this.initLogin.bind(this);
this.initClearDataView = this.initClearDataView.bind(this);
window.addEventListener('keyup', this.onKeyUp);
}
public render() {
const showResetElements =
this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES;
const wrapperClass = this.state.clearDataView
? 'clear-data-wrapper'
: 'password-prompt-wrapper';
const containerClass = this.state.clearDataView
? 'clear-data-container'
: 'password-prompt-container';
const infoAreaClass = this.state.clearDataView
? 'warning-info-area'
: 'password-info-area';
const infoTitle = this.state.clearDataView
? window.i18n('clearDataHeader')
: window.i18n('passwordViewTitle');
const buttonGroup = this.state.clearDataView
? this.renderClearDataViewButtons()
: this.renderPasswordViewButtons();
const featureElement = this.state.clearDataView ? (
<p className="text-center">{window.i18n('clearDataExplanation')}</p>
) : (
<input
id="password-prompt-input"
type="password"
autoFocus={true}
placeholder=" "
defaultValue=""
maxLength={window.CONSTANTS.MAX_PASSWORD_LENGTH}
/>
);
const infoIcon = this.state.clearDataView ? (
<SessionIcon
iconType={SessionIconType.Warning}
iconSize={35}
iconColor="#ce0000"
/>
) : (
<SessionIcon
iconType={SessionIconType.Lock}
iconSize={35}
iconColor="#00f782"
/>
);
const errorSection = !this.state.clearDataView && (
<div className="password-prompt-error-section">
{this.state.error && (
<>
{showResetElements ? (
<div className="session-label warning">
{window.i18n('maxPasswordAttempts')}
</div>
) : (
<div className="session-label primary">{this.state.error}</div>
)}
</>
)}
</div>
);
return (
<div className={wrapperClass}>
<div className={containerClass}>
<div className={infoAreaClass}>
{infoIcon}
<h1>{infoTitle}</h1>
</div>
{featureElement}
{errorSection}
{buttonGroup}
</div>
</div>
);
}
public async onKeyUp(event: any) {
switch (event.key) {
case 'Enter':
await this.initLogin();
break;
default:
}
event.preventDefault();
}
public async onLogin(passPhrase: string) {
const trimmed = passPhrase ? passPhrase.trim() : passPhrase;
try {
await window.onLogin(trimmed);
} catch (e) {
// Increment the error counter and show the button if necessary
this.setState({
errorCount: this.state.errorCount + 1,
});
this.setState({ error: e });
}
}
private async initLogin() {
const passPhrase = String($('#password-prompt-input').val());
await this.onLogin(passPhrase);
}
private initClearDataView() {
this.setState({
error: '',
errorCount: 0,
clearDataView: true,
});
}
private renderPasswordViewButtons(): JSX.Element {
const showResetElements =
this.state.errorCount >= window.CONSTANTS.MAX_LOGIN_TRIES;
return (
<div className={classNames(showResetElements && 'button-group')}>
{showResetElements && (
<>
<SessionButton
text="Reset Database"
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Danger}
onClick={this.initClearDataView}
/>
</>
)}
<SessionButton
text={window.i18n('unlock')}
buttonType={SessionButtonType.BrandOutline}
buttonColor={SessionButtonColor.Green}
onClick={this.initLogin}
/>
</div>
);
}
private renderClearDataViewButtons(): JSX.Element {
return (
<div className="button-group">
<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Primary}
onClick={() => {
this.setState({ clearDataView: false });
}}
/>
<SessionButton
text={window.i18n('deleteAllDataButton')}
buttonType={SessionButtonType.Default}
buttonColor={SessionButtonColor.Danger}
onClick={window.clearLocalData}
/>
</div>
);
}
}

View File

@ -1,12 +1,8 @@
import React from 'react';
import { SessionIconButton, SessionIconType, SessionIconSize } from './icon';
import { SessionIconButton, SessionIconSize, SessionIconType } from './icon';
interface Props {
count: number;
}
export class SessionScrollButton extends React.PureComponent<Props> {
export class SessionScrollButton extends React.PureComponent {
constructor(props: any) {
super(props);
}

View File

@ -5,7 +5,7 @@ import { icons, SessionIconSize, SessionIconType } from '../icon';
export interface Props {
iconType: SessionIconType;
iconSize: SessionIconSize;
iconSize: SessionIconSize | number;
iconColor: string;
iconPadded: boolean;
iconRotation: number;
@ -33,21 +33,25 @@ export class SessionIcon extends React.PureComponent<Props> {
} = this.props;
let iconDimensions;
switch (iconSize) {
case SessionIconSize.Small:
iconDimensions = '15';
break;
case SessionIconSize.Medium:
iconDimensions = '20';
break;
case SessionIconSize.Large:
iconDimensions = '25';
break;
case SessionIconSize.Huge:
iconDimensions = '30';
break;
default:
iconDimensions = '20';
if (typeof iconSize === 'number') {
iconDimensions = iconSize;
} else {
switch (iconSize) {
case SessionIconSize.Small:
iconDimensions = '15';
break;
case SessionIconSize.Medium:
iconDimensions = '20';
break;
case SessionIconSize.Large:
iconDimensions = '25';
break;
case SessionIconSize.Huge:
iconDimensions = '30';
break;
default:
iconDimensions = '20';
}
}
const iconDef = icons[iconType];

4
ts/global.d.ts vendored
View File

@ -4,6 +4,7 @@ interface Window {
Events: any;
deleteAllData: any;
clearLocalData: any;
getAccountManager: any;
mnemonic: any;
clipboard: any;
@ -23,6 +24,7 @@ interface Window {
// Following function needs to be written in background.js
// getMemberList: any;
onLogin: any;
setPassword: any;
textsecure: any;
Session: any;
@ -50,6 +52,8 @@ interface Window {
getSettingValue: any;
setSettingValue: any;
lokiFeatureFlags: any;
resetDatabase: any;
}
interface Promise<T> {