IGNITE-9941 Web Console: Added configuration parameter to disable signup and 
possibility to create user accounts by administrator.


Project: http://git-wip-us.apache.org/repos/asf/ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/fe824a0e
Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/fe824a0e
Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/fe824a0e

Branch: refs/heads/ignite-627
Commit: fe824a0e287f3b3432900357c3b09d939752ead7
Parents: 781d4e7
Author: Ilya Borisov <klast...@gmail.com>
Authored: Mon Oct 29 15:36:46 2018 +0700
Committer: Alexey Kuznetsov <akuznet...@apache.org>
Committed: Mon Oct 29 15:36:46 2018 +0700

----------------------------------------------------------------------
 modules/web-console/DEVNOTES.txt                |  17 ++
 modules/web-console/assembly/README.txt         |   1 +
 modules/web-console/backend/app/settings.js     |   4 +-
 .../backend/config/settings.json.sample         |   6 +-
 modules/web-console/backend/routes/public.js    |  22 +-
 modules/web-console/backend/services/mails.js   |  24 ++-
 modules/web-console/backend/services/users.js   |  12 +-
 modules/web-console/frontend/app/app.js         |   6 +-
 .../dialog-admin-create-user/component.ts       |  27 +++
 .../dialog-admin-create-user/controller.ts      |  78 +++++++
 .../dialog-admin-create-user/index.ts           |  23 ++
 .../dialog-admin-create-user/state.ts           |  29 +++
 .../dialog-admin-create-user/template.pug       |  37 ++++
 .../app/components/form-signup/component.ts     |  32 +++
 .../app/components/form-signup/controller.ts    |  46 ++++
 .../app/components/form-signup/index.ts         |  41 ++++
 .../app/components/form-signup/style.scss       |  31 +++
 .../app/components/form-signup/template.pug     | 105 +++++++++
 .../input-dialog/input-dialog.service.ts        |  12 +-
 .../input-dialog/input-dialog.tpl.pug           |  14 ++
 .../list-of-registered-users/column-defs.js     |  23 --
 .../list-of-registered-users/controller.js      | 214 ++++++++++---------
 .../list-of-registered-users/template.tpl.pug   |   1 -
 .../app/components/page-admin/controller.ts     |  28 +++
 .../frontend/app/components/page-admin/index.js |  17 +-
 .../app/components/page-signup/component.js     |   2 -
 .../app/components/page-signup/controller.js    |  76 -------
 .../app/components/page-signup/controller.ts    |  66 ++++++
 .../app/components/page-signup/index.js         |   3 +-
 .../app/components/page-signup/style.scss       |  10 -
 .../app/components/page-signup/template.pug     |  94 +-------
 .../app/components/page-signup/types.ts         |  36 ----
 .../frontend/app/modules/user/Auth.service.js   | 100 ---------
 .../frontend/app/modules/user/Auth.service.ts   |  90 ++++++++
 .../frontend/app/primitives/dropdown/index.pug  |   3 +-
 .../app/primitives/form-field/checkbox.pug      |   5 +-
 .../app/primitives/form-field/email.pug         |   7 +-
 .../app/primitives/form-field/password.pug      |  12 +-
 .../app/primitives/form-field/phone.pug         |   5 +-
 .../frontend/app/utils/dialogState.ts           |  56 +++++
 40 files changed, 934 insertions(+), 481 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/DEVNOTES.txt
----------------------------------------------------------------------
diff --git a/modules/web-console/DEVNOTES.txt b/modules/web-console/DEVNOTES.txt
index ba61150..a06608d 100644
--- a/modules/web-console/DEVNOTES.txt
+++ b/modules/web-console/DEVNOTES.txt
@@ -139,3 +139,20 @@ Unit tests are performed with Mocha framework - 
https://mochajs.org
 To launch tests on your local machine you will need:
 1. In new terminal change directory to 'modules/web-console/backend' folder 
and execute: "npm install".
 2. To start test environment and tests execute: "npm run test".
+
+
+Web Console settings
+====================
+Web Console backend could be configured with custom parameters.
+
+See "backend/config/settings.json.sample" for list of parameters with example 
value.
+
+If you need custom parameters, you will need create 
"backend/config/settings.json" and adjust values.
+
+Web Console settings for Docker
+===============================
+Web Console backend could be configured with custom parameters.
+
+You may pass custom parameters with help of "-e" option.
+
+For example: docker run -e "server_disable_signup=true" -p 9090:80 $IMAGE_ID

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/assembly/README.txt
----------------------------------------------------------------------
diff --git a/modules/web-console/assembly/README.txt 
b/modules/web-console/assembly/README.txt
index dd32399..f1e114c 100644
--- a/modules/web-console/assembly/README.txt
+++ b/modules/web-console/assembly/README.txt
@@ -47,6 +47,7 @@ All available parameters with defaults:
     HTTPS key:                  --server:key "serve/keys/test.key"
     HTTPS certificate:          --server:cert "serve/keys/test.crt"
     HTTPS passphrase:           --server:keyPassphrase "password"
+    Disable self registration:  --server:disable:signup false
     MongoDB URL:                --mongodb:url mongodb://localhost/console
     Mail service:               --mail:service "gmail"
     Signature text:             --mail:sign "Kind regards, Apache Ignite Team"

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/backend/app/settings.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/app/settings.js 
b/modules/web-console/backend/app/settings.js
index 104b66d..1c75041 100644
--- a/modules/web-console/backend/app/settings.js
+++ b/modules/web-console/backend/app/settings.js
@@ -68,7 +68,9 @@ module.exports = {
                     key: fs.readFileSync(nconf.get('server:key')),
                     cert: fs.readFileSync(nconf.get('server:cert')),
                     passphrase: nconf.get('server:keyPassphrase')
-                }
+                },
+                // eslint-disable-next-line eqeqeq
+                disableSignup: nconf.get('server:disable:signup') == 'true'
             },
             mail,
             mongoUrl: nconf.get('mongodb:url') || 
'mongodb://127.0.0.1/console',

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/backend/config/settings.json.sample
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/config/settings.json.sample 
b/modules/web-console/backend/config/settings.json.sample
index aa93e07..4070163 100644
--- a/modules/web-console/backend/config/settings.json.sample
+++ b/modules/web-console/backend/config/settings.json.sample
@@ -5,8 +5,10 @@
         "ssl": false,
         "key": "serve/keys/test.key",
         "cert": "serve/keys/test.crt",
-        "keyPassphrase": "password"
-    },
+        "keyPassphrase": "password",
+        "disable": {
+            "signup": false
+    }    },
     "mongodb": {
         "url": "mongodb://localhost/console"
     },

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/backend/routes/public.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/routes/public.js 
b/modules/web-console/backend/routes/public.js
index 155b2e9..8aa3b43 100644
--- a/modules/web-console/backend/routes/public.js
+++ b/modules/web-console/backend/routes/public.js
@@ -17,6 +17,7 @@
 
 'use strict';
 
+const _ = require('lodash');
 const express = require('express');
 const passport = require('passport');
 
