You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2016/02/08 04:44:48 UTC

[2/3] ignite git commit: IGNITE-843 Refactored server side.

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/routes/igfs.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/routes/igfs.js b/modules/control-center-web/src/main/js/routes/igfs.js
deleted file mode 100644
index 2fad048..0000000
--- a/modules/control-center-web/src/main/js/routes/igfs.js
+++ /dev/null
@@ -1,143 +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.
- */
-
-var _ = require('lodash');
-var router = require('express').Router();
-var db = require('../db');
-
-/**
- * Get spaces and IGFSs accessed for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/list', function (req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
-        if (db.processed(err, res)) {
-            var space_ids = spaces.map(function (value) {
-                return value._id;
-            });
-
-            // Get all clusters for spaces.
-            db.Cluster.find({space: {$in: space_ids}}, '_id name').sort('name').exec(function (err, clusters) {
-                if (db.processed(err, res)) {
-                    // Get all IGFSs for spaces.
-                    db.Igfs.find({space: {$in: space_ids}}).sort('name').exec(function (err, igfss) {
-                        if (db.processed(err, res)) {
-                            _.forEach(igfss, function (igfs) {
-                                // Remove deleted clusters.
-                                igfs.clusters = _.filter(igfs.clusters, function (clusterId) {
-                                    return _.findIndex(clusters, function (cluster) {
-                                            return cluster._id.equals(clusterId);
-                                        }) >= 0;
-                                });
-                            });
-
-                            res.json({
-                                spaces: spaces,
-                                clusters: clusters.map(function (cluster) {
-                                    return {value: cluster._id, label: cluster.name};
-                                }),
-                                igfss: igfss
-                            });
-                        }
-                    });                }
-            });
-        }
-    });
-});
-
-/**
- * Save IGFS.
- */
-router.post('/save', function (req, res) {
-    var params = req.body;
-    var igfsId = params._id;
-    var clusters = params.clusters;
-
-    if (params._id) {
-        db.Igfs.update({_id: igfsId}, params, {upsert: true}, function (err) {
-            if (db.processed(err, res))
-                db.Cluster.update({_id: {$in: clusters}}, {$addToSet: {igfss: igfsId}}, {multi: true}, function (err) {
-                    if (db.processed(err, res))
-                        db.Cluster.update({_id: {$nin: clusters}}, {$pull: {igfss: igfsId}}, {multi: true}, function (err) {
-                            if (db.processed(err, res))
-                                res.send(params._id);
-                        });
-                });
-        })
-    }
-    else
-        db.Igfs.findOne({space: params.space, name: params.name}, function (err, igfs) {
-            if (db.processed(err, res)) {
-                if (igfs)
-                    return res.status(500).send('IGFS with name: "' + igfs.name + '" already exist.');
-
-                (new db.Igfs(params)).save(function (err, igfs) {
-                    if (db.processed(err, res)) {
-                        igfsId = igfs._id;
-
-                        db.Cluster.update({_id: {$in: clusters}}, {$addToSet: {igfss: igfsId}}, {multi: true}, function (err) {
-                            if (db.processed(err, res))
-                                res.send(igfsId);
-                        });
-                    }
-                });
-            }
-        });
-});
-
-/**
- * Remove IGFS by ._id.
- */
-router.post('/remove', function (req, res) {
-    db.Igfs.remove(req.body, function (err) {
-        if (db.processed(err, res))
-            res.sendStatus(200);
-    })
-});
-
-/**
- * Remove all IGFSs.
- */
-router.post('/remove/all', function (req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
-        if (db.processed(err, res)) {
-            var space_ids = spaces.map(function (value) {
-                return value._id;
-            });
-
-            db.Igfs.remove({space: {$in: space_ids}}, function (err) {
-                if (err)
-                    return res.status(500).send(err.message);
-
-                db.Cluster.update({space: {$in: space_ids}}, {igfss: []}, {multi: true}, function (err) {
-                    if (db.processed(err, res))
-                        res.sendStatus(200);
-                });
-            })
-        }
-    });
-});
-
-module.exports = router;

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/routes/notebooks.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/routes/notebooks.js b/modules/control-center-web/src/main/js/routes/notebooks.js
deleted file mode 100644
index 93defd4..0000000
--- a/modules/control-center-web/src/main/js/routes/notebooks.js
+++ /dev/null
@@ -1,151 +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.
- */
-
-var router = require('express').Router();
-
-var db = require('../db');
-var utils = require('./../helpers/common-utils');
-
-/**
- * Get notebooks names accessed for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/list', function (req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        var space_ids = spaces.map(function (value) {
-            return value._id;
-        });
-
-        // Get all metadata for spaces.
-        db.Notebook.find({space: {$in: space_ids}}).select('_id name').sort('name').exec(function (err, notebooks) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.json(notebooks);
-        });
-    });
-});
-
-/**
- * Get notebook accessed for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/get', function (req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.find({$or: [{owner: user_id}, {usedBy: {$elemMatch: {account: user_id}}}]}, function (err, spaces) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        var space_ids = spaces.map(function (value) {
-            return value._id;
-        });
-
-        // Get all metadata for spaces.
-        db.Notebook.findOne({space: {$in: space_ids}, _id: req.body.noteId}).exec(function (err, notebook) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.json(notebook);
-        });
-    });
-});
-
-/**
- * Save notebook accessed for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/save', function (req, res) {
-    var note = req.body;
-    var noteId = note._id;
-
-    if (noteId)
-        db.Notebook.update({_id: noteId}, note, {upsert: true}, function (err) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            res.send(noteId);
-        });
-    else
-        db.Notebook.findOne({space: note.space, name: note.name}, function (err, note) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            if (note)
-                return res.status(500).send('Notebook with name: "' + note.name + '" already exist.');
-
-            (new db.Notebook(req.body)).save(function (err, note) {
-                if (err)
-                    return res.status(500).send(err.message);
-
-                res.send(note._id);
-            });
-        });
-});
-
-/**
- * Remove notebook by ._id.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/remove', function (req, res) {
-    db.Notebook.remove(req.body, function (err) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        res.sendStatus(200);
-    });
-});
-
-/**
- * Create new notebook for user account.
- *
- * @param req Request.
- * @param res Response.
- */
-router.post('/new', function (req, res) {
-    var user_id = req.currentUserId();
-
-    // Get owned space and all accessed space.
-    db.Space.findOne({owner: user_id}, function (err, space) {
-        if (err)
-            return res.status(500).send(err.message);
-
-        (new db.Notebook({space: space.id, name: req.body.name})).save(function (err, note) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            return res.send(note._id);
-        });
-    });
-});
-
-module.exports = router;

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/routes/profile.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/routes/profile.js b/modules/control-center-web/src/main/js/routes/profile.js
deleted file mode 100644
index ae262cc..0000000
--- a/modules/control-center-web/src/main/js/routes/profile.js
+++ /dev/null
@@ -1,85 +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.
- */
-
-var _ = require('lodash');
-
-var router = require('express').Router();
-var db = require('../db');
-
-function _updateUser(res, params) {
-    db.Account.update({_id: params._id}, params, {upsert: true}, function (err, user) {
-        // TODO IGNITE-843 Send error to admin.
-        if (err)
-            return res.status(500).send('Failed to update profile!');
-
-    if (params.email)
-        user.email = params.email;
-
-        res.sendStatus(200);
-    });
-}
-
-function _checkEmail(res, user, params) {
-    if (params.email && user.email != params.email) {
-        db.Account.findOne({email: params.email}, function(err, userForEmail) {
-            // TODO send error to admin
-            if (err)
-                return res.status(500).send('Failed to check e-mail!');
-
-            if (userForEmail && userForEmail._id != user._id)
-                return res.status(500).send('User with this e-mail already registered!');
-
-            _updateUser(res, params);
-        });
-    }
-    else
-        _updateUser(res, params);
-}
-
-/**
- * Save user profile.
- */
-router.post('/save', function (req, res) {
-    var params = req.body;
-
-    db.Account.findById(params._id, function (err, user) {
-        // TODO IGNITE-843 Send error to admin
-        if (err)
-            return res.status(500).send('Failed to find user!');
-
-        if (params.password) {
-            if (_.isEmpty(params.password))
-                return res.status(500).send('Wrong value for new password!');
-
-            user.setPassword(params.password, function (err, user) {
-                if (err)
-                    return res.status(500).send(err.message);
-
-                user.save(function(err) {
-                    if (err)
-                        return res.status(500).send("Failed to change password!");
-
-                    _checkEmail(res, user, params);
-                });
-            });
-        }
-        else
-            _checkEmail(res, user, params);
-    });
-});
-
-module.exports = router;

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/routes/public.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/routes/public.js b/modules/control-center-web/src/main/js/routes/public.js
deleted file mode 100644
index 44786a8..0000000
--- a/modules/control-center-web/src/main/js/routes/public.js
+++ /dev/null
@@ -1,231 +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.
- */
-
-var router = require('express').Router();
-var passport = require('passport');
-var nodemailer = require('nodemailer');
-
-var db = require('../db');
-var config = require('../helpers/configuration-loader.js');
-var $commonUtils = require('./../helpers/common-utils');
-
-// GET user.
-router.post('/user', function (req, res) {
-    var becomeUsed = req.session.viewedUser && req.user.admin;
-
-    var user = req.user;
-
-    if (becomeUsed) {
-        user = req.session.viewedUser;
-
-        user.becomeUsed = true;
-    }
-
-    res.json(user);
-});
-
-/**
- * Register new account.
- */
-router.post('/register', function (req, res) {
-    db.Account.count(function (err, cnt) {
-        if (err)
-            return res.status(401).send(err.message);
-
-        req.body.admin = cnt == 0;
-
-        var account = new db.Account(req.body);
-
-        account.token = $commonUtils.randomString(20);
-
-        db.Account.register(account, req.body.password, function (err, account) {
-            if (err)
-                return res.status(401).send(err.message);
-
-            if (!account)
-                return res.status(500).send('Failed to create account.');
-
-            new db.Space({name: 'Personal space', owner: account._id}).save();
-
-            req.logIn(account, {}, function (err) {
-                if (err)
-                    return res.status(401).send(err.message);
-
-                return res.sendStatus(200);
-            });
-        });
-    });
-});
-
-/**
- * Login in exist account.
- */
-router.post('/login', function (req, res, next) {
-    passport.authenticate('local', function (err, user) {
-        if (err)
-            return res.status(401).send(err.message);
-
-        if (!user)
-            return res.status(401).send('Invalid email or password');
-
-        req.logIn(user, {}, function (err) {
-            if (err)
-                return res.status(401).send(err.message);
-
-            return res.sendStatus(200);
-        });
-    })(req, res, next);
-});
-
-/**
- * Logout.
- */
-router.post('/logout', function (req, res) {
-    req.logout();
-
-    res.sendStatus(200);
-});
-
-/**
- * Send e-mail to user with reset token.
- */
-router.post('/password/forgot', function(req, res) {
-    var transporter = {
-        service: config.get('smtp:service'),
-        auth: {
-            user:config.get('smtp:email'),
-            pass: config.get('smtp:password')
-        }
-    };
-
-    if (transporter.service == '' || transporter.auth.user == '' || transporter.auth.pass == '')
-        return res.status(401).send('Can\'t send e-mail with instructions to reset password. Please ask webmaster to setup SMTP server!');
-
-    var token = $commonUtils.randomString(20);
-
-    db.Account.findOne({ email: req.body.email }, function(err, user) {
-        if (!user)
-            return res.status(401).send('No account with that email address exists!');
-
-        if (err)
-            // TODO IGNITE-843 Send email to admin
-            return res.status(401).send('Failed to reset password!');
-
-        user.resetPasswordToken = token;
-
-        user.save(function(err) {
-            if (err)
-            // TODO IGNITE-843 Send email to admin
-            return res.status(401).send('Failed to reset password!');
-
-            var mailer  = nodemailer.createTransport(transporter);
-
-            var mailOptions = {
-                from: config.address(config.get('smtp:username'), config.get('smtp:email')),
-                to: config.address(user.username, user.email),
-                subject: 'Password Reset',
-                text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
-                'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
-                'http://' + req.headers.host + '/password/reset?token=' + token + '\n\n' +
-                'If you did not request this, please ignore this email and your password will remain unchanged.\n\n' +
-                '--------------\n' +
-                'Apache Ignite Web Console\n'
-            };
-
-            mailer.sendMail(mailOptions, function(err){
-                if (err)
-                    return res.status(401).send('Failed to send e-mail with reset link! ' + err);
-
-                return res.status(200).send('An e-mail has been sent with further instructions.');
-            });
-        });
-    });
-});
-
-/**
- * Change password with given token.
- */
-router.post('/password/reset', function(req, res) {
-    db.Account.findOne({ resetPasswordToken: req.body.token }, function(err, user) {
-        if (!user)
-            return res.status(500).send('Invalid token for password reset!');
-
-        if (err)
-            // TODO IGNITE-843 Send email to admin
-            return res.status(500).send('Failed to reset password!');
-
-        user.setPassword(req.body.password, function (err, updatedUser) {
-            if (err)
-                return res.status(500).send(err.message);
-
-            updatedUser.resetPasswordToken = undefined;
-
-            updatedUser.save(function (err) {
-                if (err)
-                    return res.status(500).send(err.message);
-
-                var transporter = {
-                    service: config.get('smtp:service'),
-                    auth: {
-                        user: config.get('smtp:email'),
-                        pass: config.get('smtp:password')
-                    }
-                };
-
-                var mailer = nodemailer.createTransport(transporter);
-
-                var mailOptions = {
-                    from: config.address(config.get('smtp:username'), config.get('smtp:email')),
-                    to: config.address(user.username, user.email),
-                    subject: 'Your password has been changed',
-                    text: 'Hello,\n\n' +
-                    'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n\n' +
-                    'Now you can login: http://' + req.headers.host + '\n\n' +
-                    '--------------\n' +
-                    'Apache Ignite Web Console\n'
-                };
-
-                mailer.sendMail(mailOptions, function (err) {
-                    if (err)
-                        return res.status(503).send('Password was changed, but failed to send confirmation e-mail!<br />' + err);
-
-                    return res.status(200).send(user.email);
-                });
-            });
-        });
-    });
-});
-
-/* GET reset password page. */
-router.post('/validate/token', function (req, res) {
-    var token = req.body.token;
-
-    var data = {token: token};
-
-    db.Account.findOne({resetPasswordToken: token}, function (err, user) {
-        if (!user)
-            data.error = 'Invalid token for password reset!';
-        else if (err)
-            data.error = err;
-        else
-            data.email = user.email;
-
-        res.json(data);
-    });
-});
-
-module.exports = router;

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve.js b/modules/control-center-web/src/main/js/serve.js
index 9fff7a4..b5897a2 100644
--- a/modules/control-center-web/src/main/js/serve.js
+++ b/modules/control-center-web/src/main/js/serve.js
@@ -15,108 +15,94 @@
  * limitations under the License.
  */
 
