[WO-462] Rework login workflow

* Make keygen and import possibilities clearer in login-initial
* Show spinner when generating key
* Use mobile design (wide buttons) everywhere
* Show info about key-sync in login-new-device (for mobile users)
* remove info popovers in login screens
* allow keyfile import even when keysync has been activated
This commit is contained in:
Tankred Hase 2014-07-31 19:08:21 +02:00
parent 9b618cc20f
commit e19d8a4e5b
8 changed files with 114 additions and 112 deletions

View File

@ -9,7 +9,8 @@ define(function(require) {
states = { states = {
IDLE: 1, IDLE: 1,
PROCESSING: 2, SET_PASSPHRASE: 2,
PROCESSING: 3,
DONE: 4 DONE: 4
}; };
$scope.state.ui = states.IDLE; // initial state $scope.state.ui = states.IDLE; // initial state
@ -29,6 +30,17 @@ define(function(require) {
$location.path('/login-new-device'); $location.path('/login-new-device');
}; };
$scope.setPassphrase = function() {
if (!$scope.state.agree) {
$scope.onError({
message: termsMsg
});
return;
}
$scope.setState(states.SET_PASSPHRASE);
};
/* /*
* Taken from jQuery validate.password plug-in 1.0 * Taken from jQuery validate.password plug-in 1.0
* http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/ * http://bassistance.de/jquery-plugins/jquery-plugin-validate.password/
@ -90,20 +102,14 @@ define(function(require) {
return; return;
} }
if (!$scope.state.agree) {
$scope.onError({
message: termsMsg
});
return;
}
$scope.setState(states.PROCESSING); $scope.setState(states.PROCESSING);
setTimeout(function() { setTimeout(function() {
emailDao.unlock({ emailDao.unlock({
passphrase: (passphrase) ? passphrase : undefined passphrase: (passphrase) ? passphrase : undefined
}, function(err) { }, function(err) {
if (err) { if (err) {
$scope.setState(states.IDLE); $scope.setState(states.SET_PASSPHRASE);
$scope.onError(err); $scope.onError(err);
return; return;
} }

View File

@ -17,10 +17,14 @@
} }
@include respond-to(desktop) { @include respond-to(desktop) {
margin: 135px auto 75px; margin: 115px auto 75px;
} }
} }
.working {
text-align: center;
}
.spinner { .spinner {
font-size: 150%; font-size: 150%;
} }
@ -38,10 +42,7 @@
color: $btn-color; color: $btn-color;
margin-right: 10px; margin-right: 10px;
margin-bottom: 10px; margin-bottom: 10px;
width: 100%;
@include respond-to(mobile) {
width: 100%;
}
} }
p, label { p, label {
@ -52,6 +53,18 @@
margin: 20px 0; margin: 20px 0;
} }
fieldset {
margin: 30px 0 40px;
legend {
color: $color-blue;
}
p {
margin: 0;
}
}
input { input {
margin-right: 10px; margin-right: 10px;
} }
@ -98,17 +111,6 @@
.popover-info { .popover-info {
display: none; // hide on mobile display: none; // hide on mobile
} }
@include respond-to(desktop) {
input[type="text"],
input[type="password"],
input[type="file"] {
width: auto;
}
.popover-info {
display: inline-block;
}
}
} }
} }
@ -122,19 +124,25 @@
.view-login-privatekey-download { .view-login-privatekey-download {
.content { .content {
max-width: 500px;
input.code { fieldset {
margin-right: 0; margin-top: 20px;
width: auto; }
.code {
max-width: 240px;
margin: 0 auto;
input {
margin-right: 0;
width: auto;
}
} }
} }
} }
.view-login-set-credentials { .view-login-set-credentials {
.content { .content {
max-width: 450px;
b, a { b, a {
text-decoration: none; text-decoration: none;
} }

View File

@ -9,7 +9,6 @@
<form> <form>
<div> <div>
<input class="input-text" type="password" ng-model="passphrase" ng-change="change()" ng-class="{'input-text-error':incorrect}" placeholder="Passphrase" tabindex="1" focus-me="true"> <input class="input-text" type="password" ng-model="passphrase" ng-change="change()" ng-class="{'input-text-error':incorrect}" placeholder="Passphrase" tabindex="1" focus-me="true">
<span class="popover-info" data-icon-append="&#xe010;" popover="#passphrase-info"></span>
</div> </div>
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Forgot your passphrase?</a> <a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Forgot your passphrase?</a>
<div> <div>
@ -17,15 +16,4 @@
</div> </div>
</form> </form>
</div><!--/content--> </div><!--/content-->
</div> </div>
<!-- popovers -->
<div id="passphrase-info" class="popover right" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>What is this?</b></div>
<div class="popover-content">
<p>A passphrase is like a password that protects your PGP key.</p>
<p>There is no way to access your messages without your passphrase.</p>
<p>If you have forgotten your passphrase, please request an account reset using the provided link. You will not be able to read previous messages after a reset.</p>
</div>
</div><!--/.popover-->

View File

@ -1,21 +1,14 @@
<div class="view-login view-login-initial" ng-class="{'waiting-cursor': state.ui === 2}"> <div class="view-login view-login-initial" ng-class="{'waiting-cursor': state.ui === 3}">
<div class="logo"> <div class="logo">
<img src="img/whiteout_logo.svg" alt="whiteout.io"> <img src="img/whiteout_logo.svg" alt="whiteout.io">
</div><!--/logo--> </div><!--/logo-->
<div class="content" ng-switch on="state.ui"> <div class="content">
<div ng-show="state.ui === 1">
<p><b>PGP key.</b> You can either import an existing PGP key or generate a new one. Your private key remains on your device and is not sent to our servers.</p>
<div ng-switch-when="1">
<p><b>PGP key.</b> You can either import an existing PGP key or generate a new one.</p>
<p>If you want to generate a new key, you can set a passphrase to protect your key on disk.</p>
<form> <form>
<div>
<label class="input-error-message" ng-class="{'passphrase-label-ok': passphraseRating >= 2}">{{passphraseMsg}}</label><br>
<input class="input-text" type="password" ng-model="state.passphrase" ng-change="checkPassphraseQuality()" placeholder="Enter passphrase" tabindex="1" focus-me="true">
<input class="input-text" type="password" ng-model="state.confirmation" ng-class="{'input-text-error': (state.confirmation || state.passphrase) && state.confirmation !== state.passphrase}" placeholder="Confirm passphrase" tabindex="2">
<span class="popover-info" data-icon-append="&#xe010;" popover="#passphrase-info"></span>
</div>
<div> <div>
<input type="checkbox" ng-model="state.agree" name="checkbox" id="checkbox_id"> <input type="checkbox" ng-model="state.agree" name="checkbox" id="checkbox_id">
<label for="checkbox_id">I agree to the Whiteout Networks <a href="https://whiteout.io/terms.html" target="_blank">Terms of Service</a> and have read the <a href="https://whiteout.io/privacy-service.html" target="_blank">Privacy Policy</a>.</label> <label for="checkbox_id">I agree to the Whiteout Networks <a href="https://whiteout.io/terms.html" target="_blank">Terms of Service</a> and have read the <a href="https://whiteout.io/privacy-service.html" target="_blank">Privacy Policy</a>.</label>
@ -23,24 +16,33 @@
<div> <div>
<button wo-touch="importKey()" class="btn btn-alt">Import existing key</button> <button wo-touch="importKey()" class="btn btn-alt">Import existing key</button>
<button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="(state.confirmation || state.passphrase) && state.confirmation !== state.passphrase" tabindex="3">Generate new key</button> <button type="submit" wo-touch="setPassphrase()" class="btn"tabindex="3">Generate new key</button>
</div> </div>
</form> </form>
</div> </div>
<div ng-switch-when="2"> <div ng-show="state.ui === 2">
<p><b>Set passphrase.</b> You can set a passphrase to protect your key on disk. This must be entered everytime you start the app. For no passphrase just press continue.</p>
<form>
<div>
<label class="input-error-message" ng-class="{'passphrase-label-ok': passphraseRating >= 2}">{{passphraseMsg}}</label><br>
<input class="input-text" type="password" ng-model="state.passphrase" ng-change="checkPassphraseQuality()" placeholder="Enter passphrase" tabindex="1" focus-me="true">
<input class="input-text" type="password" ng-model="state.confirmation" ng-class="{'input-text-error': (state.confirmation || state.passphrase) && state.confirmation !== state.passphrase}" placeholder="Confirm passphrase" tabindex="2">
</div>
<div>
<button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="(state.confirmation || state.passphrase) && state.confirmation !== state.passphrase" tabindex="3">Continue</button>
</div>
</form>
</div>
<div ng-show="state.ui === 3">
<p><b>Generating key.</b> Please stand by. This can take a while...</p> <p><b>Generating key.</b> Please stand by. This can take a while...</p>
<div class="working">
<span class="spinner"></span>
</div><!--/.working-->
</div> </div>
</div><!--/content--> </div><!--/content-->
</div> </div>
<!-- popovers -->
<div id="passphrase-info" class="popover right" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>What is this?</b></div>
<div class="popover-content">
<p>A passphrase is like a password that protects your PGP key.</p>
<p>If your device is lost or stolen the passphrase protects the contents of your mailbox.</p>
</div>
</div><!--/.popover-->

View File

@ -4,40 +4,22 @@
</div><!--/logo--> </div><!--/logo-->
<div class="content"> <div class="content">
<p><b>Import keyfile.</b> To access your emails on this device, please import your existing key file.</p> <p><b>Import PGP key.</b> Please import an existing key from the file system.</p>
<fieldset>
<legend>On a mobile device?</legend>
<p>If you cannot import your key via a USB stick, you can setup <i>Key sync</i> on a desktop PC to securely transfer your PGP key over the Whiteout cloud. <a href="https://blog.whiteout.io/2014/07/07/secure-pgp-key-sync-a-proposal/" target="_blank">Learn more</a>.</p>
</fieldset>
<form> <form>
<div> <div>
<input type="file" accept=".asc" file-reader tabindex="1"> <input type="file" accept=".asc" file-reader tabindex="1">
<span class="popover-info" data-icon-append="&#xe010;" popover="#keyfile-info"></span>
</div> </div>
<div> <div>
<input class="input-text" type="password" ng-model="passphrase" ng-class="{'input-text-error':incorrect}" placeholder="Passphrase" tabindex="2" focus-me="true"> <input class="input-text" type="password" ng-model="passphrase" ng-class="{'input-text-error':incorrect}" placeholder="Passphrase" tabindex="2" focus-me="true">
<span class="popover-info" data-icon-append="&#xe010;" popover="#passphrase-info"></span>
</div> </div>
<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keyfile or passphrase?</a> <a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keyfile or passphrase?</a>
<div><button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="!key" tabindex="3">Import</button> <div><button type="submit" wo-touch="confirmPassphrase()" class="btn" ng-disabled="!key" tabindex="3">Import</button>
</form> </form>
</div> </div>
</div> </div>
<!-- popovers -->
<div id="keyfile-info" class="popover right" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>What is this?</b></div>
<div class="popover-content">
<p>The keyfile contains your PGP keys.</p>
<p>It can be exported on your first computer under "Account".</p>
<p>You can import it from a USB flash drive. Never send the keyfile to yourself via email.</p>
</div>
</div><!--/.popover-->
<div id="passphrase-info" class="popover right" ng-controller="PopoverCtrl">
<div class="arrow"></div>
<div class="popover-title"><b>What is this?</b></div>
<div class="popover-content">
<p>A passphrase is like a password that protects your PGP key.</p>
<p>There is no way to access your messages without your passphrase.</p>
<p>If you have forgotten your passphrase, please request an account reset using the provided link. You will not be able to read previous messages after a reset.</p>
</div>
</div><!--/.popover-->

View File

@ -7,10 +7,14 @@
<div class="step" ng-show="step === 1"> <div class="step" ng-show="step === 1">
<p><b>Key sync.</b> We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.</p> <p><b>Key sync.</b> We have sent you an email containing a recovery token. Please copy and paste the token below to download your key.</p>
<p>You can also just import the key file manually e.g. if you're on a device with USB access.</p>
<input type="text" class="input-text" size="42" ng-model="recoveryToken" placeholder="Recovery token" focus-me="step === 1"> <input type="text" class="input-text" size="42" ng-model="recoveryToken" placeholder="Recovery token" focus-me="step === 1">
<fieldset>
<legend>Got USB?</legend>
<p>You can also import the key file manually if you're on a device with USB access and your key is on a flash drive.</p>
</fieldset>
<div> <div>
<a class="btn btn-alt" href="#login-new-device">Import key file</a> <a class="btn btn-alt" href="#login-new-device">Import key file</a>
<button class="btn" wo-touch="goForward()">Confirm token</button> <button class="btn" wo-touch="goForward()">Confirm token</button>
@ -20,12 +24,14 @@
<div class="step" ng-show="step === 2"> <div class="step" ng-show="step === 2">
<p><b>Key sync.</b> Please enter the keychain code you wrote down during sync setup.</p> <p><b>Key sync.</b> Please enter the keychain code you wrote down during sync setup.</p>
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code0" focus-me="step === 2" focus-next ng-paste="handlePaste($event)"> - <div class="code">
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code1" focus-next> - <input type="text" class="input-text" size="4" maxlength="4" ng-model="code0" focus-me="step === 2" focus-next ng-paste="handlePaste($event)"> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code2" focus-next> - <input type="text" class="input-text" size="4" maxlength="4" ng-model="code1" focus-next> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code3" focus-next> - <input type="text" class="input-text" size="4" maxlength="4" ng-model="code2" focus-next> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code4" focus-next> - <input type="text" class="input-text" size="4" maxlength="4" ng-model="code3" focus-next> -
<input type="text" class="input-text code" size="4" maxlength="4" ng-model="code5"> <input type="text" class="input-text" size="4" maxlength="4" ng-model="code4" focus-next> -
<input type="text" class="input-text" size="4" maxlength="4" ng-model="code5">
</div>
<!--<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>--> <!--<a href="https://whiteout.io/revocation.html" title="Click here to reset your account." target="_blank">Lost your keychain code?</a>-->
<div> <div>

View File

@ -37,7 +37,7 @@
</div><!--/.settings--> </div><!--/.settings-->
</div><!--/.details--> </div><!--/.details-->
<div ng-show="busy"> <div class="working" ng-show="busy">
<span class="spinner"></span> <span class="spinner"></span>
</div> </div>

View File

@ -123,12 +123,8 @@ define(function(require) {
}); });
}); });
describe('confirm passphrase', function() { describe('setPassphrase', function() {
var setStateStub;
it('should not continue if terms are not accepted', function(done) { it('should not continue if terms are not accepted', function(done) {
scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase;
scope.state.agree = undefined; scope.state.agree = undefined;
scope.onError = function(err) { scope.onError = function(err) {
@ -136,13 +132,28 @@ define(function(require) {
done(); done();
}; };
scope.confirmPassphrase(); scope.setPassphrase();
}); });
it('should continue', function(done) {
scope.state.agree = true;
var setStateStub = sinon.stub(scope, 'setState', function(state) {
expect(setStateStub.calledOnce).to.be.true;
expect(state).to.equal(2);
done();
});
scope.setPassphrase();
});
});
describe('confirm passphrase', function() {
var setStateStub;
it('should unlock crypto', function(done) { it('should unlock crypto', function(done) {
scope.state.passphrase = passphrase; scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase; scope.state.confirmation = passphrase;
scope.state.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: passphrase passphrase: passphrase
@ -168,7 +179,6 @@ define(function(require) {
it('should not work when keypair generation fails', function(done) { it('should not work when keypair generation fails', function(done) {
scope.state.passphrase = passphrase; scope.state.passphrase = passphrase;
scope.state.confirmation = passphrase; scope.state.confirmation = passphrase;
scope.state.agree = true;
emailDaoMock.unlock.withArgs({ emailDaoMock.unlock.withArgs({
passphrase: passphrase passphrase: passphrase
@ -176,9 +186,9 @@ define(function(require) {
setStateStub = sinon.stub(scope, 'setState', function(state) { setStateStub = sinon.stub(scope, 'setState', function(state) {
if (setStateStub.calledOnce) { if (setStateStub.calledOnce) {
expect(state).to.equal(2); expect(state).to.equal(3);
} else if (setStateStub.calledTwice) { } else if (setStateStub.calledTwice) {
expect(state).to.equal(1); expect(state).to.equal(2);
expect(emailDaoMock.unlock.calledOnce).to.be.true; expect(emailDaoMock.unlock.calledOnce).to.be.true;
scope.setState.restore(); scope.setState.restore();
} }