diff --git a/admin/index.html b/admin/index.html index 24e0964..1f42b55 100644 --- a/admin/index.html +++ b/admin/index.html @@ -23,7 +23,15 @@ } .monitoring-disabled { - color: grey; + color: lightgrey; + } + + .vpn-key-set, .coords-set { + color: green; + } + + .vpn-key-unset, .coords-unset { + color: lightgrey; } diff --git a/admin/js/config.js b/admin/js/config.js index 4a1fd7f..1774ad2 100644 --- a/admin/js/config.js +++ b/admin/js/config.js @@ -34,20 +34,31 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons nodes .listView() .title('Nodes') + .perPage(5) .actions([]) .batchActions([]) .exportFields([]) .fields([ nga.field('token').cssClasses(nodeClasses), + nga.field('mac').cssClasses(nodeClasses), nga.field('hostname').cssClasses(nodeClasses), + nga.field('key').label('VPN').cssClasses(nodeClasses).template(function (node) { + return node.values.key + ? '' + : ''; + }), + nga.field('coords').label('GPS').cssClasses(nodeClasses).template(function (node) { + return node.values.coords + ? '' + : ''; + }), nga.field('monitoring').cssClasses(nodeClasses).template(function (node) { if (!node.values.monitoring) { - return ''; + return ''; } return node.values.monitoringConfirmed - ? '' - : ''; + ? '' + : ''; }) ]) .listActions([ @@ -105,8 +116,8 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons ]) .listActions( ' ' + - ' ' + - '' + ' ' + + '' ) ; @@ -116,17 +127,17 @@ angular.module('ffffngAdmin').config(function(NgAdminConfigurationProvider, Cons nga.menu() .addChild(nga .menu(nodes) - .icon('') + .icon('') ) .addChild(nga .menu(tasks) - .icon('') + .icon('') ) .addChild(nga .menu() .template( '' + - ' Logs' + + ' Logs' + '' ) ) diff --git a/admin/js/views/taskActionButton.js b/admin/js/views/taskActionButton.js index e93f3cc..c3e86fe 100644 --- a/admin/js/views/taskActionButton.js +++ b/admin/js/views/taskActionButton.js @@ -36,7 +36,7 @@ angular.module('ffffngAdmin') 'template': '' }; }); diff --git a/app/scripts/directives/nodeForm.js b/app/scripts/directives/nodeForm.js index d8ebdc1..961fc78 100644 --- a/app/scripts/directives/nodeForm.js +++ b/app/scripts/directives/nodeForm.js @@ -13,6 +13,10 @@ angular.module('ffffng') geolib, OutsideOfCommunityDialog ) { + // backwards compatibility + $scope.node.monitoring = $scope.node.monitoring || false; + $scope.node.monitoringConfirmed = $scope.node.monitoringConfirmed || false; + var initialEmail = $scope.node.email; var initialMonitoring = $scope.node.monitoring; var monitoringConfirmed = $scope.node.monitoringConfirmed; diff --git a/server/resources/nodeResource.js b/server/resources/nodeResource.js index 473c962..82b640b 100644 --- a/server/resources/nodeResource.js +++ b/server/resources/nodeResource.js @@ -11,7 +11,7 @@ angular.module('ffffng').factory('NodeResource', function ( ) { var nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring']; - function getValidNodeData(reqData) { + function getNormalizedNodeData(reqData) { var node = {}; _.each(nodeFields, function (field) { var value = Strings.normalizeString(reqData[field]); @@ -30,7 +30,7 @@ angular.module('ffffng').factory('NodeResource', function ( create: function (req, res) { var data = Resources.getData(req); - var node = getValidNodeData(data); + var node = getNormalizedNodeData(data); if (!isValidNode(node)) { return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); } @@ -51,7 +51,7 @@ angular.module('ffffng').factory('NodeResource', function ( return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); } - var node = getValidNodeData(data); + var node = getNormalizedNodeData(data); if (!isValidNode(node)) { return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); } @@ -95,14 +95,22 @@ angular.module('ffffng').factory('NodeResource', function ( }, getAll: function (req, res) { - // TODO: Paging + Sort + Filter - - return NodeService.getAllNodes(function (err, nodes) { + Resources.getValidRestParams('list', req, function (err, restParams) { if (err) { return Resources.error(res, err); } - return Resources.success(res, nodes); + // TODO: Sort + Filter + + return NodeService.getAllNodes(restParams._page, restParams._perPage, function (err, nodes, total) { + if (err) { + return Resources.error(res, err); + } + + res.set('X-Total-Count', total); + + return Resources.success(res, nodes); + }); }); } }; diff --git a/server/services/nodeService.js b/server/services/nodeService.js index e3b99f6..fd53c83 100644 --- a/server/services/nodeService.js +++ b/server/services/nodeService.js @@ -369,11 +369,13 @@ angular.module('ffffng') deleteNodeFile(token, callback); }, - getAllNodes: function (callback) { - var files = findNodeFiles({}); + getAllNodes: function (page, perPage, callback) { + var files = _.sortBy(findNodeFiles({})); + var total = files.length; + var pageFiles = files.slice((page - 1) * perPage, page * perPage); async.mapLimit( - files, + pageFiles, MAX_PARALLEL_NODES_PARSING, parseNodeFile, function (err, nodes) { @@ -382,7 +384,7 @@ angular.module('ffffng') return callback({data: 'Internal error.', type: ErrorTypes.internalError}); } - return callback(null, nodes); + return callback(null, nodes, total); } ); }, diff --git a/server/utils/resources.js b/server/utils/resources.js index ced07cf..fb634d2 100644 --- a/server/utils/resources.js +++ b/server/utils/resources.js @@ -1,6 +1,6 @@ 'use strict'; -angular.module('ffffng').factory('Resources', function (_) { +angular.module('ffffng').factory('Resources', function (_, Constraints, Validator, ErrorTypes) { function respond(res, httpCode, data) { res.writeHead(httpCode, {'Content-Type': 'application/json'}); res.end(JSON.stringify(data)); @@ -11,6 +11,31 @@ angular.module('ffffng').factory('Resources', function (_) { return _.extend({}, req.body, req.params, req.query); }, + getValidRestParams: function(type, req, callback) { + var constraints = Constraints.rest[type]; + if (!_.isPlainObject(constraints)) { + Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + } + + var data = this.getData(req); + + var restParams = {}; + _.each(_.keys(constraints), function (key) { + var value = data[key]; + restParams[key] = _.isUndefined(value) && !_.isUndefined(constraints[key].default) + ? constraints[key].default + : value; + }); + + var areValidParams = Validator.forConstraints(constraints); + if (!areValidParams(restParams)) { + return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest}); + } + + callback(null, restParams); + }, + success: function (res, data) { respond(res, 200, data); }, diff --git a/server/utils/strings.js b/server/utils/strings.js index eb5f12b..dcf3b24 100644 --- a/server/utils/strings.js +++ b/server/utils/strings.js @@ -17,6 +17,11 @@ angular.module('ffffng').factory('Strings', function (_) { } return macParts.join(':'); + }, + + parseInt: function (str) { + var parsed = _.parseInt(str, 10); + return parsed.toString() === str ? parsed : undefined; } }; }); diff --git a/server/validation/validator.js b/server/validation/validator.js index 0bc2e2f..d597d45 100644 --- a/server/validation/validator.js +++ b/server/validation/validator.js @@ -1,24 +1,64 @@ 'use strict'; -angular.module('ffffng').factory('Validator', function (_) { - var isValid = function (constraint, acceptUndefined, value) { - if (value === undefined) { - return acceptUndefined || constraint.optional; +angular.module('ffffng').factory('Validator', function (_, Strings, Logger) { + function isValidBoolean(value) { + return _.isBoolean(value); + } + + function isValidNumber(constraint, value) { + if (_.isString(value)) { + value = Strings.parseInt(value); } - if (constraint.type === 'boolean') { - return _.isBoolean(value); + if (!_.isNumber(value)) { + return false; } + if (_.isNaN(value) || !_.isFinite(value)) { + return false; + } + + if (_.isNumber(constraint.min) && value < constraint.min) { + return false; + } + + if (_.isNumber(constraint.max) && value > constraint.max) { + return false; + } + + return true; + } + + function isValidString(constraint, value) { if (!_.isString(value)) { return false; } var trimmed = value.trim(); return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex); - }; + } - var areValid = function (constraints, acceptUndefined, values) { + function isValid(constraint, acceptUndefined, value) { + if (value === undefined) { + return acceptUndefined || constraint.optional; + } + + switch (constraint.type) { + case 'boolean': + return isValidBoolean(value); + + case 'number': + return isValidNumber(constraint, value); + + case 'string': + return isValidString(constraint, value); + } + + Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type); + return false; + } + + function areValid(constraints, acceptUndefined, values) { var fields = Object.keys(constraints); for (var i = 0; i < fields.length; i ++) { var field = fields[i]; @@ -27,7 +67,7 @@ angular.module('ffffng').factory('Validator', function (_) { } } return true; - }; + } return { forConstraint: function (constraint, acceptUndefined) { diff --git a/shared/validation/constraints.js b/shared/validation/constraints.js index ab16918..4c8be92 100644 --- a/shared/validation/constraints.js +++ b/shared/validation/constraints.js @@ -46,5 +46,22 @@ angular.module('ffffng').constant('Constraints', { type: 'boolean', optional: false } + }, + rest: { + list: { + _page: { + type: 'number', + min: 1, + optional: true, + default: 1 + }, + _perPage: { + type: 'number', + min: 1, + max: 50, + optional: true, + default: 20 + } + } } });