+const http = require('http'),
+    https = require('https'),
+    path = require('path');
+
 /**
- * Module dependencies.
+ * Event listener for HTTP server "error" event.
  */
-var http = require('http');
-var https = require('https');
-var config = require('./helpers/configuration-loader.js');
-var app = require('./app');
-var agentManager = require('./agents/agent-manager');
+const _onError = (port, error) => {
+    if (error.syscall !== 'listen')
+        throw error;
+
+    var bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
 
-var fs = require('fs');
+    // Handle specific listen errors with friendly messages.
+    switch (error.code) {
+        case 'EACCES':
+            console.error(bind + ' requires elevated privileges');
+            process.exit(1);
 
-var debug = require('debug')('ignite-web-console:server');
+            break;
+        case 'EADDRINUSE':
+            console.error(bind + ' is already in use');
+            process.exit(1);
+
+            break;
+        default:
+            throw error;
+    }
+};
 
 /**
- * Get port from environment and store in Express.
+ * Event listener for HTTP server "listening" event.
  */
-var port = config.normalizePort(config.get('server:port') || process.env.PORT || 80);
+const _onListening = (addr) => {
+    var bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port;
 
-// Create HTTP server.
-var server = http.createServer(app);
+    console.log('Start listening on ' + bind);
+};
 
-app.set('port', port);
+const igniteModules = (process.env.IGNITE_MODULES && path.relative(__dirname, process.env.IGNITE_MODULES)) || './ignite_modules';
 