@@ -49,15 +50,22 @@ module.exports.factory = function(mongo, mailsService, 
usersService, authService
          * Register new account.
          */
         router.post('/signup', (req, res) => {
-            usersService.create(req.origin(), req.body)
-                .then((user) => new Promise((resolve, reject) => {
-                    req.logIn(user, {}, (err) => {
-                        if (err)
-                            reject(err);
+            const createdByAdmin = _.get(req, 'user.admin', false);
 
-                        resolve(user);
+            usersService.create(req.origin(), req.body, createdByAdmin)
+                .then((user) => {
+                    if (createdByAdmin)
+                        return user;
+
+                    return new Promise((resolve, reject) => {
+                        req.logIn(user, {}, (err) => {
+                            if (err)
+                                reject(err);
+
+                            resolve(user);
+                        });
                     });
-                }))
+                })
                 .then(res.api.ok)
                 .catch(res.api.error);
         });

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/backend/services/mails.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/mails.js 
b/modules/web-console/backend/services/mails.js
index e0737cd..75da128 100644
--- a/modules/web-console/backend/services/mails.js
+++ b/modules/web-console/backend/services/mails.js
@@ -81,17 +81,27 @@ module.exports.factory = (settings) => {
         }
 
         /**
-         * Send email to user for password reset.
-         * @param host
-         * @param user
+         * Send email when user signed up.
+         *
+         * @param host Web Console host.
+         * @param user User that signed up.
+         * @param createdByAdmin Whether user was created by admin.
          */
-        emailUserSignUp(host, user) {
+        emailUserSignUp(host, user, createdByAdmin) {
             const resetLink = 
`${host}/password/reset?token=${user.resetPasswordToken}`;
 
-            return this.send(user, `Thanks for signing up for 
${settings.mail.greeting}.`,
+            const sbj = createdByAdmin
+                ? 'Account was created for'
+                : 'Thanks for signing up for';
+
+            const reason = createdByAdmin
+                ? 'administrator created account for you'
+                : 'you have signed up';
+
+            return this.send(user, `${sbj} ${settings.mail.greeting}.`,
                 `Hello ${user.firstName} ${user.lastName}!<br><br>` +
-                `You are receiving this email because you have signed up to 
use <a href="${host}">${settings.mail.greeting}</a>.<br><br>` +
-                'If you have not done the sign up and do not know what this 
email is about, please ignore it.<br>' +
+                `You are receiving this email because ${reason} to use <a 
href="${host}">${settings.mail.greeting}</a>.<br><br>` +
+                'If you do not know what this email is about, please ignore 
it.<br>' +
                 'You may reset the password by clicking on the following link, 
or paste this into your browser:<br><br>' +
                 `<a href="${resetLink}">${resetLink}</a>`);
         }

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/backend/services/users.js
----------------------------------------------------------------------
diff --git a/modules/web-console/backend/services/users.js 
b/modules/web-console/backend/services/users.js
index 43bceda..79578c5 100644
--- a/modules/web-console/backend/services/users.js
+++ b/modules/web-console/backend/services/users.js
@@ -42,17 +42,21 @@ module.exports.factory = (errors, settings, mongo, 
spacesService, mailsService,
         /**
          * Save profile information.
          *
-         * @param {String} host - The host
-         * @param {Object} user - The user
+         * @param {String} host - The host.
+         * @param {Object} user - The user.
+         * @param {Object} createdByAdmin - Whether user created by admin.
          * @returns {Promise.<mongo.ObjectId>} that resolves account id of 
merge operation.
          */
-        static create(host, user) {
+        static create(host, user, createdByAdmin) {
             return mongo.Account.count().exec()
                 .then((cnt) => {
                     user.admin = cnt === 0;
                     user.registered = new Date();
                     user.token = 
utilsService.randomString(settings.tokenLength);
 
+                    if (settings.server.disableSignup && !user.admin && 
!createdByAdmin)
+                        throw new errors.ServerErrorException('Sign-up is not 
allowed. Ask your Web Console administrator to create account for you.');
+
                     return new mongo.Account(user);
                 })
                 .then((created) => {
@@ -74,7 +78,7 @@ module.exports.factory = (errors, settings, mongo, 
spacesService, mailsService,
                     return registered.save()
                         .then(() => mongo.Space.create({name: 'Personal 
space', owner: registered._id}))
                         .then(() => {
-                            mailsService.emailUserSignUp(host, registered);
+                            mailsService.emailUserSignUp(host, registered, 
createdByAdmin);
 
                             return registered;
                         });

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/app.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/app.js 
b/modules/web-console/frontend/app/app.js
index c890fa4..47374aa 100644
--- a/modules/web-console/frontend/app/app.js
+++ b/modules/web-console/frontend/app/app.js
@@ -112,6 +112,7 @@ import id8 from './filters/id8.filter';
 
 // Components
 import igniteListOfRegisteredUsers from 
'./components/list-of-registered-users';
+import dialogAdminCreateUser from './components/dialog-admin-create-user';
 import IgniteActivitiesUserDialog from './components/activities-user-dialog';
 import './components/input-dialog';
 import webConsoleHeader from './components/web-console-header';
@@ -156,6 +157,7 @@ import pagePasswordReset from 
'./components/page-password-reset';
 import pageSignup from './components/page-signup';
 import pageSignin from './components/page-signin';
 import pageForgotPassword from './components/page-forgot-password';
+import formSignup from './components/form-signup';
 
 import igniteServices from './services';
 
@@ -245,6 +247,7 @@ export default angular.module('ignite-console', [
     connectedClusters.name,
     connectedClustersDialog.name,
     igniteListOfRegisteredUsers.name,
+    dialogAdminCreateUser.name,
     pageProfile.name,
     pageLanding.name,
     pagePasswordChanged.name,
@@ -260,7 +263,8 @@ export default angular.module('ignite-console', [
     igniteChartSelector.name,
     statusOutput.name,
     progressLine.name,
-    formField.name
+    formField.name,
+    formSignup.name
 ])
 .service('$exceptionHandler', $exceptionHandler)
 // Directives.

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/dialog-admin-create-user/component.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/dialog-admin-create-user/component.ts
 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/component.ts
new file mode 100644
index 0000000..3a60d93
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/component.ts
@@ -0,0 +1,27 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import {DialogAdminCreateUser} from './controller';
+
+export default {
+    template,
+    controller: DialogAdminCreateUser,
+    bindings: {
+        close: '&onHide'
+    }
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/dialog-admin-create-user/controller.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/dialog-admin-create-user/controller.ts
 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/controller.ts
new file mode 100644
index 0000000..e8a29e1
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/controller.ts
@@ -0,0 +1,78 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Auth from '../../modules/user/Auth.service';
+import MessagesFactory from '../../services/Messages.service';
+import FormUtilsFactoryFactory from '../../services/FormUtils.service';
+import LoadingServiceFactory from '../../modules/loading/loading.service';
+import {ISignupData} from '../form-signup';
+
+export class DialogAdminCreateUser {
+    close: ng.ICompiledExpression;
+
+    form: ng.IFormController;
+
+    data: ISignupData = {
+        email: null,
+        password: null,
+        firstName: null,
+        lastName: null,
+        company: null,
+        country: null
+    };
+
+    serverError: string | null = null;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils', 
'IgniteLoading'];
+
+    constructor(
+        private Auth: Auth,
+        private IgniteMessages: ReturnType<typeof MessagesFactory>,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactoryFactory>,
+        private loading: ReturnType<typeof LoadingServiceFactory>
+    ) {}
+
+    canSubmitForm(form: DialogAdminCreateUser['form']) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+
+    setServerError(error: DialogAdminCreateUser['serverError']) {
+        this.serverError = error;
+    }
+
+    createUser() {
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form))
+            return;
+
+        this.loading.start('createUser');
+
+        this.Auth.signup(this.data, false)
+            .then(() => {
+                this.IgniteMessages.showInfo(`User ${this.data.email} 
created`);
+                this.close({});
+            })
+            .catch((res) => {
+                this.loading.finish('createUser');
+                this.IgniteMessages.showError(null, res.data);
+                this.setServerError(res.data);
+            });
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/dialog-admin-create-user/index.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/dialog-admin-create-user/index.ts 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/index.ts
new file mode 100644
index 0000000..5a23368
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import component from './component';
+import {registerState} from './state';
+
+export default angular.module('ignite-console.dialog-admin-create-user', [])
+    .run(registerState)
+    .component('dialogAdminCreateUser', component);

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/dialog-admin-create-user/state.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/dialog-admin-create-user/state.ts 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/state.ts
new file mode 100644
index 0000000..c64238e
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/state.ts
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {UIRouter} from '@uirouter/angularjs';
+import {dialogState} from '../../utils/dialogState';
+
+registerState.$inject = ['$uiRouter'];
+
+export function registerState(router: UIRouter) {
+    router.stateRegistry.register({
+        ...dialogState('dialog-admin-create-user'),
+        name: 'base.settings.admin.createUser',
+        url: '/create-user'
+    });
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/dialog-admin-create-user/template.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/dialog-admin-create-user/template.pug
 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/template.pug
new file mode 100644
index 0000000..0a9f2b4
--- /dev/null
+++ 
b/modules/web-console/frontend/app/components/dialog-admin-create-user/template.pug
@@ -0,0 +1,37 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+.modal-dialog(ng-click='$event.stopPropagation()' )
+    form.modal-content(
+        name='$ctrl.form' novalidate
+        ignite-loading='createUser'
+        ignite-loading-text='Creating user…'
+    )
+        .modal-header
+            h4.modal-title Create User
+
+            button.close(type='button' aria-label='Close' 
ng-click='$ctrl.close()')
+                svg(ignite-icon="cross")
+        .modal-body
+            form-signup(
+                outer-form='$ctrl.form'
+                ng-model='$ctrl.data'
+                server-error='$ctrl.serverError'
+            )
+        .modal-footer
+            div
+                
button.btn-ignite.btn-ignite--link-success(ng-click='$ctrl.close()') Cancel
+                
button.btn-ignite.btn-ignite--success(ng-click='$ctrl.createUser()') Create user

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/form-signup/component.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/form-signup/component.ts 
b/modules/web-console/frontend/app/components/form-signup/component.ts
new file mode 100644
index 0000000..79863db
--- /dev/null
+++ b/modules/web-console/frontend/app/components/form-signup/component.ts
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import template from './template.pug';
+import './style.scss';
+import {FormSignup} from './controller';
+
+export const component: ng.IComponentOptions = {
+    template,
+    controller: FormSignup,
+    bindings: {
+        outerForm: '<',
+        serverError: '<'
+    },
+    require: {
+        ngModel: 'ngModel'
+    }
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/form-signup/controller.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/form-signup/controller.ts 
b/modules/web-console/frontend/app/components/form-signup/controller.ts
new file mode 100644
index 0000000..eedf6d0
--- /dev/null
+++ b/modules/web-console/frontend/app/components/form-signup/controller.ts
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import CountriesService from '../../services/Countries.service';
+import {ISignupFormController} from '.';
+
+export class FormSignup implements ng.IPostLink, ng.IOnDestroy, ng.IOnChanges {
+    static $inject = ['IgniteCountries'];
+
+    constructor(private Countries: ReturnType<typeof CountriesService>) {}
+
+    countries = this.Countries.getAll();
+
+    innerForm: ISignupFormController;
+    outerForm: ng.IFormController;
+    ngModel: ng.INgModelController;
+    serverError: string | null = null;
+
+    $postLink() {
+        this.outerForm.$addControl(this.innerForm);
+        this.innerForm.email.$validators.server = () => !this.serverError;
+    }
+
+    $onDestroy() {
+        this.outerForm.$removeControl(this.innerForm);
+    }
+
+    $onChanges(changes: {serverError: 
ng.IChangesObject<FormSignup['serverError']>}) {
+        if (changes.serverError && this.innerForm)
+            this.innerForm.email.$validate();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/form-signup/index.ts
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/form-signup/index.ts 
b/modules/web-console/frontend/app/components/form-signup/index.ts
new file mode 100644
index 0000000..1798aa0
--- /dev/null
+++ b/modules/web-console/frontend/app/components/form-signup/index.ts
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {component} from './component';
+
+export default angular.module('ignite-console.form-signup', [])
+    .component('formSignup', component);
+
+export interface ISignupData {
+    email: string,
+    password: string,
+    firstName: string,
+    lastName: string,
+    phone?: string,
+    company: string,
+    country: string
+}
+
+export interface ISignupFormController extends ng.IFormController {
+    email: ng.INgModelController,
+    password: ng.INgModelController,
+    firstName: ng.INgModelController,
+    lastName: ng.INgModelController,
+    phone: ng.INgModelController,
+    company: ng.INgModelController,
+    country: ng.INgModelController
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/form-signup/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/form-signup/style.scss 
b/modules/web-console/frontend/app/components/form-signup/style.scss
new file mode 100644
index 0000000..e80b510
--- /dev/null
+++ b/modules/web-console/frontend/app/components/form-signup/style.scss
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+form-signup {
+    .form-signup__grid {
+        display: grid;
+        grid-gap: 10px;
+        grid-template-columns: 1fr 1fr;
+
+        .span-1 {
+            grid-column: span 1;
+        }
+        .span-2 {
+            grid-column: span 2;
+        }
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/form-signup/template.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/form-signup/template.pug 
b/modules/web-console/frontend/app/components/form-signup/template.pug
new file mode 100644
index 0000000..375ea95
--- /dev/null
+++ b/modules/web-console/frontend/app/components/form-signup/template.pug
@@ -0,0 +1,105 @@
+//-
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+         http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+include /app/helpers/jade/mixins
+
+.form-signup__grid(ng-form='signup' ng-ref='$ctrl.innerForm' 
ng-ref-read='form')
+    .span-2
+        +form-field__email({
+            label: 'Email:',
+            model: '$ctrl.ngModel.$viewValue.email',
+            name: '"email"',
+            placeholder: 'Input email',
+            required: true
+        })(
+            ng-model-options='{allowInvalid: true}'
+            autocomplete='email'
+            ignite-auto-focus
+        )
+            +form-field__error({error: 'server', message: 
`{{$ctrl.serverError}}`})
+    .span-1
+        +form-field__password({
+            label: 'Password:',
+            model: '$ctrl.ngModel.$viewValue.password',
+            name: '"password"',
+            placeholder: 'Input password',
+            required: true
+        })(
+            autocomplete='new-password'
+        )
+    .span-1
+        +form-field__password({
+            label: 'Confirm:',
+            model: 'confirm',
+            name: '"confirm"',
+            placeholder: 'Confirm password',
+            required: true
+        })(
+            ignite-match='$ctrl.ngModel.$viewValue.password'
+            autocomplete='off'
+        )
+    .span-1
+        +form-field__text({
+            label: 'First name:',
+            model: '$ctrl.ngModel.$viewValue.firstName',
+            name: '"firstName"',
+            placeholder: 'Input first name',
+            required: true
+        })(
+            autocomplete='given-name'
+        )
+    .span-1
+        +form-field__text({
+            label: 'Last name:',
+            model: '$ctrl.ngModel.$viewValue.lastName',
+            name: '"lastName"',
+            placeholder: 'Input last name',
+            required: true
+        })(
+            autocomplete='family-name'
+        )
+    .span-1
+        +form-field__phone({
+            label: 'Phone:',
+            model: '$ctrl.ngModel.$viewValue.phone',
+            name: '"phone"',
+            placeholder: 'Input phone (ex.: +15417543010)',
+            optional: true
+        })(
+            autocomplete='tel'
+        )
+    .span-1
+        +form-field__dropdown({
+            label: 'Country:',
+            model: '$ctrl.ngModel.$viewValue.country',
+            name: '"country"',
+            required: true,
+            placeholder: 'Choose your country',
+            options: '$ctrl.countries'
+        })(
+            autocomplete='country'
+        )
+    .span-2
+        +form-field__text({
+            label: 'Company:',
+            model: '$ctrl.ngModel.$viewValue.company',
+            name: '"company"',
+            placeholder: 'Input company name',
+            required: true
+        })(
+            ignite-on-enter-focus-move='countryInput'
+            autocomplete='organization'
+        )

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.ts
 
b/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.ts
index 4d8ebd7..b2a241f 100644
--- 
a/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.ts
+++ 
b/modules/web-console/frontend/app/components/input-dialog/input-dialog.service.ts
@@ -20,7 +20,7 @@ import controller from './input-dialog.controller';
 import templateUrl from './input-dialog.tpl.pug';
 import {CancellationError} from 'app/errors/CancellationError';
 
-type InputModes = 'text' | 'number' | 'date' | 'time' | 'date-and-time';
+type InputModes = 'text' | 'number' | 'email' | 'date' | 'time' | 
'date-and-time';
 
 interface ValidationFunction<T> {
     (value: T): boolean
@@ -131,6 +131,16 @@ export default class InputDialog {
     }
 
     /**
+     * Open input dialog to configure custom e-mail.
+     *
+     * @param options Object with settings for rendering e-mail input.
+     * @return User input.
+     */
+    email(options: InputOptions<string>) {
+        return this.dialogFabric({mode: 'email', ...options});
+    }
+
+    /**
      * Open input dialog to configure custom date value.
      *
      * @param options Settings for rendering date input.

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/input-dialog/input-dialog.tpl.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/input-dialog/input-dialog.tpl.pug 
b/modules/web-console/frontend/app/components/input-dialog/input-dialog.tpl.pug
index 4674c97..d79cb56 100644
--- 
a/modules/web-console/frontend/app/components/input-dialog/input-dialog.tpl.pug
+++ 
b/modules/web-console/frontend/app/components/input-dialog/input-dialog.tpl.pug
@@ -39,6 +39,7 @@ include /app/helpers/jade/mixins
                             ignite-form-field-input-autofocus='true'
                             ignite-on-enter='form.$valid && ctrl.confirm()'
                         )
+
                 .row(ng-switch-when='number')
                     .col-100
                         +form-field__number({
@@ -53,6 +54,19 @@ include /app/helpers/jade/mixins
                             required: true
                         })
 
+                .row(ng-switch-when='email')
+                    .col-100
+                        +form-field__email({
+                            label: '{{ ctrl.options.label }}',
+                            model: 'ctrl.options.value',
+                            name: '"email"',
+                            required: true,
+                            placeholder: 'Input email'
+                        })(
+                            ignite-form-field-input-autofocus='true'
+                            ignite-on-enter='form.$valid && ctrl.confirm()'
+                        )
+
                 .row(ng-switch-when='date')
                     .col-100
                         .form-field--inline

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js
 
b/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js
index 419a063..2094f0c 100644
--- 
a/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js
+++ 
b/modules/web-console/frontend/app/components/list-of-registered-users/column-defs.js
@@ -25,34 +25,11 @@ const MODEL_HEADER_TEMPLATE = `<div 
class='ui-grid-cell-contents' bs-tooltip dat
 const CACHE_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip 
data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa 
fa-database'></i>${ICON_SORT}</div>`;
 const IGFS_HEADER_TEMPLATE = `<div class='ui-grid-cell-contents' bs-tooltip 
data-title='{{ col.headerTooltip(col) }}' data-placement='top'><i class='fa 
fa-folder-o'></i>${ICON_SORT}</div>`;
 
-const ACTIONS_TEMPLATE = `
-<div class='text-center ui-grid-cell-actions'>
-    <a class='btn btn-default dropdown-toggle' bs-dropdown='' 
data-placement='bottom-right' data-container='.panel'>
-        <i class='fa fa-gear'></i>&nbsp;
-        <span class='caret'></span>
-    </a>
-    <ul class='dropdown-menu' role='menu'>
-        <li ng-show='row.entity._id != $root.user._id'>
-            <a ng-click='grid.api.becomeUser(row.entity)'>Become this user</a>
-        </li>
-        <li ng-show='row.entity._id != $root.user._id'>
-            <a ng-click='grid.api.toggleAdmin(row.entity)' 
ng-if='row.entity.admin && row.entity._id !== $root.user._id'>Revoke admin</a>
-            <a ng-click='grid.api.toggleAdmin(row.entity)' 
ng-if='!row.entity.admin && row.entity._id !== $root.user._id'>Grant admin</a>
-        </li>
-        <li ng-show='row.entity._id != $root.user._id'>
-            <a ng-click='grid.api.removeUser(row.entity)'>Remove user</a>
-        </li>
-        <li>
-            <a ng-click='grid.api.showActivities(row.entity)'>Activity 
detail</a>
-        </li>
-</div>`;
-
 const EMAIL_TEMPLATE = '<div class="ui-grid-cell-contents"><a bs-tooltip 
data-title="{{ COL_FIELD }}" ng-href="mailto:{{ COL_FIELD }}">{{ COL_FIELD 
}}</a></div>';
 const DATE_WITH_TITLE = '<div class="ui-grid-cell-contents"><label bs-tooltip 
data-title="{{ COL_FIELD | date:\'M/d/yy HH:mm\' }}">{{ COL_FIELD | 
date:"M/d/yy HH:mm" }}</label></div>';
 const VALUE_WITH_TITLE = '<div class="ui-grid-cell-contents"><label bs-tooltip 
data-title="{{ COL_FIELD }}">{{ COL_FIELD }}</label></div>';
 
 export default [
-    {name: 'actions', enableHiding: false, displayName: 'Actions', 
categoryDisplayName: 'Actions', cellTemplate: ACTIONS_TEMPLATE, field: 
'actions', minWidth: 70, width: 70, enableFiltering: false, enableSorting: 
false, visible: false},
     {name: 'user', enableHiding: false, displayName: 'User', 
categoryDisplayName: 'User', field: 'userName', cellTemplate: USER_TEMPLATE, 
minWidth: 160, enableFiltering: true, pinnedLeft: true, filter: { placeholder: 
'Filter by name...' }},
     {name: 'email', displayName: 'Email', categoryDisplayName: 'Email', field: 
'email', cellTemplate: EMAIL_TEMPLATE, minWidth: 160, width: 220, 
enableFiltering: true, filter: { placeholder: 'Filter by email...' }},
     {name: 'company', displayName: 'Company', categoryDisplayName: 'Company', 
field: 'company', cellTemplate: VALUE_WITH_TITLE, minWidth: 180, 
enableFiltering: true, filter: { placeholder: 'Filter by company...' }},

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/list-of-registered-users/controller.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/list-of-registered-users/controller.js
 
b/modules/web-console/frontend/app/components/list-of-registered-users/controller.js
index f8b2797..6a7e098 100644
--- 
a/modules/web-console/frontend/app/components/list-of-registered-users/controller.js
+++ 
b/modules/web-console/frontend/app/components/list-of-registered-users/controller.js
@@ -15,11 +15,13 @@
  * limitations under the License.
  */
 
-import headerTemplate from 'app/primitives/ui-grid-header/index.tpl.pug';
+import _ from 'lodash';
 
 import columnDefs from './column-defs';
 import categories from './categories';
 
+import headerTemplate from 'app/primitives/ui-grid-header/index.tpl.pug';
+
 const rowTemplate = `<div
   ng-repeat="(colRenderIndex, col) in colContainer.renderedColumns track by 
col.uid"
   ui-grid-one-bind-id-grid="rowRenderIndex + '-' + col.uid + '-cell'"
@@ -36,120 +38,80 @@ export default class IgniteListOfRegisteredUsersCtrl {
     static $inject = ['$scope', '$state', '$filter', 'User', 
'uiGridGroupingConstants', 'uiGridPinningConstants', 'IgniteAdminData', 
'IgniteNotebookData', 'IgniteConfirm', 'IgniteActivitiesUserDialog'];
 
     constructor($scope, $state, $filter, User, uiGridGroupingConstants, 
uiGridPinningConstants, AdminData, NotebookData, Confirm, ActivitiesUserDialog) 
{
-        const $ctrl = this;
+        this.$state = $state;
+        this.AdminData = AdminData;
+        this.ActivitiesDialogFactory = ActivitiesUserDialog;
+        this.Confirm = Confirm;
+        this.User = User;
+        this.NotebookData = NotebookData;
 
         const dtFilter = $filter('date');
 
-        $ctrl.groupBy = 'user';
+        this.groupBy = 'user';
 
-        $ctrl.selected = [];
+        this.selected = [];
 
-        $ctrl.params = {
+        this.params = {
             startDate: new Date(),
             endDate: new Date()
         };
 
-        $ctrl.uiGridPinningConstants = uiGridPinningConstants;
-        $ctrl.uiGridGroupingConstants = uiGridGroupingConstants;
-
-        User.read().then((user) => $ctrl.user = user);
-
-        const becomeUser = () => {
-            const user = this.gridApi.selection.legacyGetSelectedRows()[0];
-
-            AdminData.becomeUser(user._id)
-                .then(() => User.load())
-                .then(() => $state.go('default-state'))
-                .then(() => NotebookData.load());
-        };
-
-        const removeUser = () => {
-            const user = this.gridApi.selection.legacyGetSelectedRows()[0];
-
-            Confirm.confirm(`Are you sure you want to remove user: 
"${user.userName}"?`)
-                .then(() => AdminData.removeUser(user))
-                .then(() => {
-                    const i = _.findIndex($ctrl.gridOptions.data, (u) => u._id 
=== user._id);
+        this.uiGridPinningConstants = uiGridPinningConstants;
+        this.uiGridGroupingConstants = uiGridGroupingConstants;
 
-                    if (i >= 0) {
-                        $ctrl.gridOptions.data.splice(i, 1);
-                        $ctrl.gridApi.selection.clearSelectedRows();
-                    }
-
-                    $ctrl.adjustHeight($ctrl.gridOptions.data.length);
-
-                    return $ctrl._refreshRows();
-                });
-        };
-
-        const toggleAdmin = () => {
-            const user = this.gridApi.selection.legacyGetSelectedRows()[0];
-
-            if (user.adminChanging)
-                return;
-
-            user.adminChanging = true;
-
-            AdminData.toggleAdmin(user)
-                .finally(() => {
-                    $ctrl._updateSelected();
-
-                    user.adminChanging = false;
-                });
-        };
-
-        const showActivities = () => {
-            const user = this.gridApi.selection.legacyGetSelectedRows()[0];
-
-            return new ActivitiesUserDialog({ user });
-        };
+        User.read().then((user) => this.user = user);
 
         const companiesExcludeFilter = (renderableRows) => {
-            if (_.isNil($ctrl.params.companiesExclude))
+            if (_.isNil(this.params.companiesExclude))
                 return renderableRows;
 
             _.forEach(renderableRows, (row) => {
-                row.visible = _.isEmpty($ctrl.params.companiesExclude) ||
-                    
row.entity.company.toLowerCase().indexOf($ctrl.params.companiesExclude.toLowerCase())
 === -1;
+                row.visible = _.isEmpty(this.params.companiesExclude) ||
+                    
row.entity.company.toLowerCase().indexOf(this.params.companiesExclude.toLowerCase())
 === -1;
             });
 
             return renderableRows;
         };
 
-        $ctrl.actionOptions = [
+        this.actionOptions = [
             {
                 action: 'Become this user',
-                click: becomeUser.bind(this),
+                click: () => this.becomeUser(),
                 available: true
             },
             {
                 action: 'Revoke admin',
-                click: toggleAdmin.bind(this),
+                click: () => this.toggleAdmin(),
                 available: true
             },
             {
                 action: 'Grant admin',
-                click: toggleAdmin.bind(this),
+                click: () => this.toggleAdmin(),
                 available: false
             },
             {
+                action: 'Add user',
+                sref: '.createUser',
+                available: true
+            },
+            {
                 action: 'Remove user',
-                click: removeUser.bind(this),
+                click: () => this.removeUser(),
                 available: true
             },
             {
                 action: 'Activity detail',
-                click: showActivities.bind(this),
+                click: () => this.showActivities(),
                 available: true
             }
         ];
 
-        $ctrl._userGridOptions = {
+        this._userGridOptions = {
             columnDefs,
             categories
         };
 
-        $ctrl.gridOptions = {
+        this.gridOptions = {
             data: [],
 
             columnDefs,
@@ -176,18 +138,18 @@ export default class IgniteListOfRegisteredUsersCtrl {
             rowIdentity: (row) => row._id,
             getRowIdentity: (row) => row._id,
             onRegisterApi: (api) => {
-                $ctrl.gridApi = api;
+                this.gridApi = api;
 
-                api.selection.on.rowSelectionChanged($scope, 
$ctrl._updateSelected.bind($ctrl));
-                api.selection.on.rowSelectionChangedBatch($scope, 
$ctrl._updateSelected.bind($ctrl));
+                api.selection.on.rowSelectionChanged($scope, 
this._updateSelected.bind(this));
+                api.selection.on.rowSelectionChangedBatch($scope, 
this._updateSelected.bind(this));
 
-                api.core.on.filterChanged($scope, 
$ctrl._filteredRows.bind($ctrl));
-                api.core.on.rowsVisibleChanged($scope, 
$ctrl._filteredRows.bind($ctrl));
+                api.core.on.filterChanged($scope, 
this._filteredRows.bind(this));
+                api.core.on.rowsVisibleChanged($scope, 
this._filteredRows.bind(this));
 
                 api.grid.registerRowsProcessor(companiesExcludeFilter, 50);
 
-                $scope.$watch(() => 
$ctrl.gridApi.grid.getVisibleRows().length, (rows) => $ctrl.adjustHeight(rows));
-                $scope.$watch(() => $ctrl.params.companiesExclude, () => 
$ctrl.gridApi.grid.refreshRows());
+                $scope.$watch(() => this.gridApi.grid.getVisibleRows().length, 
(rows) => this.adjustHeight(rows));
+                $scope.$watch(() => this.params.companiesExclude, () => 
this.gridApi.grid.refreshRows());
             }
         };
 
@@ -197,20 +159,20 @@ export default class IgniteListOfRegisteredUsersCtrl {
         const reloadUsers = (params) => {
             AdminData.loadUsers(params)
                 .then((data) => {
-                    $ctrl.gridOptions.data = data;
+                    this.gridOptions.data = data;
 
-                    $ctrl.companies = _.values(_.groupBy(data, 'company'));
-                    $ctrl.countries = _.values(_.groupBy(data, 'countryCode'));
+                    this.companies = _.values(_.groupBy(data, 'company'));
+                    this.countries = _.values(_.groupBy(data, 'countryCode'));
 
-                    $ctrl._refreshRows();
+                    this._refreshRows();
                 });
         };
 
         const filterDates = _.debounce(() => {
-            const sdt = $ctrl.params.startDate;
-            const edt = $ctrl.params.endDate;
+            const sdt = this.params.startDate;
+            const edt = this.params.endDate;
 
-            $ctrl.exporterCsvFilename = `web_console_users_${dtFilter(sdt, 
'yyyy_MM')}.csv`;
+            this.exporterCsvFilename = `web_console_users_${dtFilter(sdt, 
'yyyy_MM')}.csv`;
 
             const startDate = Date.UTC(sdt.getFullYear(), sdt.getMonth(), 1);
             const endDate = Date.UTC(edt.getFullYear(), edt.getMonth() + 1, 1);
@@ -218,8 +180,9 @@ export default class IgniteListOfRegisteredUsersCtrl {
             reloadUsers({ startDate, endDate });
         }, 250);
 
-        $scope.$watch(() => $ctrl.params.startDate, filterDates);
-        $scope.$watch(() => $ctrl.params.endDate, filterDates);
+        $scope.$on('userCreated', filterDates);
+        $scope.$watch(() => this.params.startDate, filterDates);
+        $scope.$watch(() => this.params.endDate, filterDates);
     }
 
     adjustHeight(rows) {
@@ -233,36 +196,95 @@ export default class IgniteListOfRegisteredUsersCtrl {
 
     _filteredRows() {
         const filtered = _.filter(this.gridApi.grid.rows, ({ visible}) => 
visible);
-        const entities = _.map(filtered, 'entity');
 
-        this.filteredRows = entities;
+        this.filteredRows = _.map(filtered, 'entity');
     }
 
     _updateSelected() {
         const ids = this.gridApi.selection.legacyGetSelectedRows().map(({ _id 
}) => _id).sort();
 
+        if (!_.isEqual(ids, this.selected))
+            this.selected = ids;
+
         if (ids.length) {
             const user = this.gridApi.selection.legacyGetSelectedRows()[0];
             const other = this.user._id !== user._id;
 
-            this.actionOptions[1].available = other && user.admin;
-            this.actionOptions[2].available = other && !user.admin;
-
-            this.actionOptions[0].available = other;
-            this.actionOptions[3].available = other;
+            this.actionOptions[0].available = other; // Become this user.
+            this.actionOptions[1].available = other && user.admin; // Revoke 
admin.
+            this.actionOptions[2].available = other && !user.admin; // Grant 
admin.
+            this.actionOptions[4].available = other; // Remove user.
+            this.actionOptions[5].available = true; // Activity detail.
+        }
+        else {
+            this.actionOptions[0].available = false; // Become this user.
+            this.actionOptions[1].available = false; // Revoke admin.
+            this.actionOptions[2].available = false; // Grant admin.
+            this.actionOptions[4].available = false; // Remove user.
+            this.actionOptions[5].available = false; // Activity detail.
         }
-
-        if (!_.isEqual(ids, this.selected))
-            this.selected = ids;
     }
 
     _refreshRows() {
         if (this.gridApi) {
             this.gridApi.grid.refreshRows()
-                .then(() => this.selected.length && this._updateSelected());
+                .then(() => this._updateSelected());
         }
     }
 
+    becomeUser() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        this.AdminData.becomeUser(user._id)
+            .then(() => this.User.load())
+            .then(() => this.$state.go('default-state'))
+            .then(() => this.NotebookData.load());
+    }
+
+    toggleAdmin() {
+        if (!this.gridApi)
+            return;
+
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        if (user.adminChanging)
+            return;
+
+        user.adminChanging = true;
+
+        this.AdminData.toggleAdmin(user)
+            .finally(() => {
+                this._updateSelected();
+
+                user.adminChanging = false;
+            });
+    }
+
+    removeUser() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        this.Confirm.confirm(`Are you sure you want to remove user: 
"${user.userName}"?`)
+            .then(() => this.AdminData.removeUser(user))
+            .then(() => {
+                const i = _.findIndex(this.gridOptions.data, (u) => u._id === 
user._id);
+
+                if (i >= 0) {
+                    this.gridOptions.data.splice(i, 1);
+                    this.gridApi.selection.clearSelectedRows();
+                }
+
+                this.adjustHeight(this.gridOptions.data.length);
+
+                return this._refreshRows();
+            });
+    }
+
+    showActivities() {
+        const user = this.gridApi.selection.legacyGetSelectedRows()[0];
+
+        return new this.ActivitiesDialogFactory({ user });
+    }
+
     groupByUser() {
         this.groupBy = 'user';
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/list-of-registered-users/template.tpl.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/list-of-registered-users/template.tpl.pug
 
b/modules/web-console/frontend/app/components/list-of-registered-users/template.tpl.pug
index d32b64d..ff1c851 100644
--- 
a/modules/web-console/frontend/app/components/list-of-registered-users/template.tpl.pug
+++ 
b/modules/web-console/frontend/app/components/list-of-registered-users/template.tpl.pug
@@ -74,7 +74,6 @@ ul.tabs.tabs--blue
                 label: 'Actions',
                 model: '$ctrl.action',
                 name: 'action',
-                disabled: '!$ctrl.selected.length',
                 options: '$ctrl.actionOptions'
             })
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-admin/controller.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-admin/controller.ts 
b/modules/web-console/frontend/app/components/page-admin/controller.ts
new file mode 100644
index 0000000..45a5c0a
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-admin/controller.ts
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import UserNotificationsService from '../user-notifications/service';
+
+export default class PageAdminCtrl {
+    static $inject = ['UserNotifications'];
+
+    constructor(private UserNotifications: UserNotificationsService) {}
+
+    changeUserNotifications() {
+        this.UserNotifications.editor();
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-admin/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-admin/index.js 
b/modules/web-console/frontend/app/components/page-admin/index.js
index 232d889..8b26f2c 100644
--- a/modules/web-console/frontend/app/components/page-admin/index.js
+++ b/modules/web-console/frontend/app/components/page-admin/index.js
@@ -17,23 +17,12 @@
 
 import './style.scss';
 
+import controller from './controller';
 import templateUrl from './template.tpl.pug';
 
-class PageAdminCtrl {
-    static $inject = ['UserNotifications'];
-
-    constructor(UserNotifications) {
-        this.UserNotifications = UserNotifications;
-    }
-
-    changeUserNotifications() {
-        this.UserNotifications.editor();
-    }
-}
-
 export default angular
     .module('ignite-console.page-admin', [])
     .component('pageAdmin', {
-        templateUrl,
-        controller: PageAdminCtrl
+        controller,
+        templateUrl
     });

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/component.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-signup/component.js 
b/modules/web-console/frontend/app/components/page-signup/component.js
index 789a681..968ff39 100644
--- a/modules/web-console/frontend/app/components/page-signup/component.js
+++ b/modules/web-console/frontend/app/components/page-signup/component.js
@@ -15,8 +15,6 @@
  * limitations under the License.
  */
 
-import angular from 'angular';
-
 import template from './template.pug';
 import controller from './controller';
 import './style.scss';

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/controller.js
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-signup/controller.js 
b/modules/web-console/frontend/app/components/page-signup/controller.js
deleted file mode 100644
index 1039776..0000000
--- a/modules/web-console/frontend/app/components/page-signup/controller.js
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export default class PageSignup {
-    /** @type {import('./types').ISignupFormController} */
-    form;
-    /** @type {import('./types').ISignupData} */
-    data = {
-        email: null,
-        password: null,
-        firstName: null,
-        lastName: null,
-        company: null,
-        country: null
-    };
-    /** @type {string} */
-    serverError = null;
-
-    static $inject = ['IgniteCountries', 'Auth', 'IgniteMessages', 
'IgniteFormUtils'];
-
-    /**
-     * @param {ReturnType<typeof 
import('app/services/Countries.service').default>} Countries
-     * @param {import('app/modules/user/Auth.service').default} Auth
-     * @param {ReturnType<typeof 
import('app/services/Messages.service').default>} IgniteMessages
-     * @param {ReturnType<typeof 
import('app/services/FormUtils.service').default>} IgniteFormUtils
-     */
-    constructor(Countries, Auth, IgniteMessages, IgniteFormUtils) {
-        this.Auth = Auth;
-        this.IgniteMessages = IgniteMessages;
-        this.countries = Countries.getAll();
-        this.IgniteFormUtils = IgniteFormUtils;
-    }
-
-    /** @param {import('./types').ISignupFormController} form */
-    canSubmitForm(form) {
-        return form.$error.server ? true : !form.$invalid;
-    }
-
-    $postLink() {
-        this.form.email.$validators.server = () => !this.serverError;
-    }
-
-    /** @param {string} error */
-    setServerError(error) {
-        this.serverError = error;
-        this.form.email.$validate();
-    }
-
-    signup() {
-        this.IgniteFormUtils.triggerValidation(this.form);
-
-        this.setServerError(null);
-
-        if (!this.canSubmitForm(this.form))
-            return;
-
-        return this.Auth.signnup(this.data).catch((res) => {
-            this.IgniteMessages.showError(null, res.data);
-            this.setServerError(res.data);
-        });
-    }
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/controller.ts
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-signup/controller.ts 
b/modules/web-console/frontend/app/components/page-signup/controller.ts
new file mode 100644
index 0000000..ddf77be
--- /dev/null
+++ b/modules/web-console/frontend/app/components/page-signup/controller.ts
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import Auth from '../../modules/user/Auth.service';
+import MessagesFactory from '../../services/Messages.service';
+import FormUtilsFactoryFactory from '../../services/FormUtils.service';
+import {ISignupData} from '../form-signup';
+
+export default class PageSignup {
+    form: ng.IFormController;
+
+    data: ISignupData = {
+        email: null,
+        password: null,
+        firstName: null,
+        lastName: null,
+        company: null,
+        country: null
+    };
+
+    serverError: string | null = null;
+
+    static $inject = ['Auth', 'IgniteMessages', 'IgniteFormUtils'];
+
+    constructor(
+        private Auth: Auth,
+        private IgniteMessages: ReturnType<typeof MessagesFactory>,
+        private IgniteFormUtils: ReturnType<typeof FormUtilsFactoryFactory>
+    ) {}
+
+    canSubmitForm(form: PageSignup['form']) {
+        return form.$error.server ? true : !form.$invalid;
+    }
+
+    setServerError(error: PageSignup['serverError']) {
+        this.serverError = error;
+    }
+
+    signup() {
+        this.IgniteFormUtils.triggerValidation(this.form);
+
+        this.setServerError(null);
+
+        if (!this.canSubmitForm(this.form))
+            return;
+
+        return this.Auth.signup(this.data).catch((res) => {
+            this.IgniteMessages.showError(null, res.data);
+            this.setServerError(res.data);
+        });
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/index.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-signup/index.js 
b/modules/web-console/frontend/app/components/page-signup/index.js
index 4efadb5..001d269 100644
--- a/modules/web-console/frontend/app/components/page-signup/index.js
+++ b/modules/web-console/frontend/app/components/page-signup/index.js
@@ -22,7 +22,8 @@ import {registerState} from './run';
 export default angular
     .module('ignite-console.page-signup', [
         'ui.router',
-        'ignite-console.user'
+        'ignite-console.user',
+        'ignite-console.form-signup'
     ])
     .component('pageSignup', component)
     .run(registerState);

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/style.scss
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-signup/style.scss 
b/modules/web-console/frontend/app/components/page-signup/style.scss
index 93167ce..98c4729 100644
--- a/modules/web-console/frontend/app/components/page-signup/style.scss
+++ b/modules/web-console/frontend/app/components/page-signup/style.scss
@@ -41,16 +41,6 @@ page-signup {
                 margin-left: auto;
             }
         }
-
-        form {
-            display: grid;
-            grid-gap: 10px;
-            grid-template-columns: 1fr 1fr;
-
-            .full-width {
-                grid-column: 1 / 3;
-            }
-        }
     }
 
     .page-signup__has-account-message {

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/template.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/components/page-signup/template.pug 
b/modules/web-console/frontend/app/components/page-signup/template.pug
index 11bb50a..5584dfc 100644
--- a/modules/web-console/frontend/app/components/page-signup/template.pug
+++ b/modules/web-console/frontend/app/components/page-signup/template.pug
@@ -22,95 +22,13 @@ web-console-header
 
 .container--responsive.body-container
     section
-        -var form = '$ctrl.form'
         h3 Don't Have An Account?
-        form(name=form novalidate ng-submit='$ctrl.signup()')
-            .full-width
-                +form-field__email({
-                    label: 'Email:',
-                    model: '$ctrl.data.email',
-                    name: '"email"',
-                    placeholder: 'Input email',
-                    required: true
-                })(
-                    ng-model-options='{allowInvalid: true}'
-                    autocomplete='email'
-                    ignite-auto-focus
-                )
-                    +form-field__error({error: 'server', message: 
`{{$ctrl.serverError}}`})
-            div
-                +form-field__password({
-                    label: 'Password:',
-                    model: '$ctrl.data.password',
-                    name: '"password"',
-                    placeholder: 'Input password',
-                    required: true
-                })(
-                    autocomplete='new-password'
-                )
-            div
-                +form-field__password({
-                    label: 'Confirm:',
-                    model: 'confirm',
-                    name: '"confirm"',
-                    placeholder: 'Confirm password',
-                    required: true
-                })(
-                    ignite-match='$ctrl.data.password'
-                    autocomplete='off'
-                )
-            div
-                +form-field__text({
-                    label: 'First name:',
-                    model: '$ctrl.data.firstName',
-                    name: '"firstName"',
-                    placeholder: 'Input first name',
-                    required: true
-                })(
-                    autocomplete='given-name'
-                )
-            div
-                +form-field__text({
-                    label: 'Last name:',
-                    model: '$ctrl.data.lastName',
-                    name: '"lastName"',
-                    placeholder: 'Input last name',
-                    required: true
-                })(
-                    autocomplete='family-name'
-                )
-            div
-                +form-field__phone({
-                    label: 'Phone:',
-                    model: '$ctrl.data.phone',
-                    name: '"phone"',
-                    placeholder: 'Input phone (ex.: +15417543010)',
-                    optional: true
-                })(
-                    autocomplete='tel'
-                )
-            div
-                +form-field__dropdown({
-                    label: 'Country:',
-                    model: '$ctrl.data.country',
-                    name: '"country"',
-                    required: true,
-                    placeholder: 'Choose your country',
-                    options: '$ctrl.countries'
-                })(
-                    autocomplete='country'
-                )
-            .full-width
-                +form-field__text({
-                    label: 'Company:',
-                    model: '$ctrl.data.company',
-                    name: '"company"',
-                    placeholder: 'Input company name',
-                    required: true
-                })(
-                    ignite-on-enter-focus-move='countryInput'
-                    autocomplete='organization'
-                )
+        form(name='$ctrl.form' novalidate ng-submit='$ctrl.signup()')
+            form-signup(
+                outer-form='$ctrl.form'
+                ng-model='$ctrl.data'
+                server-error='$ctrl.serverError'
+            )
             footer.full-width.form-footer
                 button.btn-ignite.btn-ignite--primary(
                     type='submit'

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/components/page-signup/types.ts
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/components/page-signup/types.ts 
b/modules/web-console/frontend/app/components/page-signup/types.ts
deleted file mode 100644
index 49e8d5d..0000000
--- a/modules/web-console/frontend/app/components/page-signup/types.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-export interface ISignupData {
-    email: string,
-    password: string,
-    firstName: string,
-    lastName: string,
-    phone?: string,
-    company: string,
-    country: string
-}
-
-export interface ISignupFormController extends ng.IFormController {
-    email: ng.INgModelController,
-    password: ng.INgModelController,
-    firstName: ng.INgModelController,
-    lastName: ng.INgModelController,
-    phone: ng.INgModelController,
-    company: ng.INgModelController,
-    country: ng.INgModelController
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/modules/user/Auth.service.js
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/Auth.service.js 
b/modules/web-console/frontend/app/modules/user/Auth.service.js
deleted file mode 100644
index 8c3cc4e..0000000
--- a/modules/web-console/frontend/app/modules/user/Auth.service.js
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/**
- * @typedef {object} SignupUserInfo
- * @prop {string} email
- * @prop {string} password
- * @prop {string} firstName
- * @prop {string} lastName
- * @prop {string} company
- * @prop {string} country
- */
-
-export default class AuthService {
-    static $inject = ['$http', '$rootScope', '$state', '$window', 
'IgniteMessages', 'gettingStarted', 'User'];
-    /**
-     * @param {ng.IHttpService} $http
-     * @param {ng.IRootScopeService} $root
-     * @param {import('@uirouter/angularjs').StateService} $state
-     * @param {ng.IWindowService} $window
-     * @param {ReturnType<typeof 
import('app/services/Messages.service').default>} Messages
-     * @param {ReturnType<typeof 
import('app/modules/getting-started/GettingStarted.provider').service>} 
gettingStarted
-     * @param {ReturnType<typeof import('./User.service').default>} User
-     */
-    constructor($http, $root, $state, $window, Messages, gettingStarted, User) 
{
-        this.$http = $http;
-        this.$root = $root;
-        this.$state = $state;
-        this.$window = $window;
-        this.Messages = Messages;
-        this.gettingStarted = gettingStarted;
-        this.User = User;
-    }
-    /**
-     * @param {SignupUserInfo} userInfo
-     */
-    signnup(userInfo) {
-        return this._auth('signup', userInfo);
-    }
-    /**
-     * @param {string} email
-     * @param {string} password
-     */
-    signin(email, password) {
-        return this._auth('signin', {email, password});
-    }
-    /**
-     * @param {string} email
-     */
-    remindPassword(email) {
-        return this._auth('password/forgot', {email}).then(() => 
this.$state.go('password.send'));
-    }
-
-    // TODO IGNITE-7994: Remove _auth and move API calls to corresponding 
methods
-    /**
-     * Performs the REST API call.
-     * @private
-     * @param {('signin'|'signup'|'password/forgot')} action
-     * @param {{email:string,password:string}|SignupUserInfo|{email:string}} 
userInfo
-     */
-    _auth(action, userInfo) {
-        return this.$http.post('/api/v1/' + action, userInfo)
-            .then(() => {
-                if (action === 'password/forgot')
-                    return;
-
-                this.User.read()
-                    .then((user) => {
-                        this.$root.$broadcast('user', user);
-
-                        this.$state.go('default-state');
-
-                        this.$root.gettingStarted.tryShow();
-                    });
-            });
-    }
-    logout() {
-        return this.$http.post('/api/v1/logout')
-            .then(() => {
-                this.User.clean();
-
-                this.$window.open(this.$state.href('signin'), '_self');
-            })
-            .catch((e) => this.Messages.showError(e));
-    }
-}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/modules/user/Auth.service.ts
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/modules/user/Auth.service.ts 
b/modules/web-console/frontend/app/modules/user/Auth.service.ts
new file mode 100644
index 0000000..55956ad
--- /dev/null
+++ b/modules/web-console/frontend/app/modules/user/Auth.service.ts
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {StateService} from '@uirouter/angularjs';
+import MessagesFactory from '../../services/Messages.service';
+import {service as GettingsStartedFactory} from 
'../../modules/getting-started/GettingStarted.provider';
+import UserServiceFactory from './User.service';
+
+type SignupUserInfo = {
+    email: string,
+    password: string,
+    firstName: string,
+    lastName: string,
+    company: string,
+    country: string,
+};
+
+type AuthActions = 'signin' | 'signup' | 'password/forgot';
+type AuthOptions = {email:string, 
password:string}|SignupUserInfo|{email:string};
+
+export default class AuthService {
+    static $inject = ['$http', '$rootScope', '$state', '$window', 
'IgniteMessages', 'gettingStarted', 'User'];
+
+    constructor(
+        private $http: ng.IHttpService,
+        private $root: ng.IRootScopeService,
+        private $state: StateService,
+        private $window: ng.IWindowService,
+        private Messages: ReturnType<typeof MessagesFactory>,
+        private gettingStarted: ReturnType<typeof GettingsStartedFactory>,
+        private User: ReturnType<typeof UserServiceFactory>
+    ) {}
+
+    signup(userInfo: SignupUserInfo, loginAfterSignup: boolean = true) {
+        return this._auth('signup', userInfo, loginAfterSignup);
+    }
+
+    signin(email: string, password: string) {
+        return this._auth('signin', {email, password});
+    }
+
+    remindPassword(email: string) {
+        return this._auth('password/forgot', {email}).then(() => 
this.$state.go('password.send'));
+    }
+
+    // TODO IGNITE-7994: Remove _auth and move API calls to corresponding 
methods
+    /**
+     * Performs the REST API call.
+     */
+    private _auth(action: AuthActions, userInfo: AuthOptions, loginAfterwards: 
boolean = true) {
+        return this.$http.post('/api/v1/' + action, userInfo)
+            .then(() => {
+                if (action === 'password/forgot')
+                    return;
+
+                this.User.read()
+                    .then((user) => {
+                        if (loginAfterwards) {
+                            this.$root.$broadcast('user', user);
+                            this.$state.go('default-state');
+                            this.$root.gettingStarted.tryShow();
+                        } else
+                            this.$root.$broadcast('userCreated');
+                    });
+            });
+    }
+    logout() {
+        return this.$http.post('/api/v1/logout')
+            .then(() => {
+                this.User.clean();
+
+                this.$window.open(this.$state.href('signin'), '_self');
+            })
+            .catch((e) => this.Messages.showError(e));
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/primitives/dropdown/index.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/dropdown/index.pug 
b/modules/web-console/frontend/app/primitives/dropdown/index.pug
index 0099457..aad6efd 100644
--- a/modules/web-console/frontend/app/primitives/dropdown/index.pug
+++ b/modules/web-console/frontend/app/primitives/dropdown/index.pug
@@ -37,5 +37,6 @@ mixin ignite-form-field-bsdropdown({label, model, name, 
disabled, required, opti
 
         ul.dropdown-menu(role='menu')
             li(ng-repeat=`item in ${options}` ng-if='item.available')
-                a(ng-click='item.click()') {{ item.action }}
+                a(ng-if='item.click' ng-click='item.click()') {{ item.action }}
+                a(ng-if='item.sref' ui-sref='{{:: item.sref}}') {{ item.action 
}}
 

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/primitives/form-field/checkbox.pug
----------------------------------------------------------------------
diff --git 
a/modules/web-console/frontend/app/primitives/form-field/checkbox.pug 
b/modules/web-console/frontend/app/primitives/form-field/checkbox.pug
index 88b8f5a..fe0f808 100644
--- a/modules/web-console/frontend/app/primitives/form-field/checkbox.pug
+++ b/modules/web-console/frontend/app/primitives/form-field/checkbox.pug
@@ -15,7 +15,7 @@
     limitations under the License.
 
 mixin form-field__checkbox({ label, model, name, disabled, required, tip, 
tipOpts })
-    .form-field.form-field__checkbox(id=`{{ ${name} }}Field`)
+    .form-field.ignite-form-field.form-field__checkbox(id=`{{ ${name} }}Field`)
         +form-field__label({ label, name, required, disabled })
             +form-field__tooltip({ title: tip, options: tipOpts })
 
@@ -24,7 +24,8 @@ mixin form-field__checkbox({ label, model, name, disabled, 
required, tip, tipOpt
             +form-field__input({ name, model, disabled, required, placeholder 
})(attributes=attributes)
 
         .form-field__errors(
-            ng-messages=`(${form}[${name}].$dirty || ${form}[${name}].$touched 
|| ${form}[${name}].$submitted) && ${form}[${name}].$invalid ? 
${form}[${name}].$error : {}`
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) 
&& $input.$invalid`
         )
             if block
                 block

http://git-wip-us.apache.org/repos/asf/ignite/blob/fe824a0e/modules/web-console/frontend/app/primitives/form-field/email.pug
----------------------------------------------------------------------
diff --git a/modules/web-console/frontend/app/primitives/form-field/email.pug 
b/modules/web-console/frontend/app/primitives/form-field/email.pug
index b68a520..5fb0c30 100644
--- a/modules/web-console/frontend/app/primitives/form-field/email.pug
+++ b/modules/web-console/frontend/app/primitives/form-field/email.pug
@@ -15,9 +15,9 @@
     limitations under the License.
 
 mixin form-field__email({ label, model, name, disabled, required, placeholder, 
tip })
-    -var errLbl = label.substring(0, label.length - 1)
+    -let errLbl = label[label.length - 1] === ':' ? label.substring(0, 
label.length - 1) : label
 
-    .form-field
+    .form-field.ignite-form-field
         +form-field__label({ label, name, required, disabled })
             +form-field__tooltip({ title: tip, options: tipOpts })
 
@@ -26,7 +26,8 @@ mixin form-field__email({ label, model, name, disabled, 
required, placeholder, t
             +form-field__input({ name, model, disabled, required, placeholder 
})(attributes=attributes)
 
         .form-field__errors(
-            ng-messages=`(${form}[${name}].$dirty || ${form}[${name}].$touched 
|| ${form}[${name}].$submitted) && ${form}[${name}].$invalid ? 
${form}[${name}].$error : {}`
+            ng-messages=`$input.$error`
+            ng-show=`($input.$dirty || $input.$touched || $input.$submitted) 
&& $input.$invalid`
         )
             if block
                 block

Reply via email to