diff --git a/src/index.html b/src/index.html index 74de1ae..0771090 100644 --- a/src/index.html +++ b/src/index.html @@ -6,10 +6,6 @@ - - diff --git a/src/js/app-config.js b/src/js/app-config.js index 41e3d2f..5224acb 100644 --- a/src/js/app-config.js +++ b/src/js/app-config.js @@ -32,12 +32,26 @@ define(function(require) { app.config = { cloudUrl: cloudUrl || 'https://keys.whiteout.io', privkeyServerUrl: keychainUrl || 'https://keychain.whiteout.io', + adminUrl: 'https://admin-node.whiteout.io', + wmailDomain: 'wmail.io', serverPrivateKeyId: 'EE342F0DDBB0F3BE', symKeySize: 256, symIvSize: 96, asymKeySize: 2048, workerPath: 'js', reconnectInterval: 10000, + wmail: { + imap: { + host: 'imap.wmail.io', + port: 993, + secure: true + }, + smtp: { + host: 'smtp.wmail.io', + port: 465, + secure: true + } + }, gmail: { clientId: clientId || '440907777130.apps.googleusercontent.com', imap: { diff --git a/src/js/app-controller.js b/src/js/app-controller.js index 71d4d80..3011948 100644 --- a/src/js/app-controller.js +++ b/src/js/app-controller.js @@ -17,6 +17,7 @@ define(function(require) { RestDAO = require('js/dao/rest-dao'), appConfig = require('js/app-config'), EmailDAO = require('js/dao/email-dao'), + AdminDao = require('js/dao/admin-dao'), KeychainDAO = require('js/dao/keychain-dao'), PublicKeyDAO = require('js/dao/publickey-dao'), LawnchairDAO = require('js/dao/lawnchair-dao'), @@ -102,6 +103,7 @@ define(function(require) { self._emailDao = emailDao = new EmailDAO(keychain, pgp, userStorage, pgpbuilder, mailreader); self._outboxBo = new OutboxBO(emailDao, keychain, userStorage); self._updateHandler = new UpdateHandler(appConfigStore, userStorage, auth); + self._adminDao = new AdminDao(new RestDAO(config.adminUrl)); emailDao.onError = self.onError; }; diff --git a/src/js/controller/add-account.js b/src/js/controller/add-account.js index 2530a41..ac1e982 100644 --- a/src/js/controller/add-account.js +++ b/src/js/controller/add-account.js @@ -1,7 +1,8 @@ define(function(require) { 'use strict'; - var appCtrl = require('js/app-controller'); + var appCtrl = require('js/app-controller'), + cfg = require('js/app-config').config; var AddAccountCtrl = function($scope, $location, $routeParams) { if (!appCtrl._auth && !$routeParams.dev) { @@ -9,6 +10,57 @@ define(function(require) { return; } + $scope.step = 1; + + $scope.goTo = function(step) { + $scope.step = step; + }; + + $scope.createWhiteoutAccount = function() { + if ($scope.form.$invalid) { + return; + } + + $scope.busy = true; + $scope.errMsg = undefined; // reset error msg + $scope.emailAddress = $scope.user + '@' + cfg.wmailDomain; + + // call REST api + appCtrl._adminDao.createUser({ + emailAddress: $scope.emailAddress, + password: $scope.pass, + //phone: $scope.phone, + //betaCode: $scope.betaCode + }, function(err) { + if (err) { + $scope.busy = false; + $scope.errMsg = err.errMsg || err.message; + $scope.$apply(); + return; + } + + // proceed to login + $scope.login(); + }); + }; + + $scope.login = function() { + // store credentials in memory + appCtrl._auth.setCredentials({ + provider: 'wmail', + emailAddress: $scope.emailAddress, + username: $scope.emailAddress, + realname: $scope.realname, + password: $scope.pass, + imap: cfg.wmail.imap, + smtp: cfg.wmail.smtp + }); + + // proceed to login and keygen + $location.path('/login'); + $scope.$apply(); + }; + $scope.connectToGoogle = function() { // test for oauth support if (appCtrl._auth._oauth.isSupported()) { @@ -31,39 +83,9 @@ define(function(require) { }); }; - $scope.connectToYahoo = function() { + $scope.connectTo = function(provider) { $location.path('/login-set-credentials').search({ - provider: 'yahoo' - }); - }; - - $scope.connectToTonline = function() { - $location.path('/login-set-credentials').search({ - provider: 'tonline' - }); - }; - - $scope.connectToOutlook = function() { - $location.path('/login-set-credentials').search({ - provider: 'outlook' - }); - }; - - $scope.connectToGmx = function() { - $location.path('/login-set-credentials').search({ - provider: 'gmx' - }); - }; - - $scope.connectToWebde = function() { - $location.path('/login-set-credentials').search({ - provider: 'webde' - }); - }; - - $scope.connectOther = function() { - $location.path('/login-set-credentials').search({ - provider: 'custom' + provider: provider }); }; }; diff --git a/src/js/dao/admin-dao.js b/src/js/dao/admin-dao.js new file mode 100644 index 0000000..79ded1b --- /dev/null +++ b/src/js/dao/admin-dao.js @@ -0,0 +1,38 @@ +define(function() { + 'use strict'; + + var AdminDAO = function(restDao) { + this._restDao = restDao; + }; + + /** + * Create a new email account. + * @param {String} options.emailAddress The desired email address + * @param {String} options.password The password to be used for the account. + * @param {String} options.phone The user's mobile phone number (required for verification and password reset). + * @param {Function} callback(error) + */ + AdminDAO.prototype.createUser = function(options, callback) { + var uri; + + if (!options.emailAddress || !options.password /* || !options.phone*/ ) { + callback(new Error('Incomplete arguments!')); + return; + } + + uri = '/user'; + this._restDao.post(options, uri, function(err) { + if (err && err.code === 409) { + callback(new Error('User name is already taken!')); + return; + } else if (err) { + callback(new Error('Error creating new user!')); + return; + } + + callback(); + }); + }; + + return AdminDAO; +}); \ No newline at end of file diff --git a/src/manifest.json b/src/manifest.json index fb83e65..aec0803 100644 --- a/src/manifest.json +++ b/src/manifest.json @@ -17,6 +17,7 @@ "notifications", "https://keys-test.whiteout.io/", "https://keychain-test.whiteout.io/", + "https://admin-node.whiteout.io/", "https://www.googleapis.com/", "identity", { "socket": [ diff --git a/src/sass/views/_add-account.scss b/src/sass/views/_add-account.scss index 7f70f72..a6be8e9 100644 --- a/src/sass/views/_add-account.scss +++ b/src/sass/views/_add-account.scss @@ -1,144 +1,191 @@ .view-add-account { - height: 100%; - background-color: $color-grey-lightest; - color: $color-grey-dark; - overflow-y: auto; - h1 { - padding: 60px 0 50px 0; - text-align: center; - margin: 0; - font-size: 32px; - line-height: 64px; + .choose { + .choice { + padding: 20px; - @include respond-to(mobile) { - padding: 30px 0 20px 0; + p { + margin-top: 0; + } + } + + hr { + margin: 30px 0; + color: $color-grey-lighter; } } - ul { - list-style-type: none; - width: 320px; - margin: 0 auto 30px auto; - padding: 0; - border-width: 1px; - border-style: solid; - border-color: $color-grey-lighter; + .create-account { + div.flex { + width: 100%; + margin: 0; + display: flex; - li { - position: relative; - height: 68px; - cursor: pointer; - &:hover, - &:focus { - opacity: 0.6; - } - &:active { - opacity: 1; - top: 1px; - left: 1px; + input[type="text"].wmail { + flex-grow: 1; } - &:nth-child(odd) { - background-color: white; - } - - &:nth-child(even) { - background-color: $color-grey-lightest; + .domain { + font-size: $font-size-bigger; + margin: 0; + height: 38px; + line-height: 38px; + flex-shrink: 0; + + @include respond-to(mobile) { + font-size: $font-size-big; + } } - &.google { - div { - width: 164px; - height: 58px; - margin: 0px auto; - padding: 8px 0; + input[type="password"] { + flex: 1; + } - img { + input[type="password"].right { + flex: 1; + margin-right: 0; + } + } + } + + .providers { + h1 { + padding: 60px 0 50px 0; + text-align: center; + margin: 0; + font-size: 32px; + line-height: 64px; + + @include respond-to(mobile) { + padding: 30px 0 20px 0; + } + } + + ul { + list-style-type: none; + max-width: 320px; + width: 100%; + margin: 0 auto 30px auto; + padding: 0; + border-width: 1px; + border-style: solid; + border-color: $color-grey-lighter; + + li { + position: relative; + height: 68px; + cursor: pointer; + &:hover, + &:focus { + opacity: 0.6; + } + &:active { + opacity: 1; + top: 1px; + left: 1px; + } + + &:nth-child(odd) { + background-color: white; + } + + &:nth-child(even) { + background-color: $color-grey-lightest; + } + + &.google { + div { width: 164px; height: 58px; + margin: 0px auto; + padding: 8px 0; + + img { + width: 164px; + height: 58px; + } } } - } - &.whiteout { - div { - width: 210px; - margin: 0 auto; - padding: 15px 0; + &.whiteout { + div { + width: 210px; + margin: 0 auto; + padding: 15px 0; - img { - width: 100%; + img { + width: 100%; + } } } - } - &.yahoo { - div { - width: 181px; - margin: 0 auto; + &.yahoo { + div { + width: 181px; + margin: 0 auto; - img { - width: 100%; + img { + width: 100%; + } } } - } - &.tonline { - div { - width: 271px; - margin: 0 auto; + &.tonline { + div { + width: 271px; + margin: 0 auto; - img { - width: 100%; + img { + width: 100%; + } } } - } - &.outlook { - div { - width: 256px; - margin: 0 auto; - padding: 13px 0; + &.outlook { + div { + width: 256px; + margin: 0 auto; + padding: 13px 0; - img { - width: 100%; + img { + width: 100%; + } } } - } - &.gmx { - div { - width: 115px; - margin: 0 auto; - padding: 13px 0; - - img { + &.gmx { + div { width: 115px; - height: 37px; + margin: 0 auto; + padding: 13px 0; + + img { + width: 115px; + height: 37px; + } } } - } - &.webde { - div { - width: 137px; - margin: 0 auto; - padding: 17px 0; - - img { + &.webde { + div { width: 137px; - height: 31px; + margin: 0 auto; + padding: 17px 0; + + img { + width: 137px; + height: 31px; + } } } - } - &.other { - h3 { - margin: 0; - line-height: 68px; - font-size: 21px; - text-align: center; + &.other { + h3 { + margin: 0; + line-height: 68px; + font-size: 21px; + text-align: center; + } } } } diff --git a/src/sass/views/_dialog.scss b/src/sass/views/_dialog.scss index 370230e..40ee77c 100644 --- a/src/sass/views/_dialog.scss +++ b/src/sass/views/_dialog.scss @@ -14,6 +14,10 @@ .control { button { min-width: 100px; + + &.ng-animate { + transition: none; + } } } } \ No newline at end of file diff --git a/src/sass/views/_login.scss b/src/sass/views/_login.scss index ddc8558..bbdd69f 100644 --- a/src/sass/views/_login.scss +++ b/src/sass/views/_login.scss @@ -69,6 +69,7 @@ margin-right: 10px; } label, + input[type="tel"], input[type="text"], input[type="email"], input[type="number"], diff --git a/src/tpl/add-account.html b/src/tpl/add-account.html index 33fe96e..fd4e764 100644 --- a/src/tpl/add-account.html +++ b/src/tpl/add-account.html @@ -1,33 +1,86 @@ -
+
-

Select email account

+
+ -
    -
  • -
    Whiteout Mailbox
    -
  • -
  • -
    Google Mail
    -
  • -
  • -
    Outlook.com
    -
  • -
  • -
    Yahoo! Mail
    -
  • -
  • -
    GMX.net
    -
  • -
  • -
    Web.de
    -
  • -
  • -
    T-Online
    -
  • -
  • -

    Custom server...

    -
  • -
+
+
+

Create Whiteout account. Create a new fully encrypted Whiteout Mailbox.

+ +
+
+
+

Login to IMAP account. Connect Whiteout Mail to any existing email account via IMAP.

+ +
+
+
+ +
+ + + +
+ +
+

Select account

+ +
    +
  • +
    Whiteout Mailbox
    +
  • +
  • +
    Google Mail
    +
  • +
  • +
    Outlook.com
    +
  • +
  • +
    Yahoo! Mail
    +
  • +
  • +
    GMX.net
    +
  • +
  • +
    Web.de
    +
  • +
  • +
    T-Online
    +
  • +
  • +

    Custom server...

    +
  • +
+
@@ -44,7 +97,7 @@
-
Whiteout Mailbox (coming soon)
+
Whiteout Mailbox

Connect Whiteout Mail to your fully encrypted Whiteout Mailbox (hosted in Europe).

Incoming cleartext messages are encrypted with your public PGP key before being stored in your inbox.

diff --git a/test/unit/add-account-ctrl-test.js b/test/unit/add-account-ctrl-test.js index 00fe15f..31639e2 100644 --- a/test/unit/add-account-ctrl-test.js +++ b/test/unit/add-account-ctrl-test.js @@ -6,15 +6,17 @@ define(function(require) { mocks = require('angularMocks'), AddAccountCtrl = require('js/controller/add-account'), Auth = require('js/bo/auth'), + AdminDao = require('js/dao/admin-dao'), appController = require('js/app-controller'); describe('Add Account Controller unit test', function() { - var scope, location, ctrl, authStub, origAuth; + var scope, location, ctrl, authStub, origAuth, adminStub; beforeEach(function() { // remember original module to restore later, then replace it origAuth = appController._auth; appController._auth = authStub = sinon.createStubInstance(Auth); + appController._adminDao = adminStub = sinon.createStubInstance(AdminDao); angular.module('addaccounttest', []); mocks.module('addaccounttest'); @@ -22,6 +24,7 @@ define(function(require) { location = $location; scope = $rootScope.$new(); scope.state = {}; + scope.form = {}; sinon.stub(location, 'path').returns(location); sinon.stub(location, 'search').returns(location); @@ -41,11 +44,61 @@ define(function(require) { location.path.restore(); location.search.restore(); - scope.$apply.restore(); + if (scope.$apply.restore) { + scope.$apply.restore(); + } + }); + + describe('createWhiteoutAccount', function() { + it('should return early for invalid form', function() { + scope.form.$invalid = true; + scope.createWhiteoutAccount(); + expect(adminStub.createUser.called).to.be.false; + }); + + it('should fail to error creating user', function(done) { + scope.form.$invalid = false; + adminStub.createUser.yieldsAsync(new Error('asdf')); + + scope.$apply = function() { + expect(scope.busy).to.be.false; + expect(scope.errMsg).to.equal('asdf'); + expect(adminStub.createUser.calledOnce).to.be.true; + done(); + }; + + scope.createWhiteoutAccount(); + expect(scope.busy).to.be.true; + }); + + it('should work', function(done) { + scope.form.$invalid = false; + adminStub.createUser.yieldsAsync(); + + scope.login = function() { + expect(scope.busy).to.be.true; + expect(scope.errMsg).to.be.undefined; + expect(adminStub.createUser.calledOnce).to.be.true; + done(); + }; + + scope.createWhiteoutAccount(); + expect(scope.busy).to.be.true; + }); + }); + + describe('login', function() { + it('should work', function() { + scope.form.$invalid = false; + authStub.setCredentials.returns(); + + scope.login(); + expect(authStub.setCredentials.calledOnce).to.be.true; + expect(location.path.calledWith('/login')).to.be.true; + }); }); describe('connectToGoogle', function() { - it('should forward to login', function() { authStub._oauth = { isSupported: function() { @@ -101,70 +154,17 @@ define(function(require) { }); }); - describe('connectToYahoo', function() { + describe('connectTo', function() { it('should forward to login', function() { - scope.connectToYahoo(); + var provider = 'wmail'; + scope.connectTo(provider); expect(location.path.calledWith('/login-set-credentials')).to.be.true; expect(location.search.calledWith({ - provider: 'yahoo' + provider: provider })).to.be.true; }); }); - describe('connectToTonline', function() { - it('should forward to login', function() { - scope.connectToTonline(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'tonline' - })).to.be.true; - }); - }); - - describe('connectToOutlook', function() { - it('should forward to login', function() { - scope.connectToOutlook(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'outlook' - })).to.be.true; - }); - }); - - describe('connectToGmx', function() { - it('should forward to login', function() { - scope.connectToGmx(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'gmx' - })).to.be.true; - }); - }); - - describe('connectToWebde', function() { - it('should forward to login', function() { - scope.connectToWebde(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'webde' - })).to.be.true; - }); - }); - - describe('connectOther', function() { - it('should forward to login', function() { - scope.connectOther(); - - expect(location.path.calledWith('/login-set-credentials')).to.be.true; - expect(location.search.calledWith({ - provider: 'custom' - })).to.be.true; - }); - }); }); }); \ No newline at end of file diff --git a/test/unit/admin-dao-test.js b/test/unit/admin-dao-test.js new file mode 100644 index 0000000..708b1b5 --- /dev/null +++ b/test/unit/admin-dao-test.js @@ -0,0 +1,83 @@ +define(function(require) { + 'use strict'; + + var RestDAO = require('js/dao/rest-dao'), + AdminDAO = require('js/dao/admin-dao'), + expect = chai.expect; + + describe('Admin DAO unit tests', function() { + + var adminDao, restDaoStub, + emailAddress = 'test@example.com', + password = 'secret'; + + beforeEach(function() { + restDaoStub = sinon.createStubInstance(RestDAO); + adminDao = new AdminDAO(restDaoStub); + }); + + afterEach(function() {}); + + describe('createUser', function() { + it('should fail due to incomplete args', function(done) { + var opt = { + emailAddress: emailAddress + }; + + adminDao.createUser(opt, function(err) { + expect(err).to.exist; + done(); + }); + }); + + it('should fail if user already exists', function(done) { + var opt = { + emailAddress: emailAddress, + password: password + }; + + restDaoStub.post.withArgs(opt, '/user').yields({ + code: 409 + }); + + adminDao.createUser(opt, function(err) { + expect(err.message).to.contain('already taken'); + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + + it('should fail due to unknown error', function(done) { + var opt = { + emailAddress: emailAddress, + password: password + }; + + restDaoStub.post.withArgs(opt, '/user').yields(new Error()); + + adminDao.createUser(opt, function(err) { + expect(err).to.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + + it('should work', function(done) { + var opt = { + emailAddress: emailAddress, + password: password + }; + + restDaoStub.post.withArgs(opt, '/user').yields(); + + adminDao.createUser(opt, function(err) { + expect(err).to.not.exist; + expect(restDaoStub.post.calledOnce).to.be.true; + done(); + }); + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/unit/main.js b/test/unit/main.js index 63c494d..e51935e 100644 --- a/test/unit/main.js +++ b/test/unit/main.js @@ -79,6 +79,7 @@ function startTests() { 'test/unit/crypto-test', 'test/unit/backbutton-handler-test', 'test/unit/rest-dao-test', + 'test/unit/admin-dao-test', 'test/unit/publickey-dao-test', 'test/unit/privatekey-dao-test', 'test/unit/lawnchair-dao-test',