-/**
- * Listen on provided port, on all network interfaces.
- */
-server.listen(port);
-server.on('error', onError);
-server.on('listening', onListening);
-
-if (config.get('server:ssl')) {
-    httpsServer = https.createServer({
-        key: fs.readFileSync(config.get('server:key')),
-        cert: fs.readFileSync(config.get('server:cert')),
-        passphrase: config.get('server:keyPassphrase')
-    }, app);
-
-    var httpsPort = config.normalizePort(config.get('server:https-port') || 443);
-
-    /**
-     * Listen on provided port, on all network interfaces.
-     */
-    httpsServer.listen(httpsPort);
-    httpsServer.on('error', onError);
-    httpsServer.on('listening', onListening);
-}
+const fireUp = require('fire-up').newInjector({
+    basePath: __dirname,
+    modules: [
+        './serve/**/*.js',
+        `${igniteModules}/**/*.js`
+    ]
+});
 
-/**
- * Start agent server.
- */
-var agentServer;
+Promise.all([fireUp('settings'), fireUp('app'), fireUp('agent')])
+    .then((values) => {
+        const settings = values[0], app = values[1], agent = values[2];
 
-if (config.get('agent-server:ssl')) {
-    agentServer = https.createServer({
-    key: fs.readFileSync(config.get('agent-server:key')),
-    cert: fs.readFileSync(config.get('agent-server:cert')),
-    passphrase: config.get('agent-server:keyPassphrase')
-  });
-}
-else {
-  agentServer = http.createServer();
-}
+        // Create HTTP server.
+        const server = http.createServer(app);
 
-agentServer.listen(config.get('agent-server:port'));
+        app.set('port', settings.server.port);
 
-agentManager.createManager(agentServer);
+        server.listen(settings.server.port);
+        server.on('error', _onError.bind(null, settings.server.port));
+        server.on('listening', _onListening.bind(null, server.address()));
 
-/**
- * Event listener for HTTP server "error" event.
- */
-function onError(error) {
-  if (error.syscall !== 'listen') {
-    throw error;
-  }
-
-  var bind = typeof port === 'string'
-    ? 'Pipe ' + port
-    : 'Port ' + port;
-
-  // Handle specific listen errors with friendly messages.
-  switch (error.code) {
-    case 'EACCES':
-      console.error(bind + ' requires elevated privileges');
-      process.exit(1);
-      break;
-    case 'EADDRINUSE':
-      console.error(bind + ' is already in use');
-      process.exit(1);
-      break;
-    default:
-      throw error;
-  }
-}
+        // Create HTTPS server if needed.
+        if (settings.serverSSLOptions) {
+            const httpsServer = https.createServer(settings.server.SSLOptions, app);
 
-/**
- * Event listener for HTTP server "listening" event.
- */
-function onListening() {
-  var addr = server.address();
-  var bind = typeof addr === 'string'
-    ? 'pipe ' + addr
-    : 'port ' + addr.port;
-
-  console.log('Start listening on ' + bind);
-}
+            const httpsPort = settings.server.SSLOptions.port;
+
+            httpsServer.listen(httpsPort);
+            httpsServer.on('error', _onError.bind(null, httpsPort));
+            httpsServer.on('listening', _onListening.bind(null, httpsServer.address()));
+        }
+
+        // Start agent server.
+        const agentServer = settings.agent.SSLOptions
+            ? https.createServer(settings.agent.SSLOptions) : http.createServer();
+
+        agentServer.listen(settings.agent.port);
+        agentServer.on('error', _onError.bind(null, settings.agent.port));
+        agentServer.on('listening', _onListening.bind(null, agentServer.address()));
+
+        agent.listen(agentServer);
+
+        // Used for automated test.
+        if (process.send)
+            process.send('running');
+    }).catch((err) => {
+        console.error(err);
+
+        process.exit(1);
+    });

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/agent.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/agent.js b/modules/control-center-web/src/main/js/serve/agent.js
new file mode 100644
index 0000000..d1ffe1a
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/agent.js
@@ -0,0 +1,380 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'agent',
+    inject: ['require(fs)', 'require(ws)', 'require(apache-ignite)', 'mongo']
+};
+
+module.exports.factory = function (fs, ws, apacheIgnite, mongo) {
+    /**
+     * @constructor
+     */
+    function AgentManager() {
+        this._clients = {};
+    }
+
+    /**
+     *
+     */
+    AgentManager.prototype.listen = function (srv) {
+        if (this._server)
+            throw 'Agent server already started!';
+
+        this._server = srv;
+
+        this._wss = new ws.Server({server: this._server});
+
+        var self = this;
+
+        this._wss.on('connection', function (ws) {
+            new Client(ws, self);
+        });
+    };
+
+    /**
+     * @param userId
+     * @param {Client} client
+     */
+    AgentManager.prototype._removeClient = function (userId, client) {
+        var connections = this._clients[userId];
+
+        if (connections) {
+            var idx;
+
+            while ((idx = connections.indexOf(client)) !== -1)
+                connections.splice(idx, 1);
+
+            if (connections.length == 0)
+                delete this._clients[userId];
+        }
+    };
+
+    /**
+     * @param userId
+     * @param {Client} client
+     */
+    AgentManager.prototype._addClient = function (userId, client) {
+        var existingConnections = this._clients[userId];
+
+        if (!existingConnections) {
+            existingConnections = [];
+
+            this._clients[userId] = existingConnections;
+        }
+
+        existingConnections.push(client);
+    };
+
+    /**
+     * @param userId
+     * @returns {Client}
+     */
+    AgentManager.prototype.findClient = function (userId) {
+        const clientsList = this._clients[userId];
+
+        if (!clientsList || clientsList.length == 0)
+            return null;
+
+        return clientsList[0];
+    };
+
+    /**
+     * Creates an instance of server for Ignite
+     *
+     * @constructor
+     * @this {AgentServer}
+     * @param {Client} client Connected client
+     * @param {Boolean} demo Use demo node for request
+     */
+    function AgentServer(client, demo) {
+        this._client = client;
+        this._demo = !!demo;
+    }
+
+    /**
+     * Run http request
+     *
+     * @this {AgentServer}
+     * @param {Command} cmd Command
+     * @param {callback} callback on finish
+     */
+    AgentServer.prototype.runCommand = function (cmd, callback) {
+        var params = {cmd: cmd.name()};
+
+        for (var key in cmd._params)
+            params[key] = cmd._params[key];
+
+        var body = undefined;
+
+        var headers = undefined;
+
+        var method = 'GET';
+
+        if (cmd._isPost()) {
+            body = cmd.postData();
+
+            method = 'POST';
+
+            headers = {'JSONObject': 'application/json'};
+        }
+
+        this._client.executeRest("ignite", params, this._demo, method, headers, body, callback);
+    };
+
+    /**
+     * @constructor
+     * @param {AgentManager} manager
+     * @param {WebSocket} ws
+     */
+    function Client(ws, manager) {
+        var self = this;
+
+        this._manager = manager;
+        this._ws = ws;
+
+        ws.on('close', function () {
+            if (self._user) {
+                self._manager._removeClient(self._user._id, self);
+            }
+        });
+
+        ws.on('message', function (msgStr) {
+            var msg = JSON.parse(msgStr);
+
+            self['_rmt' + msg.type](msg);
+        });
+
+        this._reqCounter = 0;
+
+        this._cbMap = {};
+    }
+
+    Client.prototype._runCommand = function (method, args) {
+        var self = this;
+
+        return new Promise(function (resolve, reject) {
+            self._invokeRmtMethod(method, args, function (error, res) {
+                if (error != null)
+                    return reject(error);
+
+                resolve(res);
+            });
+        });
+    };
+
+    /**
+     * @param {String} uri
+     * @param {Object} params
+     * @param {Boolean} demo
+     * @param {String} [method]
+     * @param {Object} [headers]
+     * @param {String} [body]
+     * @param {callback} [callback] Callback. Take 3 arguments: {Number} successStatus, {String} error,  {String} response.
+     */
+    Client.prototype.executeRest = function (uri, params, demo, method, headers, body, callback) {
+        if (typeof(params) != 'object')
+            throw '"params" argument must be an object';
+
+        if (typeof(callback) != 'function')
+            throw 'callback must be a function';
+
+        if (body && typeof(body) != 'string')
+            throw 'body must be a string';
+
+        if (headers && typeof(headers) != 'object')
+            throw 'headers must be an object';
+
+        if (!method)
+            method = 'GET';
+        else
+            method = method.toUpperCase();
+
+        if (method != 'GET' && method != 'POST')
+            throw 'Unknown HTTP method: ' + method;
+
+        const cb = function (error, restResult) {
+            if (error)
+                return callback(error);
+
+            const restError = restResult.error;
+
+            if (restError)
+                return callback(restError);
+
+            const restCode = restResult.restCode;
+
+            if (restCode !== 200) {
+                if (restCode === 401)
+                    return callback.call({code: restCode, message: "Failed to authenticate on node."});
+
+                return callback.call({
+                    code: restCode,
+                    message: restError || "Failed connect to node and execute REST command."
+                });
+            }
+
+            try {
+                var nodeResponse = JSON.parse(restResult.response);
+
+                if (nodeResponse.successStatus === 0)
+                    return callback(null, nodeResponse.response);
+
+                switch (nodeResponse.successStatus) {
+                    case 1:
+                        return callback({code: 500, message: nodeResponse.error});
+                    case 2:
+                        return callback({code: 401, message: nodeResponse.error});
+                    case 3:
+                        return callback({code: 403, message: nodeResponse.error});
+                }
+
+                callback(nodeResponse.error);
+            }
+            catch (e) {
+                callback(e);
+            }
+        };
+
+        this._invokeRmtMethod('executeRest', [uri, params, demo, method, headers, body], cb);
+    };
+
+    /**
+     * @param {string} error
+     */
+    Client.prototype.authResult = function (error) {
+        return this._runCommand('authResult', [].slice.call(arguments));
+    };
+
+    /**
+     * @param {String} driverPath
+     * @param {String} driverClass
+     * @param {String} url
+     * @param {Object} info
+     * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class)
+     */
+    Client.prototype.metadataSchemas = function (driverPath, driverClass, url, info) {
+        return this._runCommand('schemas', [].slice.call(arguments));
+    };
+
+    /**
+     * @param {String} driverPath
+     * @param {String} driverClass
+     * @param {String} url
+     * @param {Object} info
+     * @param {Array} schemas
+     * @param {Boolean} tablesOnly
+     * @returns {Promise} Promise on list of tables (see org.apache.ignite.schema.parser.DbTable java class)
+     */
+    Client.prototype.metadataTables = function (driverPath, driverClass, url, info, schemas, tablesOnly) {
+        return this._runCommand('metadata', [].slice.call(arguments));
+    };
+
+    /**
+     * @returns {Promise} Promise on list of jars from driver folder.
+     */
+    Client.prototype.availableDrivers = function () {
+        return this._runCommand('availableDrivers', [].slice.call(arguments));
+    };
+
+    /**
+     * Run http request
+     *
+     * @this {AgentServer}
+     * @param {String} method Command name.
+     * @param {Array} args Command params.
+     * @param {Function} callback on finish
+     */
+    Client.prototype._invokeRmtMethod = function (method, args, callback) {
+        if (this._ws.readyState != 1) {
+            if (callback)
+                callback('org.apache.ignite.agent.AgentException: Connection is closed');
+
+            return;
+        }
+
+        var msg = {
+            method: method,
+            args: args
+        };
+
+        if (callback) {
+            var reqId = this._reqCounter++;
+
+            this._cbMap[reqId] = callback;
+
+            msg.reqId = reqId;
+        }
+
+        this._ws.send(JSON.stringify(msg))
+    };
+
+    Client.prototype._rmtAuthMessage = function (msg) {
+        var self = this;
+
+        fs.stat('public/agent/ignite-web-agent-1.5.0.final.zip', function (err, stats) {
+            var relDate = 0;
+
+            if (!err)
+                relDate = stats.birthtime.getTime();
+
+            if ((msg.relDate || 0) < relDate)
+                self.authResult('You are using an older version of the agent. Please reload agent archive');
+
+            mongo.Account.findOne({token: msg.token}, function (err, account) {
+                if (err) {
+                    self.authResult('Failed to authorize user');
+                    // TODO IGNITE-1379 send error to web master.
+                }
+                else if (!account)
+                    self.authResult('Invalid token, user not found');
+                else {
+                    self.authResult(null);
+
+                    self._user = account;
+
+                    self._manager._addClient(account._id, self);
+
+                    self._cluster = new apacheIgnite.Ignite(new AgentServer(self));
+
+                    self._demo = new apacheIgnite.Ignite(new AgentServer(self, true));
+                }
+            });
+        });
+    };
+
+    Client.prototype._rmtCallRes = function (msg) {
+        var callback = this._cbMap[msg.reqId];
+
+        if (!callback) return;
+
+        delete this._cbMap[msg.reqId];
+
+        callback(msg.error, msg.response);
+    };
+
+    /**
+     * @returns {Ignite}
+     */
+    Client.prototype.ignite = function (demo) {
+        return demo ? this._demo : this._cluster;
+    };
+
+    return new AgentManager();
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/app.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/app.js b/modules/control-center-web/src/main/js/serve/app.js
new file mode 100644
index 0000000..cc93ed9
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/app.js
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'app',
+    inject: ['require(express)', 'configure', 'routes']
+};
+
+module.exports.factory = function(express, configure, routes) {
+    const app = new express();
+
+    configure(app);
+
+    routes.register(app);
+
+    return app;
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/config/default.json
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/config/default.json b/modules/control-center-web/src/main/js/serve/config/default.json
new file mode 100644
index 0000000..574d42a
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/config/default.json
@@ -0,0 +1,26 @@
+{
+    "server": {
+        "port": 3000,
+        "https-port": 8443,
+        "ssl": false,
+        "key": "serve/keys/test.key",
+        "cert": "serve/keys/test.crt",
+        "keyPassphrase": "password"
+    },
+    "mongoDB": {
+        "url": "mongodb://localhost/web-control-center"
+    },
+    "agent-server": {
+        "port": 3001,
+        "ssl": true,
+        "key": "serve/keys/test.key",
+        "cert": "serve/keys/test.crt",
+        "keyPassphrase": "password"
+    },
+    "smtp": {
+        "service": "",
+        "username": "",
+        "email": "",
+        "password": ""
+    }
+}

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/configure.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/configure.js b/modules/control-center-web/src/main/js/serve/configure.js
new file mode 100644
index 0000000..c16b516
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/configure.js
@@ -0,0 +1,67 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'configure',
+    inject: ['require(morgan)', 'require(cookie-parser)', 'require(body-parser)', 'require(express-force-ssl)',
+        'require(express-session)', 'require(connect-mongo)', 'require(passport)', 'settings', 'mongo']
+};
+
+module.exports.factory = function (logger, cookieParser, bodyParser, forceSSL, session, connectMongo, passport,
+                                   settings, mongo) {
+    return (app) => {
+        app.use(logger('dev', {
+            skip: function (req, res) {
+                return res.statusCode < 400;
+            }
+        }));
+
+        app.use(cookieParser(settings.sessionSecret));
+
+        app.use(bodyParser.json({limit: '50mb'}));
+        app.use(bodyParser.urlencoded({limit: '50mb', extended: true}));
+
+        var mongoStore = connectMongo(session);
+
+        app.use(session({
+            secret: settings.sessionSecret,
+            resave: false,
+            saveUninitialized: true,
+            cookie: {
+                expires: new Date(Date.now() + settings.cookieTTL),
+                maxAge: settings.cookieTTL
+            }
+            , store: new mongoStore({mongooseConnection: mongo.connection})
+        }));
+
+        app.use(passport.initialize());
+        app.use(passport.session());
+
+        passport.serializeUser(mongo.Account.serializeUser());
+        passport.deserializeUser(mongo.Account.deserializeUser());
+
+        passport.use(mongo.Account.createStrategy());
+
+        if (settings.SSLOptions) {
+            app.set('forceSSLOptions', settings.SSLOptions);
+
+            app.use(forceSSL);
+        }
+    };
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/keys/test.crt
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/keys/test.crt b/modules/control-center-web/src/main/js/serve/keys/test.crt
new file mode 100644
index 0000000..50c6d5c
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/keys/test.crt
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE-----
+MIIB6zCCAVQCCQDcAphbU6UcLjANBgkqhkiG9w0BAQsFADA6MRIwEAYDVQQDDAls
+b2NhbGhvc3QxJDAiBgkqhkiG9w0BCQEWFXNldmRva2ltb3ZAYXBhY2hlLm9yZzAe
+Fw0xNTA3MTQxMzAyNTNaFw0xODA2MjMxMzAyNTNaMDoxEjAQBgNVBAMMCWxvY2Fs
+aG9zdDEkMCIGCSqGSIb3DQEJARYVc2V2ZG9raW1vdkBhcGFjaGUub3JnMIGfMA0G
+CSqGSIb3DQEBAQUAA4GNADCBiQKBgQDP/zpJrdHqCj6lPpeFF6LQtzKef6UiyBBo
+rbuOtCCgW8KMJJciluBWk2126qLt9smBN4jBpSNU3pq0r9gBMUTd/LSe7aY4D5ED
+Pjp7XsypNVKeHaHbFi7KhfHy0LYxsWiNPmmHJv4dtYOp+pGK25rkXNfyJxxjgxN6
+wo34+MnZIQIDAQABMA0GCSqGSIb3DQEBCwUAA4GBAFk9XEjcdyihws+fVmdGGUFo
+bVxI9YGH6agiNbU3WNF4B4VRzcPPW8z2mEo7eF9kgYmq/YzH4T8tgi/qkL/u8eZV
+Wmi9bg6RThLN6/hj3wVoOFKykbDQ05FFdhIJXN5UOjPmxYM97EKqg6J0W2HAb8SG
++UekPnmAo/2HTKsLykH8
+-----END CERTIFICATE-----

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/keys/test.key
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/keys/test.key b/modules/control-center-web/src/main/js/serve/keys/test.key
new file mode 100644
index 0000000..1b395c0
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/keys/test.key
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,6798185330CE2EE2
+
+sOwkmD8rvjx11l09V26dJhLhl+SyPIhyeZ3TqHXrYCATKoXlzidT+uPu1jVYtrwr
+nBLA6TrIDYRrBNlEsqGZ0cSvWTIczzVW1xZKHEJo5q2vUT/W8u/Q1QQtS3P3GeKF
+dEzx496rpZqwwVw59GNbuIwyYoVvQf3iEXzfhplGmLPELYIplDFOLgNuXQyXSGx6
+rwKsCxXMLsDyrA6DCz0Odf08p2HvWk/s5Ne3DFcQlqRNtIrBVGD2O0/Fp8ZZ2I4E
+Yn2OIIWJff3HanOjLOWKdN8YAn5UleNmlEUdIHeS5qaQ68mabOxLkSef9qglV+sd
+FHTtUq0cG6t6nhxZBziexha6v1yl/xABAHHhNPOfak+HthWxRD4N9f1yFYAeTmkn
+4kwBWoSUe12XRf2pGNqhEUKN/KhDmWk85wI55i/Cu2XmNoiBFlS9BXrRYU8uVCJw
+KlxjKTDWl1opCyvxTDxJnMkt44ZT445LRePKVueGIIKSUIXNQypOE+C1I0CL0N2W
+Ts3m9nthquvLeMx92k7b8yW69BER5uac3SIlGCOJObQXsHgyk8wYiyd/zLKfjctG
+PXieaW81UKjp+GqWpvWPz3VqnKwoyUWeVOOTviurli6kYOrHuySTMqMb6hxJctw9
+grAQTT0UPiAKWcM7InLzZnRjco+v9QLLEokjVngXPba16K/CItFY16xuGlaFLW7Y
+XTc67AkL8b76HBZelMjmCsqjvSoULhuMFwTOvUMm/mSM8rMoi9asrJRLQHRMWCST
+/6RENPLzPlOMnNLBujpBbn8V3/aYzEZsHMI+6S3d27WYlTJIqpabSA==
+-----END RSA PRIVATE KEY-----

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/mongo.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/mongo.js b/modules/control-center-web/src/main/js/serve/mongo.js
new file mode 100644
index 0000000..edfb5f8
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/mongo.js
@@ -0,0 +1,551 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'mongo',
+    inject: ['require(mongoose-deep-populate)', 'require(passport-local-mongoose)', 'settings', 'ignite_modules/mongo:*']
+};
+
+module.exports.factory = function (deepPopulatePlugin, passportMongo, settings, pluginMongo) {
+    var mongoose = require('mongoose');
+
+    const deepPopulate = deepPopulatePlugin(mongoose);
+
+    // Connect to mongoDB database.
+    mongoose.connect(settings.mongoUrl, {server: {poolSize: 4}});
+
+    const Schema = mongoose.Schema, ObjectId = mongoose.Schema.Types.ObjectId,
+        result = { connection: mongoose.connection };
+
+    // Define Account schema.
+    var AccountSchema = new Schema({
+        username: String,
+        email: String,
+        company: String,
+        country: String,
+        lastLogin: Date,
+        admin: Boolean,
+        token: String,
+        resetPasswordToken: String
+    });
+
+    // Install passport plugin.
+    AccountSchema.plugin(passportMongo, {
+        usernameField: 'email', limitAttempts: true, lastLoginField: 'lastLogin',
+        usernameLowerCase: true
+    });
+
+    // Configure transformation to JSON.
+    AccountSchema.set('toJSON', {
+        transform: function (doc, ret) {
+            return {
+                _id: ret._id,
+                email: ret.email,
+                username: ret.username,
+                company: ret.company,
+                country: ret.country,
+                admin: ret.admin,
+                token: ret.token,
+                lastLogin: ret.lastLogin
+            };
+        }
+    });
+
+    // Define Account model.
+    result.Account = mongoose.model('Account', AccountSchema);
+
+    // Define Space model.
+    result.Space = mongoose.model('Space', new Schema({
+        name: String,
+        owner: {type: ObjectId, ref: 'Account'},
+        usedBy: [{
+            permission: {type: String, enum: ['VIEW', 'FULL']},
+            account: {type: ObjectId, ref: 'Account'}
+        }]
+    }));
+
+    // Define Domain model schema.
+    var DomainModelSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space'},
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        queryMetadata: {type: String, enum: ['Annotations', 'Configuration']},
+        kind: {type: String, enum: ['query', 'store', 'both']},
+        databaseSchema: String,
+        databaseTable: String,
+        keyType: String,
+        valueType: String,
+        keyFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        valueFields: [{
+            databaseFieldName: String,
+            databaseFieldType: String,
+            javaFieldName: String,
+            javaFieldType: String
+        }],
+        fields: [{name: String, className: String}],
+        aliases: [{field: String, alias: String}],
+        indexes: [{
+            name: String,
+            indexType: {type: String, enum: ['SORTED', 'FULLTEXT', 'GEOSPATIAL']},
+            fields: [{name: String, direction: Boolean}]
+        }],
+        demo: Boolean
+    });
+
+    // Define model of Domain models.
+    result.DomainModel = mongoose.model('DomainModel', DomainModelSchema);
+
+    // Define Cache schema.
+    var CacheSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space'},
+        name: String,
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        domains: [{type: ObjectId, ref: 'DomainModel'}],
+        cacheMode: {type: String, enum: ['PARTITIONED', 'REPLICATED', 'LOCAL']},
+        atomicityMode: {type: String, enum: ['ATOMIC', 'TRANSACTIONAL']},
+
+        backups: Number,
+        memoryMode: {type: String, enum: ['ONHEAP_TIERED', 'OFFHEAP_TIERED', 'OFFHEAP_VALUES']},
+        offHeapMaxMemory: Number,
+        startSize: Number,
+        swapEnabled: Boolean,
+
+        evictionPolicy: {
+            kind: {type: String, enum: ['LRU', 'FIFO', 'Sorted']},
+            LRU: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            FIFO: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            },
+            SORTED: {
+                batchSize: Number,
+                maxMemorySize: Number,
+                maxSize: Number
+            }
+        },
+
+        rebalanceMode: {type: String, enum: ['SYNC', 'ASYNC', 'NONE']},
+        rebalanceBatchSize: Number,
+        rebalanceBatchesPrefetchCount: Number,
+        rebalanceOrder: Number,
+        rebalanceDelay: Number,
+        rebalanceTimeout: Number,
+        rebalanceThrottle: Number,
+
+        cacheStoreFactory: {
+            kind: {
+                type: String,
+                enum: ['CacheJdbcPojoStoreFactory', 'CacheJdbcBlobStoreFactory', 'CacheHibernateBlobStoreFactory']
+            },
+            CacheJdbcPojoStoreFactory: {
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                }
+            },
+            CacheJdbcBlobStoreFactory: {
+                connectVia: {type: String, enum: ['URL', 'DataSource']},
+                connectionUrl: String,
+                user: String,
+                dataSourceBean: String,
+                dialect: {
+                    type: String,
+                    enum: ['Generic', 'Oracle', 'DB2', 'SQLServer', 'MySQL', 'PostgreSQL', 'H2']
+                },
+                initSchema: Boolean,
+                createTableQuery: String,
+                loadQuery: String,
+                insertQuery: String,
+                updateQuery: String,
+                deleteQuery: String
+            },
+            CacheHibernateBlobStoreFactory: {
+                hibernateProperties: [String]
+            }
+        },
+        storeKeepBinary: Boolean,
+        loadPreviousValue: Boolean,
+        readThrough: Boolean,
+        writeThrough: Boolean,
+
+        writeBehindEnabled: Boolean,
+        writeBehindBatchSize: Number,
+        writeBehindFlushSize: Number,
+        writeBehindFlushFrequency: Number,
+        writeBehindFlushThreadCount: Number,
+
+        invalidate: Boolean,
+        defaultLockTimeout: Number,
+        atomicWriteOrderMode: {type: String, enum: ['CLOCK', 'PRIMARY']},
+        writeSynchronizationMode: {type: String, enum: ['FULL_SYNC', 'FULL_ASYNC', 'PRIMARY_SYNC']},
+
+        sqlEscapeAll: Boolean,
+        sqlSchema: String,
+        sqlOnheapRowCacheSize: Number,
+        longQueryWarningTimeout: Number,
+        sqlFunctionClasses: [String],
+        snapshotableIndex: Boolean,
+        statisticsEnabled: Boolean,
+        managementEnabled: Boolean,
+        readFromBackup: Boolean,
+        copyOnRead: Boolean,
+        maxConcurrentAsyncOperations: Number,
+        nearCacheEnabled: Boolean,
+        nearConfiguration: {
+            nearStartSize: Number,
+            nearEvictionPolicy: {
+                kind: {type: String, enum: ['LRU', 'FIFO', 'Sorted']},
+                LRU: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                FIFO: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                },
+                SORTED: {
+                    batchSize: Number,
+                    maxMemorySize: Number,
+                    maxSize: Number
+                }
+            }
+        },
+        demo: Boolean
+    });
+
+    // Install deep populate plugin.
+    CacheSchema.plugin(deepPopulate, {
+        whitelist: ['domains']
+    });
+
+    // Define Cache model.
+    result.Cache = mongoose.model('Cache', CacheSchema);
+
+    var IgfsSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space'},
+        name: String,
+        clusters: [{type: ObjectId, ref: 'Cluster'}],
+        affinnityGroupSize: Number,
+        blockSize: Number,
+        streamBufferSize: Number,
+        dataCacheName: String,
+        metaCacheName: String,
+        defaultMode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']},
+        dualModeMaxPendingPutsSize: Number,
+        dualModePutExecutorService: String,
+        dualModePutExecutorServiceShutdown: Boolean,
+        fragmentizerConcurrentFiles: Number,
+        fragmentizerEnabled: Boolean,
+        fragmentizerThrottlingBlockLength: Number,
+        fragmentizerThrottlingDelay: Number,
+        ipcEndpointConfiguration: {
+            type: {type: String, enum: ['SHMEM', 'TCP']},
+            host: String,
+            port: Number,
+            memorySize: Number,
+            tokenDirectoryPath: String
+        },
+        ipcEndpointEnabled: Boolean,
+        maxSpaceSize: Number,
+        maximumTaskRangeLength: Number,
+        managementPort: Number,
+        pathModes: [{path: String, mode: {type: String, enum: ['PRIMARY', 'PROXY', 'DUAL_SYNC', 'DUAL_ASYNC']}}],
+        perNodeBatchSize: Number,
+        perNodeParallelBatchCount: Number,
+        prefetchBlocks: Number,
+        sequentialReadsBeforePrefetch: Number,
+        trashPurgeTimeout: Number,
+        secondaryFileSystemEnabled: Boolean,
+        secondaryFileSystem: {
+            uri: String,
+            cfgPath: String,
+            userName: String
+        }
+    });
+
+    // Define IGFS model.
+    result.Igfs = mongoose.model('Igfs', IgfsSchema);
+
+    // Define Cluster schema.
+    var ClusterSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space'},
+        name: String,
+        localHost: String,
+        discovery: {
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            addressResolver: String,
+            socketTimeout: Number,
+            ackTimeout: Number,
+            maxAckTimeout: Number,
+            networkTimeout: Number,
+            joinTimeout: Number,
+            threadPriority: Number,
+            heartbeatFrequency: Number,
+            maxMissedHeartbeats: Number,
+            maxMissedClientHeartbeats: Number,
+            topHistorySize: Number,
+            listener: String,
+            dataExchange: String,
+            metricsProvider: String,
+            reconnectCount: Number,
+            statisticsPrintFrequency: Number,
+            ipFinderCleanFrequency: Number,
+            authenticator: String,
+            forceServerMode: Boolean,
+            clientReconnectDisabled: Boolean,
+            kind: {type: String, enum: ['Vm', 'Multicast', 'S3', 'Cloud', 'GoogleStorage', 'Jdbc', 'SharedFs']},
+            Vm: {
+                addresses: [String]
+            },
+            Multicast: {
+                multicastGroup: String,
+                multicastPort: Number,
+                responseWaitTime: Number,
+                addressRequestAttempts: Number,
+                localAddress: String,
+                addresses: [String]
+            },
+            S3: {
+                bucketName: String
+            },
+            Cloud: {
+                credential: String,
+                credentialPath: String,
+                identity: String,
+                provider: String,
+                regions: [String],
+                zones: [String]
+            },
+            GoogleStorage: {
+                projectName: String,
+                bucketName: String,
+                serviceAccountP12FilePath: String,
+                serviceAccountId: String,
+                addrReqAttempts: String
+            },
+            Jdbc: {
+                initSchema: Boolean
+            },
+            SharedFs: {
+                path: String
+            }
+        },
+        atomicConfiguration: {
+            backups: Number,
+            cacheMode: {type: String, enum: ['LOCAL', 'REPLICATED', 'PARTITIONED']},
+            atomicSequenceReserveSize: Number
+        },
+        binaryConfiguration: {
+            idMapper: String,
+            serializer: String,
+            typeConfigurations: [{typeName: String, idMapper: String, serializer: String, enum: Boolean}],
+            compactFooter: Boolean
+        },
+        caches: [{type: ObjectId, ref: 'Cache'}],
+        clockSyncSamples: Number,
+        clockSyncFrequency: Number,
+        deploymentMode: {type: String, enum: ['PRIVATE', 'ISOLATED', 'SHARED', 'CONTINUOUS']},
+        discoveryStartupDelay: Number,
+        igfsThreadPoolSize: Number,
+        igfss: [{type: ObjectId, ref: 'Igfs'}],
+        includeEventTypes: [String],
+        managementThreadPoolSize: Number,
+        marshaller: {
+            kind: {type: String, enum: ['OptimizedMarshaller', 'JdkMarshaller']},
+            OptimizedMarshaller: {
+                poolSize: Number,
+                requireSerializable: Boolean
+            }
+        },
+        marshalLocalJobs: Boolean,
+        marshallerCacheKeepAliveTime: Number,
+        marshallerCacheThreadPoolSize: Number,
+        metricsExpireTime: Number,
+        metricsHistorySize: Number,
+        metricsLogFrequency: Number,
+        metricsUpdateFrequency: Number,
+        networkTimeout: Number,
+        networkSendRetryDelay: Number,
+        networkSendRetryCount: Number,
+        communication: {
+            listener: String,
+            localAddress: String,
+            localPort: Number,
+            localPortRange: Number,
+            sharedMemoryPort: Number,
+            directBuffer: Boolean,
+            directSendBuffer: Boolean,
+            idleConnectionTimeout: Number,
+            connectTimeout: Number,
+            maxConnectTimeout: Number,
+            reconnectCount: Number,
+            socketSendBuffer: Number,
+            socketReceiveBuffer: Number,
+            messageQueueLimit: Number,
+            slowClientQueueLimit: Number,
+            tcpNoDelay: Boolean,
+            ackSendThreshold: Number,
+            unacknowledgedMessagesBufferSize: Number,
+            socketWriteTimeout: Number,
+            selectorsCount: Number,
+            addressResolver: String
+        },
+        connector: {
+            enabled: Boolean,
+            jettyPath: String,
+            host: String,
+            port: Number,
+            portRange: Number,
+            idleTimeout: Number,
+            idleQueryCursorTimeout: Number,
+            idleQueryCursorCheckFrequency: Number,
+            receiveBufferSize: Number,
+            sendBufferSize: Number,
+            directBuffer: Boolean,
+            noDelay: Boolean,
+            selectorCount: Number,
+            threadPoolSize: Number,
+            messageInterceptor: String,
+            secretKey: String,
+            sslEnabled: Boolean,
+            sslClientAuth: Boolean,
+            sslFactory: String
+        },
+        peerClassLoadingEnabled: Boolean,
+        peerClassLoadingLocalClassPathExclude: [String],
+        peerClassLoadingMissedResourcesCacheSize: Number,
+        peerClassLoadingThreadPoolSize: Number,
+        publicThreadPoolSize: Number,
+        swapSpaceSpi: {
+            kind: {type: String, enum: ['FileSwapSpaceSpi']},
+            FileSwapSpaceSpi: {
+                baseDirectory: String,
+                readStripesNumber: Number,
+                maximumSparsity: Number,
+                maxWriteQueueSize: Number,
+                writeBufferSize: Number
+            }
+        },
+        systemThreadPoolSize: Number,
+        timeServerPortBase: Number,
+        timeServerPortRange: Number,
+        transactionConfiguration: {
+            defaultTxConcurrency: {type: String, enum: ['OPTIMISTIC', 'PESSIMISTIC']},
+            defaultTxIsolation: {type: String, enum: ['READ_COMMITTED', 'REPEATABLE_READ', 'SERIALIZABLE']},
+            defaultTxTimeout: Number,
+            pessimisticTxLogLinger: Number,
+            pessimisticTxLogSize: Number,
+            txSerializableEnabled: Boolean,
+            txManagerFactory: String
+        },
+        sslEnabled: Boolean,
+        sslContextFactory: {
+            keyAlgorithm: String,
+            keyStoreFilePath: String,
+            keyStoreType: String,
+            protocol: String,
+            trustStoreFilePath: String,
+            trustStoreType: String,
+            trustManagers: [String]
+        },
+        rebalanceThreadPoolSize: Number
+    });
+
+    // Install deep populate plugin.
+    ClusterSchema.plugin(deepPopulate, {
+        whitelist: [
+            'caches',
+            'caches.domains',
+            'igfss'
+        ]
+    });
+
+    // Define Cluster model.
+    result.Cluster = mongoose.model('Cluster', ClusterSchema);
+
+    result.ClusterDefaultPopulate = '';
+
+    // Define Notebook schema.
+    var NotebookSchema = new Schema({
+        space: {type: ObjectId, ref: 'Space'},
+        name: String,
+        expandedParagraphs: [Number],
+        paragraphs: [{
+            name: String,
+            query: String,
+            editor: Boolean,
+            result: {type: String, enum: ['none', 'table', 'bar', 'pie', 'line', 'area']},
+            pageSize: Number,
+            timeLineSpan: String,
+            hideSystemColumns: Boolean,
+            cacheName: String,
+            chartsOptions: {barChart: {stacked: Boolean}, areaChart: {style: String}},
+            rate: {
+                value: Number,
+                unit: Number
+            }
+        }]
+    });
+
+    // Define Notebook model.
+    result.Notebook = mongoose.model('Notebook', NotebookSchema);
+
+    result.upsert = function (model, data, cb) {
+        if (data._id) {
+            var id = data._id;
+
+            delete data._id;
+
+            model.findOneAndUpdate({_id: id}, data, cb);
+        }
+        else
+            new model(data).save(cb);
+    };
+
+    result.processed = function(err, res) {
+        if (err) {
+            res.status(500).send(err);
+
+            return false;
+        }
+
+        return true;
+    };
+
+    // Registering the routes of all plugin modules
+    for (var name in pluginMongo)
+        if (pluginMongo.hasOwnProperty(name))
+            pluginMongo[name].register(mongoose, deepPopulate, result);
+
+    return result;
+};

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/routes/admin.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/routes/admin.js b/modules/control-center-web/src/main/js/serve/routes/admin.js
new file mode 100644
index 0000000..a79d584
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/routes/admin.js
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'admin-routes',
+    inject: ['require(lodash)', 'require(express)', 'require(nodemailer)', 'mongo']
+};
+
+module.exports.factory = function (_, express, nodemailer, mongo) {
+    return new Promise((resolve) => {
+        const router = express.Router();
+
+        /**
+         * Get list of user accounts.
+         */
+        router.post('/list', function (req, res) {
+            mongo.Account.find({}).sort('username').exec(function (err, users) {
+                if (err)
+                    return res.status(500).send(err.message);
+
+                res.json(users);
+            });
+        });
+
+        // Remove user.
+        router.post('/remove', function (req, res) {
+            var userId = req.body.userId;
+
+            mongo.Account.findByIdAndRemove(userId, function (err, user) {
+                if (err)
+                    return res.status(500).send(err.message);
+
+                mongo.Space.find({owner: userId}, function (err, spaces) {
+                    _.forEach(spaces, function (space) {
+                        mongo.Cluster.remove({space: space._id}).exec();
+                        mongo.Cache.remove({space: space._id}).exec();
+                        mongo.DomainModel.remove({space: space._id}).exec();
+                        mongo.Notebook.remove({space: space._id}).exec();
+                        mongo.Space.remove({owner: space._id}).exec();
+                    });
+                });
+
+                var transporter = {
+                    service: settings.smtp.service,
+                    auth: {
+                        user: settings.smtp.email,
+                        pass: settings.smtp.password
+                    }
+                };
+
+                if (transporter.service != '' || transporter.auth.user != '' || transporter.auth.pass != '') {
+                    var mailer = nodemailer.createTransport(transporter);
+
+                    var mailOptions = {
+                        from: settings.smtp.address(settings.smtp.username, settings.smtp.email),
+                        to: settings.smtp.address(user.username, user.email),
+                        subject: 'Your account was deleted',
+                        text: 'You are receiving this e-mail because admin remove your account.\n\n' +
+                        '--------------\n' +
+                        'Apache Ignite Web Console http://' + req.headers.host + '\n'
+                    };
+
+                    mailer.sendMail(mailOptions, function (err) {
+                        if (err)
+                            return res.status(503).send('Account was removed, but failed to send e-mail notification to user!<br />' + err);
+
+                        res.sendStatus(200);
+                    });
+                }
+                else
+                    res.sendStatus(200);
+            });
+        });
+
+        // Save user.
+        router.post('/save', function (req, res) {
+            var userId = req.body.userId;
+            var adminFlag = req.body.adminFlag;
+
+            mongo.Account.findByIdAndUpdate(userId, {admin: adminFlag}, function (err) {
+                if (err)
+                    return res.status(500).send(err.message);
+
+                res.sendStatus(200);
+            });
+        });
+
+        // Become user.
+        router.get('/become', function (req, res) {
+            mongo.Account.findById(req.query.viewedUserId).exec(function (err, viewedUser) {
+                if (err)
+                    return res.sendStatus(404);
+
+                req.session.viewedUser = viewedUser;
+
+                return res.sendStatus(200);
+            })
+        });
+
+        // Become user.
+        router.get('/revert/identity', function (req, res) {
+            req.session.viewedUser = null;
+
+            return res.sendStatus(200);
+        });
+
+        resolve(router);
+    });
+};
+

