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
+ }
+ }
}
});