http://git-wip-us.apache.org/repos/asf/ignite/blob/721a1165/modules/control-center-web/src/main/js/serve/routes/agent.js
----------------------------------------------------------------------
diff --git a/modules/control-center-web/src/main/js/serve/routes/agent.js b/modules/control-center-web/src/main/js/serve/routes/agent.js
new file mode 100644
index 0000000..e5abf0f
--- /dev/null
+++ b/modules/control-center-web/src/main/js/serve/routes/agent.js
@@ -0,0 +1,331 @@
+/*
+ * 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.
+ */
+
+// Fire me up!
+
+module.exports = {
+    implements: 'agent-routes',
+    inject: ['require(lodash)', 'require(express)', 'require(apache-ignite)', 'require(fs)', 'require(jszip)', 'settings', 'agent']
+};
+
+/**
+ * @param _
+ * @param express
+ * @param apacheIgnite
+ * @param fs
+ * @param JSZip
+ * @param settings
+ * @param {AgentManager} agent
+ * @returns {Promise}
+ */
+module.exports.factory = function (_, express, apacheIgnite, fs, JSZip, settings, agent) {
+    return new Promise((resolve) => {
+        const router = express.Router();
+
+        const SqlFieldsQuery = apacheIgnite.SqlFieldsQuery, ScanQuery = apacheIgnite.ScanQuery;
+
+        const _client = (userId) => {
+            return new Promise(function (resolve, reject) {
+                var client = agent.findClient(userId);
+
+                if (client)
+                    return resolve(client);
+
+                reject({code: 503, message: 'Connection to Ignite Web Agent is not established'});
+            });
+        };
+
+        const _compact = (className) => {
+            return className.replace('java.lang.', '').replace('java.util.', '').replace('java.sql.', '');
+        };
+
+        const _handleException = (res) => {
+            return function (error) {
+                if (_.isObject(error))
+                    return res.status(error.code).send(error.message);
+
+                return res.status(500).send(error);
+            }
+        };
+
+        /* Get grid topology. */
+        router.get('/download/zip', function (req, res) {
+            var agentFld = settings.agentFile;
+            var agentZip = agentFld + '.zip';
+            var agentPathZip = 'public/agent/' + agentFld + '.zip';
+
+            fs.stat(agentPathZip, function (err, stats) {
+                if (err)
+                    return res.download(agentPathZip, agentZip);
+
+                // Read a zip file.
+                fs.readFile(agentPathZip, function (err, data) {
+                    if (err)
+                        return res.download(agentPathZip, agentZip);
+
+                    var zip = new JSZip(data);
+
+                    var prop = [];
+
+                    var host = req.hostname.match(/:/g) ? req.hostname.slice(0, req.hostname.indexOf(':')) : req.hostname;
+
+                    prop.push('token=' + req.user.token);
+                    prop.push('server-uri=wss://' + host + ':' + settings.agentPort);
+                    prop.push('#Uncomment following options if needed:');
+                    prop.push('#node-uri=http://localhost:8080');
+                    prop.push('#driver-folder=./jdbc-drivers');
+                    prop.push('');
+                    prop.push("#Note: Don't change this auto generated line");
+                    prop.push('rel-date=' + stats.birthtime.getTime());
+
+                    zip.file(agentFld + '/default.properties', prop.join('\n'));
+
+                    var buffer = zip.generate({type: 'nodebuffer', platform: 'UNIX'});
+
+                    // Set the archive name.
+                    res.attachment(agentZip);
+
+                    res.send(buffer);
+                });
+            });
+        });
+
+        /* Get grid topology. */
+        router.post('/topology', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => client.ignite(req.body.demo).cluster(req.body.attr, req.body.mtr))
+                .then((clusters) => res.json(clusters))
+                .catch(_handleException(res));
+        });
+
+        /* Execute query. */
+        router.post('/query', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    // Create sql query.
+                    var qry = new SqlFieldsQuery(req.body.query);
+
+                    // Set page size for query.
+                    qry.setPageSize(req.body.pageSize);
+
+                    return client.ignite(req.body.demo).cache(req.body.cacheName).query(qry).nextPage()
+                })
+                .then((cursor) => res.json({
+                    meta: cursor.fieldsMetadata(),
+                    rows: cursor.page(),
+                    queryId: cursor.queryId()
+                }))
+                .catch(_handleException(res));
+        });
+
+        /* Execute query getAll. */
+        router.post('/query/getAll', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    // Create sql query.
+                    const qry = req.body.query ? new SqlFieldsQuery(req.body.query) : new ScanQuery();
+
+                    // Set page size for query.
+                    qry.setPageSize(1024);
+
+                    // Get query cursor.
+                    const cursor = client.ignite(req.body.demo).cache(req.body.cacheName).query(qry);
+
+                    return new Promise(function (resolve) {
+                        cursor.getAll().then(rows => resolve({meta: cursor.fieldsMetadata(), rows}))
+                    });
+                })
+                .then(response => res.json(response))
+                .catch(_handleException(res));
+        });
+
+        /* Execute query. */
+        router.post('/scan', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    // Create sql query.
+                    var qry = new ScanQuery();
+
+                    // Set page size for query.
+                    qry.setPageSize(req.body.pageSize);
+
+                    // Get query cursor.
+                    return client.ignite(req.body.demo).cache(req.body.cacheName).query(qry).nextPage()
+                })
+                .then((cursor) => res.json({
+                    meta: cursor.fieldsMetadata(),
+                    rows: cursor.page(),
+                    queryId: cursor.queryId()
+                }))
+                .catch(_handleException(res));
+        });
+
+        /* Get next query page. */
+        router.post('/query/fetch', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    var cache = client.ignite(req.body.demo).cache(req.body.cacheName);
+
+                    var cmd = cache._createCommand('qryfetch')
+                        .addParam('qryId', req.body.queryId)
+                        .addParam('pageSize', req.body.pageSize);
+
+                    return cache.__createPromise(cmd);
+                })
+                .then((page) => res.json({rows: page['items'], last: page === null || page['last']}))
+                .catch(_handleException(res));
+        });
+
+        /* Close query cursor by id. */
+        router.post('/query/close', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    var cache = client.ignite(req.body.demo).cache(req.body.cacheName);
+
+                    return cache.__createPromise(cache._createCommand('qrycls').addParam('qryId', req.body.queryId))
+                })
+                .then(() => res.sendStatus(200))
+                .catch(_handleException(res));
+        });
+
+        /* Get metadata for cache. */
+        router.post('/cache/metadata', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => client.ignite(req.body.demo).cache(req.body.cacheName).metadata())
+                .then((caches) => {
+                    var types = [];
+
+                    for (var meta of caches) {
+                        var cacheTypes = meta.types.map(function (typeName) {
+                            var fields = meta.fields[typeName];
+
+                            var columns = [];
+
+                            for (var fieldName in fields) {
+                                var fieldClass = _compact(fields[fieldName]);
+
+                                columns.push({
+                                    type: 'field',
+                                    name: fieldName,
+                                    clazz: fieldClass,
+                                    system: fieldName == "_KEY" || fieldName == "_VAL",
+                                    cacheName: meta.cacheName,
+                                    typeName: typeName
+                                });
+                            }
+
+                            var indexes = [];
+
+                            for (var index of meta.indexes[typeName]) {
+                                fields = [];
+
+                                for (var field of index.fields) {
+                                    fields.push({
+                                        type: 'index-field',
+                                        name: field,
+                                        order: index.descendings.indexOf(field) < 0,
+                                        unique: index.unique,
+                                        cacheName: meta.cacheName,
+                                        typeName: typeName
+                                    });
+                                }
+
+                                if (fields.length > 0)
+                                    indexes.push({
+                                        type: 'index',
+                                        name: index.name,
+                                        children: fields,
+                                        cacheName: meta.cacheName,
+                                        typeName: typeName
+                                    });
+                            }
+
+                            columns = _.sortBy(columns, 'name');
+
+                            if (!_.isEmpty(indexes))
+                                columns = columns.concat({
+                                    type: 'indexes',
+                                    name: 'Indexes',
+                                    cacheName: meta.cacheName,
+                                    typeName: typeName,
+                                    children: indexes
+                                });
+
+                            return {
+                                type: 'type',
+                                cacheName: meta.cacheName || "",
+                                typeName: typeName,
+                                children: columns
+                            };
+                        });
+
+                        if (!_.isEmpty(cacheTypes))
+                            types = types.concat(cacheTypes);
+                    }
+
+                    res.json(types);
+                })
+                .catch(_handleException(res));
+        });
+
+        /* Ping client. */
+        router.post('/ping', function (req, res) {
+            _client(req.currentUserId())
+                .then(() => res.sendStatus(200))
+                .catch(_handleException(res));
+        });
+
+        /* Get JDBC drivers list. */
+        router.post('/drivers', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => client.availableDrivers())
+                .then((arr) => res.json(arr))
+                .catch(_handleException(res));
+        });
+
+        /** Get database schemas. */
+        router.post('/schemas', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    var args = req.body;
+
+                    args.jdbcInfo = {user: args.user, password: args.password};
+
+                    return client.metadataSchemas(args.jdbcDriverJar, args.jdbcDriverClass, args.jdbcUrl, args.jdbcInfo)
+                })
+                .then((arr) => res.json(arr))
+                .catch(_handleException(res));
+        });
+
+        /** Get database tables. */
+        router.post('/tables', function (req, res) {
+            _client(req.currentUserId())
+                .then((client) => {
+                    var args = req.body;
+
+                    args.jdbcInfo = {user: args.user, password: args.password};
+
+                    return client.metadataTables(args.jdbcDriverJar, args.jdbcDriverClass, args.jdbcUrl, args.jdbcInfo, args.schemas, args.tablesOnly)
+                })
+                .then((arr) => res.json(arr))
+                .catch(_handleException(res));
+        });
+
+        resolve(router);
+    });
+};
+