Removing ng-di on the server.

This commit is contained in:
baldo 2018-12-17 22:49:54 +01:00
parent ddb2f47a9d
commit 8697d79ba5
37 changed files with 2838 additions and 2878 deletions

View file

@ -33,7 +33,6 @@
"lodash": "^4.17.11", "lodash": "^4.17.11",
"moment": "^2.22.2", "moment": "^2.22.2",
"ng-admin": "^1.0.13", "ng-admin": "^1.0.13",
"ng-di": "^0.2.1",
"node-cron": "^2.0.1", "node-cron": "^2.0.1",
"nodemailer": "^4.6.8", "nodemailer": "^4.6.8",
"nodemailer-html-to-text": "^3.0.0", "nodemailer-html-to-text": "^3.0.0",

View file

@ -1,16 +1,21 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('app', function (fs, config, _) { const _ = require('lodash')
var express = require('express'); const auth = require('http-auth');
var auth = require('http-auth'); const bodyParser = require('body-parser');
var bodyParser = require('body-parser'); const compress = require('compression');
var compress = require('compression'); const express = require('express');
const fs = require('graceful-fs')
var app = express(); const config = require('./config').config
var router = express.Router();
const app = express();
module.exports = (() => {
const router = express.Router();
// urls beneath /internal are protected // urls beneath /internal are protected
var internalAuth = auth.basic( const internalAuth = auth.basic(
{ {
realm: 'Knotenformular - Intern' realm: 'Knotenformular - Intern'
}, },
@ -27,24 +32,24 @@ angular.module('ffffng').factory('app', function (fs, config, _) {
router.use(bodyParser.json()); router.use(bodyParser.json());
router.use(bodyParser.urlencoded({ extended: true })); router.use(bodyParser.urlencoded({ extended: true }));
var adminDir = __dirname + '/../admin'; const adminDir = __dirname + '/../admin';
var clientDir = __dirname + '/../client'; const clientDir = __dirname + '/../client';
var templateDir = __dirname + '/templates'; const templateDir = __dirname + '/templates';
var jsTemplateFiles = [ const jsTemplateFiles = [
'/config.js' '/config.js'
]; ];
router.use(compress()); router.use(compress());
function serveTemplate(mimeType, req, res, next) { function serveTemplate (mimeType, req, res, next) {
return fs.readFile(templateDir + '/' + req.path, 'utf8', function (err, body) { return fs.readFile(templateDir + '/' + req.path, 'utf8', function (err, body) {
if (err) { if (err) {
return next(err); return next(err);
} }
res.writeHead(200, {'Content-Type': mimeType}); res.writeHead(200, { 'Content-Type': mimeType });
res.end(_.template(body)( { config: config.client })); res.end(_.template(body)({ config: config.client }));
return null; // to suppress warning return null; // to suppress warning
}); });
@ -63,4 +68,4 @@ angular.module('ffffng').factory('app', function (fs, config, _) {
app.use(config.server.rootPath, router); app.use(config.server.rootPath, router);
return app; return app;
}); })()

View file

@ -1,58 +1,59 @@
'use strict'; 'use strict';
var commandLineArgs = require('command-line-args'); module.exports = (() => {
var commandLineUsage = require('command-line-usage'); const commandLineArgs = require('command-line-args');
const commandLineUsage = require('command-line-usage');
var commandLineDefs = [ const commandLineDefs = [
{ name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' },
{ name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, { name: 'config', alias: 'c', type: String, description: 'Location of config.json' },
{ name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' }
]; ];
var commandLineOptions; let commandLineOptions;
try { try {
commandLineOptions = commandLineArgs(commandLineDefs); commandLineOptions = commandLineArgs(commandLineDefs);
} catch (e) { } catch (e) {
console.error(e.message); console.error(e.message);
console.error('Try \'--help\' for more information.'); console.error('Try \'--help\' for more information.');
process.exit(1); process.exit(1);
} }
var fs = require('graceful-fs'); const fs = require('graceful-fs');
var packageJsonFile = __dirname + '/../package.json'; const packageJsonFile = __dirname + '/../package.json';
var version = 'unknown'; let version = 'unknown';
if (fs.existsSync(packageJsonFile)) { if (fs.existsSync(packageJsonFile)) {
version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version;
} }
function usage() { function usage () {
console.log(commandLineUsage([ console.log(commandLineUsage([
{ {
header: 'ffffng - ' + version + ' - Freifunk node management form', header: 'ffffng - ' + version + ' - Freifunk node management form',
optionList: commandLineDefs optionList: commandLineDefs
} }
])); ]));
} }
if (commandLineOptions.help) { if (commandLineOptions.help) {
usage(); usage();
process.exit(0); process.exit(0);
} }
if (commandLineOptions.version) { if (commandLineOptions.version) {
console.log('ffffng - ' + version); console.log('ffffng - ' + version);
process.exit(0); process.exit(0);
} }
if (!commandLineOptions.config) { if (!commandLineOptions.config) {
usage(); usage();
process.exit(1); process.exit(1);
} }
var deepExtend = require('deep-extend'); const deepExtend = require('deep-extend');
var defaultConfig = { const defaultConfig = {
server: { server: {
baseUrl: 'http://localhost:8080', baseUrl: 'http://localhost:8080',
port: 8080, port: 8080,
@ -120,37 +121,38 @@ var defaultConfig = {
layers: {} layers: {}
} }
} }
}; };
var configJSONFile = commandLineOptions.config; const configJSONFile = commandLineOptions.config;
var configJSON = {}; let configJSON = {};
if (fs.existsSync(configJSONFile)) { if (fs.existsSync(configJSONFile)) {
configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8'));
} else { } else {
console.error('config.json not found: ' + configJSONFile); console.error('config.json not found: ' + configJSONFile);
process.exit(1); process.exit(1);
} }
var _ = require('lodash'); const _ = require('lodash');
function stripTrailingSlash(obj, field) { function stripTrailingSlash (obj, field) {
var url = obj[field]; const url = obj[field];
if (_.isString(url) && _.last(url) === '/') { if (_.isString(url) && _.last(url) === '/') {
obj[field] = url.substr(0, url.length - 1); obj[field] = url.substr(0, url.length - 1);
} }
} }
var config = deepExtend({}, defaultConfig, configJSON); const config = deepExtend({}, defaultConfig, configJSON);
stripTrailingSlash(config.server, 'baseUrl'); stripTrailingSlash(config.server, 'baseUrl');
stripTrailingSlash(config.client.map, 'mapUrl'); stripTrailingSlash(config.client.map, 'mapUrl');
var url = require('url'); const url = require('url');
config.server.rootPath = url.parse(config.server.baseUrl).pathname; config.server.rootPath = url.parse(config.server.baseUrl).pathname;
config.client.rootPath = config.server.rootPath; config.client.rootPath = config.server.rootPath;
module.exports = config; return {
config,
angular.module('ffffng').constant('config', config); version
angular.module('ffffng').constant('version', version); }
})()

View file

@ -5,7 +5,7 @@ const fs = require('graceful-fs');
const glob = util.promisify(require('glob')); const glob = util.promisify(require('glob'));
const path = require('path'); const path = require('path');
const config = require('../config'); const config = require('../config').config;
const Logger = require('../logger'); const Logger = require('../logger');
async function applyPatch(db, file) { async function applyPatch(db, file) {
@ -72,10 +72,7 @@ async function init() {
throw error; throw error;
} }
// WARNING: We have to use funtion() syntax here, to satisfy ng-di. m( module.exports.db = db;
return angular.module('ffffng').factory('Database', function () {
return db;
});
} }
module.exports = { module.exports = {

View file

@ -0,0 +1,18 @@
'use strict';
const Logger = require('../logger')
const NodeService = require('../services/nodeService')
module.exports = {
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
run: function (callback) {
NodeService.fixNodeFilenames(function (err) {
if (err) {
Logger.tag('nodes', 'fix-filenames').error('Error fixing filenames:', err);
}
callback();
});
}
}

View file

@ -0,0 +1,18 @@
'use strict';
const Logger = require('../logger')
const MailService = require('../services/mailService')
module.exports = {
description: 'Send pending emails (up to 5 attempts in case of failures).',
run: function (callback) {
MailService.sendPendingMails(function (err) {
if (err) {
Logger.tag('mail', 'queue').error('Error sending pending mails:', err);
}
callback();
});
}
}

View file

@ -0,0 +1,18 @@
'use strict';
const Logger = require('../logger')
const MonitoringService = require('../services/monitoringService')
module.exports = {
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
run: function (callback) {
MonitoringService.sendMonitoringMails(function (err) {
if (err) {
Logger.tag('monitoring', 'mail-sending').error('Error sending monitoring mails:', err);
}
callback();
});
}
}

View file

@ -0,0 +1,18 @@
'use strict';
const Logger = require('../logger')
const MonitoringService = require('../services/monitoringService')
module.exports = {
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
run: function (callback) {
MonitoringService.retrieveNodeInformation(function (err) {
if (err) {
Logger.tag('monitoring', 'information-retrieval').error('Error retrieving node data:', err);
}
callback();
});
}
}

View file

@ -0,0 +1,18 @@
'use strict';
const Logger = require('../logger')
const MonitoringService = require('../services/monitoringService')
module.exports = {
description: 'Delete nodes that are offline for more than 100 days.',
run: function (callback) {
MonitoringService.deleteOfflineNodes(function (err) {
if (err) {
Logger.tag('nodes', 'delete-offline').error('Error deleting offline nodes:', err);
}
callback();
});
}
}

View file

@ -1,17 +0,0 @@
'use strict';
angular.module('ffffng').factory('FixNodeFilenamesJob', function (NodeService, Logger) {
return {
description: 'Makes sure node files (holding fastd key, name, etc.) are correctly named.',
run: function (callback) {
NodeService.fixNodeFilenames(function (err) {
if (err) {
Logger.tag('nodes', 'fix-filenames').error('Error fixing filenames:', err);
}
callback();
});
}
};
});

View file

@ -1,17 +0,0 @@
'use strict';
angular.module('ffffng').factory('MailQueueJob', function (MailService, Logger) {
return {
description: 'Send pending emails (up to 5 attempts in case of failures).',
run: function (callback) {
MailService.sendPendingMails(function (err) {
if (err) {
Logger.tag('mail', 'queue').error('Error sending pending mails:', err);
}
callback();
});
}
};
});

View file

@ -1,17 +0,0 @@
'use strict';
angular.module('ffffng').factory('MonitoringMailsSendingJob', function (MonitoringService, Logger) {
return {
description: 'Sends monitoring emails depending on the monitoring state of nodes retrieved by the NodeInformationRetrievalJob.',
run: function (callback) {
MonitoringService.sendMonitoringMails(function (err) {
if (err) {
Logger.tag('monitoring', 'mail-sending').error('Error sending monitoring mails:', err);
}
callback();
});
}
};
});

View file

@ -1,17 +0,0 @@
'use strict';
angular.module('ffffng').factory('NodeInformationRetrievalJob', function (MonitoringService, Logger) {
return {
description: 'Fetches the nodes.json and calculates and stores the monitoring / online status for registered nodes.',
run: function (callback) {
MonitoringService.retrieveNodeInformation(function (err) {
if (err) {
Logger.tag('monitoring', 'information-retrieval').error('Error retrieving node data:', err);
}
callback();
});
}
};
});

View file

@ -1,17 +0,0 @@
'use strict';
angular.module('ffffng').factory('OfflineNodesDeletionJob', function (MonitoringService, Logger) {
return {
description: 'Delete nodes that are offline for more than 100 days.',
run: function (callback) {
MonitoringService.deleteOfflineNodes(function (err) {
if (err) {
Logger.tag('nodes', 'delete-offline').error('Error deleting offline nodes:', err);
}
callback();
});
}
};
});

View file

@ -1,29 +1,31 @@
'use strict'; 'use strict';
var glob = require('glob'); const _ = require('lodash');
var _ = require('lodash'); const cron = require('node-cron');
const glob = require('glob');
const moment = require('moment');
var jobFiles = glob.sync(__dirname + '/*Job.js'); const config = require('../config').config
const Logger = require('../logger')
const jobFiles = glob.sync(__dirname + '/*Job.js');
_.each(jobFiles, function (jobFile) { _.each(jobFiles, function (jobFile) {
require(jobFile); require(jobFile);
}); });
angular.module('ffffng').factory('Scheduler', function ($injector, Logger, config, moment) { const tasks = {};
var cron = require('node-cron');
var tasks = {}; let taskId = 1;
function nextTaskId() {
var taskId = 1; const id = taskId;
function nextTaskId() {
var id = taskId;
taskId += 1; taskId += 1;
return id; return id;
} }
function schedule(expr, jobName) { function schedule(expr, jobName) {
Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName); Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName);
var job = $injector.get(jobName); var job = require(`../jobs/${jobName}`);
if (!_.isFunction(job.run)) { if (!_.isFunction(job.run)) {
throw new Error('The job ' + jobName + ' does not provide a "run" function.'); throw new Error('The job ' + jobName + ' does not provide a "run" function.');
@ -67,9 +69,9 @@ angular.module('ffffng').factory('Scheduler', function ($injector, Logger, confi
cron.schedule(expr, task.run); cron.schedule(expr, task.run);
tasks['' + id] = task; tasks['' + id] = task;
} }
return { module.exports = {
init: function () { init: function () {
Logger.tag('jobs').info('Scheduling background jobs...'); Logger.tag('jobs').info('Scheduling background jobs...');
@ -94,5 +96,4 @@ angular.module('ffffng').factory('Scheduler', function ($injector, Logger, confi
getTasks: function () { getTasks: function () {
return tasks; return tasks;
} }
}; }
});

View file

@ -1,24 +0,0 @@
'use strict';
(function () {
var module = angular.module('ffffng');
function lib(name, nodeModule) {
if (!nodeModule) {
nodeModule = name;
}
module.factory(name, function () {
return require(nodeModule);
});
}
lib('_', 'lodash');
lib('async');
lib('crypto');
lib('deepExtend', 'deep-extend');
lib('fs', 'graceful-fs');
lib('glob');
lib('moment');
lib('request');
})();

View file

@ -1,7 +1,7 @@
'use strict'; 'use strict';
var config = require('./config'); const app = require('./app');
const config = require('./config').config;
// Hack to allow proper logging of Error. // Hack to allow proper logging of Error.
Object.defineProperty(Error.prototype, 'message', { Object.defineProperty(Error.prototype, 'message', {
@ -14,7 +14,7 @@ Object.defineProperty(Error.prototype, 'stack', {
}); });
var scribe = require('scribe-js')({ const scribe = require('scribe-js')({
rootPath: config.server.logging.directory, rootPath: config.server.logging.directory,
}); });
@ -33,30 +33,26 @@ function addLogger(name, color, active) {
addLogger('debug', 'grey', config.server.logging.debug); addLogger('debug', 'grey', config.server.logging.debug);
addLogger('profile', 'blue', config.server.logging.profile); addLogger('profile', 'blue', config.server.logging.profile);
angular.module('ffffng').factory('Logger', function (app) { if (config.server.logging.logRequests) {
if (config.server.logging.logRequests) {
app.use(scribe.express.logger()); app.use(scribe.express.logger());
} }
if (config.server.internal.active) { if (config.server.internal.active) {
var prefix = config.server.rootPath === '/' ? '' : config.server.rootPath; const prefix = config.server.rootPath === '/' ? '' : config.server.rootPath;
app.use(prefix + '/internal/logs', scribe.webPanel()); app.use(prefix + '/internal/logs', scribe.webPanel());
} }
// Hack to allow correct logging of node.js Error objects. // Hack to allow correct logging of node.js Error objects.
// See: https://github.com/bluejamesbond/Scribe.js/issues/70 // See: https://github.com/bluejamesbond/Scribe.js/issues/70
Object.defineProperty(Error.prototype, 'toJSON', { Object.defineProperty(Error.prototype, 'toJSON', {
configurable: true, configurable: true,
value: function () { value: function () {
var alt = {}; const alt = {};
var storeKey = function (key) { const storeKey = function (key) {
alt[key] = this[key]; alt[key] = this[key];
}; };
Object.getOwnPropertyNames(this).forEach(storeKey, this); Object.getOwnPropertyNames(this).forEach(storeKey, this);
return alt; return alt;
} }
});
return process.console;
}); });
module.exports = process.console; module.exports = process.console;

View file

@ -2,11 +2,6 @@
/*jslint node: true */ /*jslint node: true */
'use strict'; 'use strict';
// Dirty hack to allow usage of angular modules.
global.angular = require('ng-di');
angular.module('ffffng', []);
(function () { (function () {
// Use graceful-fs instead of fs also in all libraries to have more robust fs handling. // Use graceful-fs instead of fs also in all libraries to have more robust fs handling.
const realFs = require('fs'); const realFs = require('fs');
@ -14,52 +9,24 @@ angular.module('ffffng', []);
gracefulFs.gracefulify(realFs); gracefulFs.gracefulify(realFs);
})(); })();
require('./config'); const config = require('./config').config;
require('./logger').tag('main', 'startup').info('Server starting up...'); const Logger = require('./logger')
Logger.tag('main', 'startup').info('Server starting up...');
require('./app'); require('./db/database').init()
require('./router'); .then(() => {
require('./libs');
require('./utils/databaseUtil');
require('./utils/errorTypes');
require('./utils/resources');
require('./utils/strings');
require('./utils/urlBuilder');
require('./resources/versionResource');
require('./resources/statisticsResource');
require('./resources/frontendResource');
require('./resources/taskResource');
require('./resources/mailResource');
require('./resources/nodeResource');
require('./resources/monitoringResource');
require('./services/mailService');
require('./services/mailTemplateService');
require('./services/nodeService');
require('./services/monitoringService');
require('../shared/validation/constraints');
require('./validation/validator');
require('./jobs/scheduler');
const db = require('./db/database');
db.init().then(() => {
// WARNING: We have to use funtion() syntax here, to satisfy ng-di. m(
angular.injector(['ffffng']).invoke(function (config, app, Logger, Scheduler, Router) {
Logger.tag('main').info('Initializing...'); Logger.tag('main').info('Initializing...');
Scheduler.init(); const app = require('./app');
Router.init();
require('./jobs/scheduler').init();
require('./router').init();
app.listen(config.server.port, '::'); app.listen(config.server.port, '::');
module.exports = app; module.exports = app;
}); })
}).catch(error => { .catch(error => {
console.error('Could not init database: ', error); console.error('Could not init database: ', error);
process.exit(1); process.exit(1);
}); });

View file

@ -1,16 +1,16 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('FrontendResource', function ( const fs = require('graceful-fs')
Logger,
Resources,
ErrorTypes,
fs
) {
var indexHtml = __dirname + '/../../client/index.html';
return { const ErrorTypes = require('../utils/errorTypes')
render: function (req, res) { const Logger = require('../logger')
var data = Resources.getData(req); const Resources = require('../utils/resources')
const indexHtml = __dirname + '/../../client/index.html';
module.exports = {
render (req, res) {
const data = Resources.getData(req);
fs.readFile(indexHtml, 'utf8', function (err, body) { fs.readFile(indexHtml, 'utf8', function (err, body) {
if (err) { if (err) {
@ -27,5 +27,4 @@ angular.module('ffffng').factory('FrontendResource', function (
); );
}); });
} }
}; }
});

View file

@ -1,28 +1,27 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('MailResource', function ( const Constraints = require('../../shared/validation/constraints')
Constraints, const ErrorTypes = require('../utils/errorTypes')
Validator, const Logger = require('../logger')
MailService, const MailService = require('../services/mailService')
Resources, const Resources = require('../utils/resources')
Logger, const Strings = require('../utils/strings')
ErrorTypes, const Validator = require('../validation/validator')
Strings
) {
var isValidId = Validator.forConstraint(Constraints.id);
function withValidMailId(req, res, callback) { const isValidId = Validator.forConstraint(Constraints.id);
var id = Strings.normalizeString(Resources.getData(req).id);
function withValidMailId(req, res, callback) {
const id = Strings.normalizeString(Resources.getData(req).id);
if (!isValidId(id)) { if (!isValidId(id)) {
return callback({data: 'Invalid mail id.', type: ErrorTypes.badRequest}); return callback({data: 'Invalid mail id.', type: ErrorTypes.badRequest});
} }
callback(null, id); callback(null, id);
} }
return { module.exports = {
get: function (req, res) { get (req, res) {
withValidMailId(req, res, function (err, id) { withValidMailId(req, res, function (err, id) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -43,7 +42,7 @@ angular.module('ffffng').factory('MailResource', function (
}); });
}, },
getAll: function (req, res) { getAll (req, res) {
Resources.getValidRestParams('list', null, req, function (err, restParams) { Resources.getValidRestParams('list', null, req, function (err, restParams) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -64,7 +63,7 @@ angular.module('ffffng').factory('MailResource', function (
}); });
}, },
delete: function (req, res) { delete (req, res) {
withValidMailId(req, res, function (err, id) { withValidMailId(req, res, function (err, id) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -81,7 +80,7 @@ angular.module('ffffng').factory('MailResource', function (
}); });
}, },
resetFailures: function (req, res) { resetFailures (req, res) {
withValidMailId(req, res, function (err, id) { withValidMailId(req, res, function (err, id) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -97,5 +96,4 @@ angular.module('ffffng').factory('MailResource', function (
}); });
}); });
} }
}; }
});

View file

@ -1,19 +1,19 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('MonitoringResource', function ( const _ = require('lodash')
Constraints,
Validator,
MonitoringService,
Logger,
_,
Strings,
Resources,
ErrorTypes
) {
var isValidToken = Validator.forConstraint(Constraints.token);
return { const Constraints = require('../../shared/validation/constraints')
getAll: function (req, res) { const ErrorTypes = require('../utils/errorTypes')
const Logger = require('../logger')
const MonitoringService = require('../services/monitoringService')
const Resources = require('../utils/resources')
const Strings = require('../utils/strings')
const Validator = require('../validation/validator')
const isValidToken = Validator.forConstraint(Constraints.token);
module.exports = {
getAll (req, res) {
Resources.getValidRestParams('list', null, req, function (err, restParams) { Resources.getValidRestParams('list', null, req, function (err, restParams) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -37,10 +37,10 @@ angular.module('ffffng').factory('MonitoringResource', function (
}); });
}, },
confirm: function (req, res) { confirm (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var token = Strings.normalizeString(data.token); const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
@ -59,10 +59,10 @@ angular.module('ffffng').factory('MonitoringResource', function (
}); });
}, },
disable: function (req, res) { disable (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var token = Strings.normalizeString(data.token); const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
@ -79,5 +79,4 @@ angular.module('ffffng').factory('MonitoringResource', function (
}); });
}); });
} }
}; }
});

View file

@ -1,39 +1,39 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('NodeResource', function ( const _ = require('lodash')
Constraints, const deepExtend = require('deep-extend')
Validator,
Logger,
MonitoringService,
NodeService,
_,
deepExtend,
Strings,
Resources,
ErrorTypes
) {
var nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
function getNormalizedNodeData(reqData) { const Constraints = require('../../shared/validation/constraints')
var node = {}; const ErrorTypes = require('../utils/errorTypes')
const Logger = require('../logger')
const MonitoringService = require('../services/monitoringService')
const NodeService = require('../services/nodeService')
const Strings = require('../utils/strings')
const Validator = require('../validation/validator')
const Resources = require('../utils/resources')
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
function getNormalizedNodeData(reqData) {
const node = {};
_.each(nodeFields, function (field) { _.each(nodeFields, function (field) {
var value = Strings.normalizeString(reqData[field]); let value = Strings.normalizeString(reqData[field]);
if (field === 'mac') { if (field === 'mac') {
value = Strings.normalizeMac(value); value = Strings.normalizeMac(value);
} }
node[field] = value; node[field] = value;
}); });
return node; return node;
} }
var isValidNode = Validator.forConstraints(Constraints.node); const isValidNode = Validator.forConstraints(Constraints.node);
var isValidToken = Validator.forConstraint(Constraints.token); const isValidToken = Validator.forConstraint(Constraints.token);
return { module.exports = {
create: function (req, res) { create: function (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var node = getNormalizedNodeData(data); const node = getNormalizedNodeData(data);
if (!isValidNode(node)) { if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
} }
@ -47,14 +47,14 @@ angular.module('ffffng').factory('NodeResource', function (
}, },
update: function (req, res) { update: function (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var token = Strings.normalizeString(data.token); const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
var node = getNormalizedNodeData(data); const node = getNormalizedNodeData(data);
if (!isValidNode(node)) { if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
} }
@ -68,9 +68,9 @@ angular.module('ffffng').factory('NodeResource', function (
}, },
delete: function (req, res) { delete: function (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var token = Strings.normalizeString(data.token); const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
@ -84,7 +84,7 @@ angular.module('ffffng').factory('NodeResource', function (
}, },
get: function (req, res) { get: function (req, res) {
var token = Strings.normalizeString(Resources.getData(req).token); const token = Strings.normalizeString(Resources.getData(req).token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
} }
@ -108,12 +108,12 @@ angular.module('ffffng').factory('NodeResource', function (
return Resources.error(res, err); return Resources.error(res, err);
} }
var realNodes = _.filter(nodes, function (node) { const realNodes = _.filter(nodes, function (node) {
// We ignore nodes without tokens as those are only manually added ones like gateways. // We ignore nodes without tokens as those are only manually added ones like gateways.
return node.token; return node.token;
}); });
var macs = _.map(realNodes, function (node) { const macs = _.map(realNodes, function (node) {
return node.mac; return node.mac;
}); });
@ -123,8 +123,8 @@ angular.module('ffffng').factory('NodeResource', function (
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
var enhancedNodes = _.map(realNodes, function (node) { const enhancedNodes = _.map(realNodes, function (node) {
var nodeState = nodeStateByMac[node.mac]; const nodeState = nodeStateByMac[node.mac];
if (nodeState) { if (nodeState) {
return deepExtend({}, node, { return deepExtend({}, node, {
site: nodeState.site, site: nodeState.site,
@ -136,7 +136,7 @@ angular.module('ffffng').factory('NodeResource', function (
return node; return node;
}); });
var filteredNodes = Resources.filter( const filteredNodes = Resources.filter(
enhancedNodes, enhancedNodes,
[ [
'hostname', 'hostname',
@ -151,9 +151,9 @@ angular.module('ffffng').factory('NodeResource', function (
], ],
restParams restParams
); );
var total = filteredNodes.length; const total = filteredNodes.length;
var sortedNodes = Resources.sort( const sortedNodes = Resources.sort(
filteredNodes, filteredNodes,
[ [
'hostname', 'hostname',
@ -170,7 +170,7 @@ angular.module('ffffng').factory('NodeResource', function (
], ],
restParams restParams
); );
var pageNodes = Resources.getPageEntities(sortedNodes, restParams); const pageNodes = Resources.getPageEntities(sortedNodes, restParams);
res.set('X-Total-Count', total); res.set('X-Total-Count', total);
return Resources.success(res, pageNodes); return Resources.success(res, pageNodes);
@ -178,5 +178,4 @@ angular.module('ffffng').factory('NodeResource', function (
}); });
}); });
} }
}; }
});

View file

@ -1,14 +1,13 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('StatisticsResource', function ( const ErrorTypes = require('../utils/errorTypes')
Logger, const Logger = require('../logger')
NodeService, const NodeService = require('../services/nodeService')
Resources, const Resources = require('../utils/resources')
ErrorTypes
) { module.exports = {
return { get (req, res) {
get: function (req, res) { NodeService.getNodeStatistics((err, nodeStatistics) => {
NodeService.getNodeStatistics(function (err, nodeStatistics) {
if (err) { if (err) {
Logger.tag('statistics').error('Error getting statistics:', err); Logger.tag('statistics').error('Error getting statistics:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
@ -22,5 +21,4 @@ angular.module('ffffng').factory('StatisticsResource', function (
); );
}); });
} }
}; }
});

View file

@ -1,17 +1,17 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('TaskResource', function ( const _ = require('lodash')
Constraints,
Validator,
_,
Strings,
Resources,
ErrorTypes,
Scheduler
) {
var isValidId = Validator.forConstraint(Constraints.id);
function toExternalTask(task) { const Constraints = require('../../shared/validation/constraints')
const ErrorTypes = require('../utils/errorTypes')
const Resources = require('../utils/resources')
const Scheduler = require('../jobs/scheduler')
const Strings = require('../utils/strings')
const Validator = require('../validation/validator')
const isValidId = Validator.forConstraint(Constraints.id);
function toExternalTask(task) {
return { return {
id: task.id, id: task.id,
name: task.name, name: task.name,
@ -23,30 +23,30 @@ angular.module('ffffng').factory('TaskResource', function (
state: task.state, state: task.state,
enabled: task.enabled enabled: task.enabled
}; };
} }
function withValidTaskId(req, res, callback) { function withValidTaskId(req, res, callback) {
var id = Strings.normalizeString(Resources.getData(req).id); const id = Strings.normalizeString(Resources.getData(req).id);
if (!isValidId(id)) { if (!isValidId(id)) {
return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest}); return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest});
} }
callback(null, id); callback(null, id);
} }
function getTask(id, callback) { function getTask(id, callback) {
var tasks = Scheduler.getTasks(); const tasks = Scheduler.getTasks();
var task = tasks[id]; const task = tasks[id];
if (!task) { if (!task) {
return callback({data: 'Task not found.', type: ErrorTypes.notFound}); return callback({data: 'Task not found.', type: ErrorTypes.notFound});
} }
callback(null, task); callback(null, task);
} }
function withTask(req, res, callback) { function withTask(req, res, callback) {
withValidTaskId(req, res, function (err, id) { withValidTaskId(req, res, function (err, id) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -60,9 +60,9 @@ angular.module('ffffng').factory('TaskResource', function (
callback(null, task); callback(null, task);
}); });
}); });
} }
function setTaskEnabled(req, res, enable) { function setTaskEnabled(req, res, enable) {
withTask(req, res, function (err, task) { withTask(req, res, function (err, task) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -72,35 +72,35 @@ angular.module('ffffng').factory('TaskResource', function (
return Resources.success(res, toExternalTask(task)); return Resources.success(res, toExternalTask(task));
}); });
} }
return { module.exports = {
getAll: function (req, res) { getAll (req, res) {
Resources.getValidRestParams('list', null, req, function (err, restParams) { Resources.getValidRestParams('list', null, req, function (err, restParams) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
} }
var tasks = Resources.sort( const tasks = Resources.sort(
_.values(Scheduler.getTasks()), _.values(Scheduler.getTasks()),
['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], ['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'],
restParams restParams
); );
var filteredTasks = Resources.filter( const filteredTasks = Resources.filter(
tasks, tasks,
['id', 'name', 'schedule', 'state'], ['id', 'name', 'schedule', 'state'],
restParams restParams
); );
var total = filteredTasks.length; const total = filteredTasks.length;
var pageTasks = Resources.getPageEntities(filteredTasks, restParams); const pageTasks = Resources.getPageEntities(filteredTasks, restParams);
res.set('X-Total-Count', total); res.set('X-Total-Count', total);
return Resources.success(res, _.map(pageTasks, toExternalTask)); return Resources.success(res, _.map(pageTasks, toExternalTask));
}); });
}, },
run: function (req, res) { run (req, res) {
withTask(req, res, function (err, task) { withTask(req, res, function (err, task) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
@ -116,12 +116,11 @@ angular.module('ffffng').factory('TaskResource', function (
}); });
}, },
enable: function (req, res) { enable (req, res) {
setTaskEnabled(req, res, true); setTaskEnabled(req, res, true);
}, },
disable: function (req, res) { disable (req, res) {
setTaskEnabled(req, res, false); setTaskEnabled(req, res, false);
} }
}; }
});

View file

@ -1,17 +1,15 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('VersionResource', function ( const Resources = require('../utils/resources')
version, const version = require('../config').version
Resources
) { module.exports = {
return { get (req, res) {
get: function (req, res) {
return Resources.success( return Resources.success(
res, res,
{ {
version: version version
} }
); );
} }
}; }
});

View file

@ -1,20 +1,20 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Router', function ( const express = require('express');
app,
VersionResource, const app = require('./app')
StatisticsResource, const config = require('./config').config
FrontendResource, const VersionResource = require('./resources/versionResource')
NodeResource, const StatisticsResource = require('./resources/statisticsResource')
MonitoringResource, const FrontendResource = require('./resources/frontendResource')
TaskResource, const NodeResource = require('./resources/nodeResource')
MailResource, const MonitoringResource = require('./resources/monitoringResource')
config const TaskResource = require('./resources/taskResource')
) { const MailResource = require('./resources/mailResource')
return {
init: function () { module.exports = {
var express = require('express'); init () {
var router = express.Router(); const router = express.Router();
router.post('/', FrontendResource.render); router.post('/', FrontendResource.render);
@ -49,5 +49,4 @@ angular.module('ffffng').factory('Router', function (
app.use(config.server.rootPath, router); app.use(config.server.rootPath, router);
} }
}; }
});

View file

@ -1,33 +1,31 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('MailService', function ( const async = require('async')
Database, const deepExtend = require('deep-extend')
MailTemplateService, const moment = require('moment')
config,
_,
async,
deepExtend,
fs,
moment,
Logger,
Resources
) {
var MAIL_QUEUE_DB_BATCH_SIZE = 50;
var MAIL_QUEUE_MAX_PARALLEL_SENDING = 3;
var transporter = require('nodemailer').createTransport(deepExtend( const config = require('../config').config
const Database = require('../db/database').db
const Logger = require('../logger')
const MailTemplateService = require('./mailTemplateService')
const Resources = require('../utils/resources')
const MAIL_QUEUE_DB_BATCH_SIZE = 50;
const MAIL_QUEUE_MAX_PARALLEL_SENDING = 3;
const transporter = require('nodemailer').createTransport(deepExtend(
{}, {},
config.server.email.smtp, config.server.email.smtp,
{ {
transport: 'smtp', transport: 'smtp',
pool: true pool: true
} }
)); ));
MailTemplateService.configureTransporter(transporter); MailTemplateService.configureTransporter(transporter);
function sendMail(options, callback) { function sendMail(options, callback) {
Logger Logger
.tag('mail', 'queue') .tag('mail', 'queue')
.info( .info(
@ -41,7 +39,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var mailOptions = { const mailOptions = {
from: options.sender, from: options.sender,
to: options.recipient, to: options.recipient,
subject: renderedTemplate.subject, subject: renderedTemplate.subject,
@ -59,9 +57,9 @@ angular.module('ffffng')
}); });
} }
); );
} }
function findPendingMailsBefore(beforeMoment, limit, callback) { function findPendingMailsBefore(beforeMoment, limit, callback) {
Database.all( Database.all(
'SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?', 'SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?',
[beforeMoment.unix(), 5, limit], [beforeMoment.unix(), 5, limit],
@ -70,7 +68,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var pendingMails; let pendingMails;
try { try {
pendingMails = _.map(rows, function (row) { pendingMails = _.map(rows, function (row) {
return deepExtend( return deepExtend(
@ -89,22 +87,22 @@ angular.module('ffffng')
callback(null, pendingMails); callback(null, pendingMails);
} }
); );
} }
function removePendingMailFromQueue(id, callback) { function removePendingMailFromQueue(id, callback) {
Database.run('DELETE FROM email_queue WHERE id = ?', [id], callback); Database.run('DELETE FROM email_queue WHERE id = ?', [id], callback);
} }
function incrementFailureCounterForPendingEmail(id, callback) { function incrementFailureCounterForPendingEmail(id, callback) {
var now = moment(); const now = moment();
Database.run( Database.run(
'UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?', 'UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?',
[now.unix(), id], [now.unix(), id],
callback callback
); );
} }
function sendPendingMail(pendingMail, callback) { function sendPendingMail(pendingMail, callback) {
sendMail(pendingMail, function (err) { sendMail(pendingMail, function (err) {
if (err) { if (err) {
// we only log the error and increment the failure counter as we want to continue with pending mails // we only log the error and increment the failure counter as we want to continue with pending mails
@ -120,14 +118,14 @@ angular.module('ffffng')
removePendingMailFromQueue(pendingMail.id, callback); removePendingMailFromQueue(pendingMail.id, callback);
}); });
} }
function doGetMail(id, callback) { function doGetMail(id, callback) {
Database.get('SELECT * FROM email_queue WHERE id = ?', [id], callback); Database.get('SELECT * FROM email_queue WHERE id = ?', [id], callback);
} }
return { module.exports = {
enqueue: function (sender, recipient, email, data, callback) { enqueue (sender, recipient, email, data, callback) {
if (!_.isPlainObject(data)) { if (!_.isPlainObject(data)) {
return callback(new Error('Unexpected data: ' + data)); return callback(new Error('Unexpected data: ' + data));
} }
@ -142,11 +140,11 @@ angular.module('ffffng')
); );
}, },
getMail: function (id, callback) { getMail (id, callback) {
doGetMail(id, callback); doGetMail(id, callback);
}, },
getPendingMails: function (restParams, callback) { getPendingMails (restParams, callback) {
Database.get( Database.get(
'SELECT count(*) AS total FROM email_queue', 'SELECT count(*) AS total FROM email_queue',
[], [],
@ -155,9 +153,9 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var total = row.total; const total = row.total;
var filter = Resources.filterClause( const filter = Resources.filterClause(
restParams, restParams,
'id', 'id',
['id', 'failures', 'sender', 'recipient', 'email', 'created_at', 'modified_at'], ['id', 'failures', 'sender', 'recipient', 'email', 'created_at', 'modified_at'],
@ -179,11 +177,11 @@ angular.module('ffffng')
); );
}, },
deleteMail: function (id, callback) { deleteMail (id, callback) {
removePendingMailFromQueue(id, callback); removePendingMailFromQueue(id, callback);
}, },
resetFailures: function (id, callback) { resetFailures (id, callback) {
Database.run( Database.run(
'UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?', 'UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?',
[moment().unix(), id], [moment().unix(), id],
@ -201,12 +199,12 @@ angular.module('ffffng')
); );
}, },
sendPendingMails: function (callback) { sendPendingMails (callback) {
Logger.tag('mail', 'queue').debug('Start sending pending mails...'); Logger.tag('mail', 'queue').debug('Start sending pending mails...');
var startTime = moment(); const startTime = moment();
var sendNextBatch = function (err) { const sendNextBatch = function (err) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -234,5 +232,4 @@ angular.module('ffffng')
sendNextBatch(null); sendNextBatch(null);
} }
}; }
});

View file

@ -1,23 +1,22 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('MailTemplateService', function ( const async = require('async')
UrlBuilder, const deepExtend = require('deep-extend')
config, const fs = require('graceful-fs')
_, const moment = require('moment')
async,
deepExtend,
fs,
moment,
Logger
) {
var templateBasePath = __dirname + '/../mailTemplates';
var snippetsBasePath = templateBasePath + '/snippets';
var templateFunctions = {}; const config = require('../config').config
const Logger = require('../logger')
const UrlBuilder = require('../utils/urlBuilder')
function renderSnippet(name, data) { const templateBasePath = __dirname + '/../mailTemplates';
var snippetFile = snippetsBasePath + '/' + name + '.html'; const snippetsBasePath = templateBasePath + '/snippets';
const templateFunctions = {};
function renderSnippet(name, data) {
const snippetFile = snippetsBasePath + '/' + name + '.html';
return _.template(fs.readFileSync(snippetFile).toString())(deepExtend( return _.template(fs.readFileSync(snippetFile).toString())(deepExtend(
{}, {},
@ -27,58 +26,58 @@ angular.module('ffffng')
data, data,
templateFunctions templateFunctions
)); ));
} }
function snippet(name) { function snippet(name) {
return function (data) { return function (data) {
return renderSnippet.bind(this)(name, data); return renderSnippet.bind(this)(name, data);
}; };
} }
function renderLink(href, text) { function renderLink(href, text) {
return _.template( return _.template(
'<a href="<%- href %>#" style="color: #E5287A;"><%- text %></a>' '<a href="<%- href %>#" style="color: #E5287A;"><%- text %></a>'
)({ )({
href: href, href: href,
text: text || href text: text || href
}); });
} }
function renderHR() { function renderHR() {
return '<hr style="border-top: 1px solid #333333; border-left: 0; border-right: 0; border-bottom: 0;" />'; return '<hr style="border-top: 1px solid #333333; border-left: 0; border-right: 0; border-bottom: 0;" />';
} }
function formatDateTime(unix) { function formatDateTime(unix) {
return moment.unix(unix).locale('de').local().format('DD.MM.YYYY HH:mm'); return moment.unix(unix).locale('de').local().format('DD.MM.YYYY HH:mm');
} }
function formatFromNow(unix) { function formatFromNow(unix) {
return moment.unix(unix).locale('de').fromNow(); return moment.unix(unix).locale('de').fromNow();
} }
templateFunctions.header = snippet('header'); templateFunctions.header = snippet('header');
templateFunctions.footer = snippet('footer'); templateFunctions.footer = snippet('footer');
templateFunctions.monitoringFooter = snippet('monitoring-footer'); templateFunctions.monitoringFooter = snippet('monitoring-footer');
templateFunctions.snippet = renderSnippet; templateFunctions.snippet = renderSnippet;
templateFunctions.link = renderLink; templateFunctions.link = renderLink;
templateFunctions.hr = renderHR; templateFunctions.hr = renderHR;
templateFunctions.formatDateTime = formatDateTime; templateFunctions.formatDateTime = formatDateTime;
templateFunctions.formatFromNow = formatFromNow; templateFunctions.formatFromNow = formatFromNow;
return { module.exports = {
configureTransporter: function (transporter) { configureTransporter (transporter) {
var htmlToText = require('nodemailer-html-to-text').htmlToText; const htmlToText = require('nodemailer-html-to-text').htmlToText;
transporter.use('compile', htmlToText({ transporter.use('compile', htmlToText({
tables: ['.table'] tables: ['.table']
})); }));
}, },
render: function (mailOptions, callback) { render (mailOptions, callback) {
var templatePathPrefix = templateBasePath + '/' + mailOptions.email; const templatePathPrefix = templateBasePath + '/' + mailOptions.email;
async.parallel({ async.parallel({
subject: _.partial(fs.readFile, templatePathPrefix + '.subject.txt'), subject: _.partial(fs.readFile, templatePathPrefix + '.subject.txt'),
@ -89,7 +88,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var data = deepExtend( const data = deepExtend(
{}, {},
mailOptions.data, mailOptions.data,
{ {
@ -99,18 +98,17 @@ angular.module('ffffng')
templateFunctions templateFunctions
); );
function render(field) { function render (field) {
return _.template(templates[field].toString())(data); return _.template(templates[field].toString())(data);
} }
var renderedTemplate; let renderedTemplate;
try { try {
renderedTemplate = { renderedTemplate = {
subject: _.trim(render('subject')), subject: _.trim(render('subject')),
body: render('body') body: render('body')
}; };
} } catch (error) {
catch (error) {
Logger Logger
.tag('mail', 'template') .tag('mail', 'template')
.error('Error rendering template for mail[' + mailOptions.id + ']:', error); .error('Error rendering template for mail[' + mailOptions.id + ']:', error);
@ -121,5 +119,4 @@ angular.module('ffffng')
} }
); );
} }
}; }
});

View file

@ -1,43 +1,41 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('MonitoringService', function ( const async = require('async')
_, const moment = require('moment')
async, const request = require('request')
config,
deepExtend, const config = require('../config').config
Database, const Constraints = require('../../shared/validation/constraints')
DatabaseUtil, const Database = require('../db/database').db
ErrorTypes, const DatabaseUtil = require('../utils/databaseUtil')
Logger, const ErrorTypes = require('../utils/errorTypes')
moment, const Logger = require('../logger')
MailService, const MailService = require('../services/mailService')
NodeService, const NodeService = require('../services/nodeService')
request, const Resources = require('../utils/resources')
Strings, const Strings = require('../utils/strings')
UrlBuilder, const UrlBuilder = require('../utils/urlBuilder')
Validator, const Validator = require('../validation/validator')
Constraints,
Resources const MONITORING_STATE_MACS_CHUNK_SIZE = 100;
) { const MONITORING_MAILS_DB_BATCH_SIZE = 50;
var MONITORING_STATE_MACS_CHUNK_SIZE = 100; /**
var MONITORING_MAILS_DB_BATCH_SIZE = 50;
/**
* Defines the intervals emails are sent if a node is offline * Defines the intervals emails are sent if a node is offline
*/ */
var MONITORING_OFFLINE_MAILS_SCHEDULE = { const MONITORING_OFFLINE_MAILS_SCHEDULE = {
1: { amount: 3, unit: 'hours' }, 1: { amount: 3, unit: 'hours' },
2: { amount: 1, unit: 'days' }, 2: { amount: 1, unit: 'days' },
3: { amount: 7, unit: 'days' } 3: { amount: 7, unit: 'days' }
}; };
var DELETE_OFFLINE_NODES_AFTER_DURATION = { const DELETE_OFFLINE_NODES_AFTER_DURATION = {
amount: 100, amount: 100,
unit: 'days' unit: 'days'
}; };
var previousImportTimestamp = null; let previousImportTimestamp = null;
function insertNodeInformation(nodeData, node, callback) { function insertNodeInformation(nodeData, node, callback) {
Logger Logger
.tag('monitoring', 'information-retrieval') .tag('monitoring', 'information-retrieval')
.debug('Node is new in monitoring, creating data: %s', nodeData.mac); .debug('Node is new in monitoring, creating data: %s', nodeData.mac);
@ -60,9 +58,9 @@ angular.module('ffffng')
], ],
callback callback
); );
} }
function updateNodeInformation(nodeData, node, row, callback) { function updateNodeInformation(nodeData, node, row, callback) {
Logger Logger
.tag('monitoring', 'information-retrieval') .tag('monitoring', 'information-retrieval')
.debug('Node is known in monitoring: %s', nodeData.mac); .debug('Node is known in monitoring: %s', nodeData.mac);
@ -107,9 +105,9 @@ angular.module('ffffng')
], ],
callback callback
); );
} }
function storeNodeInformation(nodeData, node, callback) { function storeNodeInformation(nodeData, node, callback) {
Logger.tag('monitoring', 'information-retrieval').debug('Storing status for node: %s', nodeData.mac); Logger.tag('monitoring', 'information-retrieval').debug('Storing status for node: %s', nodeData.mac);
return Database.get('SELECT * FROM node_state WHERE mac = ?', [node.mac], function (err, row) { return Database.get('SELECT * FROM node_state WHERE mac = ?', [node.mac], function (err, row) {
@ -117,7 +115,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var nodeDataForStoring; let nodeDataForStoring;
if (nodeData === 'missing') { if (nodeData === 'missing') {
nodeDataForStoring = { nodeDataForStoring = {
mac: node.mac, mac: node.mac,
@ -139,18 +137,18 @@ angular.module('ffffng')
return updateNodeInformation(nodeDataForStoring, node, row, callback); return updateNodeInformation(nodeDataForStoring, node, row, callback);
} }
}); });
} }
var isValidMac = Validator.forConstraint(Constraints.node.mac); const isValidMac = Validator.forConstraint(Constraints.node.mac);
function parseTimestamp (timestamp) { function parseTimestamp (timestamp) {
if (!_.isString(timestamp)) { if (!_.isString(timestamp)) {
return moment.invalid(); return moment.invalid();
} }
return moment.utc(timestamp); return moment.utc(timestamp);
} }
function parseNode (importTimestamp, nodeData, nodeId) { function parseNode (importTimestamp, nodeData, nodeId) {
if (!_.isPlainObject(nodeData)) { if (!_.isPlainObject(nodeData)) {
throw new Error( throw new Error(
'Node ' + nodeId + ': Unexpected node type: ' + (typeof nodeData) 'Node ' + nodeId + ': Unexpected node type: ' + (typeof nodeData)
@ -173,7 +171,7 @@ angular.module('ffffng')
'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac 'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac
); );
} }
var mac = Strings.normalizeMac(nodeData.nodeinfo.network.mac); const mac = Strings.normalizeMac(nodeData.nodeinfo.network.mac);
if (!_.isPlainObject(nodeData.flags)) { if (!_.isPlainObject(nodeData.flags)) {
throw new Error( throw new Error(
@ -185,23 +183,23 @@ angular.module('ffffng')
'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online)
); );
} }
var isOnline = nodeData.flags.online; const isOnline = nodeData.flags.online;
var lastSeen = parseTimestamp(nodeData.lastseen); const lastSeen = parseTimestamp(nodeData.lastseen);
if (!lastSeen.isValid()) { if (!lastSeen.isValid()) {
throw new Error( throw new Error(
'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen 'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen
); );
} }
var site = null; let site = null;
// jshint -W106 // jshint -W106
if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.site_code)) { if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.site_code)) {
site = nodeData.nodeinfo.system.site_code; site = nodeData.nodeinfo.system.site_code;
} }
// jshint +W106 // jshint +W106
var domain = null; let domain = null;
// jshint -W106 // jshint -W106
if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.domain_code)) { if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.domain_code)) {
domain = nodeData.nodeinfo.system.domain_code; domain = nodeData.nodeinfo.system.domain_code;
@ -216,15 +214,15 @@ angular.module('ffffng')
site: site, site: site,
domain: domain domain: domain
}; };
} }
function parseNodesJson (body, callback) { function parseNodesJson (body, callback) {
Logger.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...'); Logger.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...');
var data = {}; const data = {};
try { try {
var json = JSON.parse(body); const json = JSON.parse(body);
if (json.version !== 1) { if (json.version !== 1) {
return callback(new Error('Unexpected nodes.json version: ' + json.version)); return callback(new Error('Unexpected nodes.json version: ' + json.version));
@ -264,9 +262,9 @@ angular.module('ffffng')
} }
callback(null, data); callback(null, data);
} }
function updateSkippedNode(id, node, callback) { function updateSkippedNode(id, node, callback) {
Database.run( Database.run(
'UPDATE node_state ' + 'UPDATE node_state ' +
'SET hostname = ?, monitoring_state = ?, modified_at = ?' + 'SET hostname = ?, monitoring_state = ?, modified_at = ?' +
@ -277,12 +275,12 @@ angular.module('ffffng')
], ],
callback callback
); );
} }
function sendMonitoringMailsBatched(name, mailType, findBatchFun, callback) { function sendMonitoringMailsBatched(name, mailType, findBatchFun, callback) {
Logger.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name); Logger.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name);
var sendNextBatch = function (err) { const sendNextBatch = function (err) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -302,7 +300,7 @@ angular.module('ffffng')
async.each( async.each(
nodeStates, nodeStates,
function (nodeState, mailCallback) { function (nodeState, mailCallback) {
var mac = nodeState.mac; const mac = nodeState.mac;
Logger.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac); Logger.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac);
NodeService.getNodeDataByMac(mac, function (err, node, nodeSecrets) { NodeService.getNodeDataByMac(mac, function (err, node, nodeSecrets) {
if (err) { if (err) {
@ -349,7 +347,7 @@ angular.module('ffffng')
.tag('monitoring', 'mail-sending') .tag('monitoring', 'mail-sending')
.debug('Updating node state: ', mac); .debug('Updating node state: ', mac);
var now = moment().unix(); const now = moment().unix();
Database.run( Database.run(
'UPDATE node_state ' + 'UPDATE node_state ' +
'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' + 'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' +
@ -376,9 +374,9 @@ angular.module('ffffng')
}; };
sendNextBatch(null); sendNextBatch(null);
} }
function sendOnlineAgainMails(startTime, callback) { function sendOnlineAgainMails(startTime, callback) {
sendMonitoringMailsBatched( sendMonitoringMailsBatched(
'online again', 'online again',
'monitoring-online-again', 'monitoring-online-again',
@ -400,15 +398,15 @@ angular.module('ffffng')
}, },
callback callback
); );
} }
/** /**
* sends one of three mails if a node is offline * sends one of three mails if a node is offline
* @param {moment} startTime the moment the job started * @param {moment} startTime the moment the job started
* @param {Number} mailNumber which of three mails * @param {Number} mailNumber which of three mails
* @param {Function} callback gets all nodes that are offline * @param {Function} callback gets all nodes that are offline
*/ */
function sendOfflineMails(startTime, mailNumber, callback) { function sendOfflineMails(startTime, mailNumber, callback) {
sendMonitoringMailsBatched( sendMonitoringMailsBatched(
'offline ' + mailNumber, 'offline ' + mailNumber,
'monitoring-offline-' + mailNumber, 'monitoring-offline-' + mailNumber,
@ -416,14 +414,14 @@ angular.module('ffffng')
/** /**
* descriptive string that stores, which was the last mail type, stored in the database as last_status_mail_type * descriptive string that stores, which was the last mail type, stored in the database as last_status_mail_type
*/ */
var previousType = const previousType =
mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1)); mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1));
// the first time the first offline mail is send, there was no mail before // the first time the first offline mail is send, there was no mail before
var allowNull = mailNumber === 1 ? ' OR last_status_mail_type IS NULL' : ''; const allowNull = mailNumber === 1 ? ' OR last_status_mail_type IS NULL' : '';
var schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber]; const schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber];
var scheduledTimeBefore = moment().subtract(schedule.amount, schedule.unit); const scheduledTimeBefore = moment().subtract(schedule.amount, schedule.unit);
Database.all( Database.all(
'SELECT * FROM node_state ' + 'SELECT * FROM node_state ' +
@ -444,9 +442,9 @@ angular.module('ffffng')
}, },
callback callback
); );
} }
function withUrlsData(urls, callback) { function withUrlsData(urls, callback) {
async.map(urls, function (url, urlCallback) { async.map(urls, function (url, urlCallback) {
Logger.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url); Logger.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url);
request(url, function (err, response, body) { request(url, function (err, response, body) {
@ -464,16 +462,16 @@ angular.module('ffffng')
parseNodesJson(body, urlCallback); parseNodesJson(body, urlCallback);
}); });
}, callback); }, callback);
} }
function retrieveNodeInformationForUrls(urls, callback) { function retrieveNodeInformationForUrls(urls, callback) {
withUrlsData(urls, function (err, datas) { withUrlsData(urls, function (err, datas) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var maxTimestamp = datas[0].importTimestamp; let maxTimestamp = datas[0].importTimestamp;
var minTimestamp = maxTimestamp; let minTimestamp = maxTimestamp;
_.each(datas, function (data) { _.each(datas, function (data) {
if (data.importTimestamp.isAfter(maxTimestamp)) { if (data.importTimestamp.isAfter(maxTimestamp)) {
maxTimestamp = data.importTimestamp; maxTimestamp = data.importTimestamp;
@ -497,15 +495,15 @@ angular.module('ffffng')
// We do not parallelize here as the sqlite will start slowing down and blocking with too many // We do not parallelize here as the sqlite will start slowing down and blocking with too many
// parallel queries. This has resulted in blocking other requests too and thus in a major slowdown. // parallel queries. This has resulted in blocking other requests too and thus in a major slowdown.
var allNodes = _.flatMap(datas, function (data) { const allNodes = _.flatMap(datas, function (data) {
return data.nodes; return data.nodes;
}); });
// Get rid of duplicates from different nodes.json files. Always use the one with the newest // Get rid of duplicates from different nodes.json files. Always use the one with the newest
var sortedNodes = _.orderBy(allNodes, [function (node) { const sortedNodes = _.orderBy(allNodes, [function (node) {
return node.lastSeen.unix(); return node.lastSeen.unix();
}], ['desc']); }], ['desc']);
var uniqueNodes = _.uniqBy(sortedNodes, function (node) { const uniqueNodes = _.uniqBy(sortedNodes, function (node) {
return node.mac; return node.mac;
}); });
async.eachSeries( async.eachSeries(
@ -567,11 +565,11 @@ angular.module('ffffng')
} }
); );
}); });
} }
return { module.exports = {
getAll: function (restParams, callback) { getAll: function (restParams, callback) {
var sortFields = [ const sortFields = [
'id', 'id',
'hostname', 'hostname',
'mac', 'mac',
@ -586,7 +584,7 @@ angular.module('ffffng')
'created_at', 'created_at',
'modified_at' 'modified_at'
]; ];
var filterFields = [ const filterFields = [
'hostname', 'hostname',
'mac', 'mac',
'monitoring_state', 'monitoring_state',
@ -594,7 +592,7 @@ angular.module('ffffng')
'last_status_mail_type' 'last_status_mail_type'
]; ];
var where = Resources.whereCondition(restParams, filterFields); const where = Resources.whereCondition(restParams, filterFields);
Database.get( Database.get(
'SELECT count(*) AS total FROM node_state WHERE ' + where.query, 'SELECT count(*) AS total FROM node_state WHERE ' + where.query,
@ -604,9 +602,9 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var total = row.total; const total = row.total;
var filter = Resources.filterClause( const filter = Resources.filterClause(
restParams, restParams,
'id', 'id',
sortFields, sortFields,
@ -636,7 +634,7 @@ angular.module('ffffng')
async.map( async.map(
_.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE), _.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE),
function (subMacs, subCallback) { function (subMacs, subCallback) {
var inCondition = DatabaseUtil.inCondition('mac', subMacs); const inCondition = DatabaseUtil.inCondition('mac', subMacs);
Database.all( Database.all(
'SELECT * FROM node_state WHERE ' + inCondition.query, 'SELECT * FROM node_state WHERE ' + inCondition.query,
@ -649,7 +647,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var nodeStateByMac = {}; const nodeStateByMac = {};
_.each(_.flatten(rowsArrays), function (row) { _.each(_.flatten(rowsArrays), function (row) {
nodeStateByMac[row.mac] = row; nodeStateByMac[row.mac] = row;
}); });
@ -707,7 +705,7 @@ angular.module('ffffng')
}, },
retrieveNodeInformation: function (callback) { retrieveNodeInformation: function (callback) {
var urls = config.server.map.nodesJsonUrl; let urls = config.server.map.nodesJsonUrl;
if (_.isEmpty(urls)) { if (_.isEmpty(urls)) {
return callback( return callback(
new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl') new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl')
@ -723,7 +721,7 @@ angular.module('ffffng')
sendMonitoringMails: function (callback) { sendMonitoringMails: function (callback) {
Logger.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...'); Logger.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...');
var startTime = moment(); const startTime = moment();
sendOnlineAgainMails(startTime, function (err) { sendOnlineAgainMails(startTime, function (err) {
if (err) { if (err) {
@ -786,7 +784,7 @@ angular.module('ffffng')
async.eachSeries( async.eachSeries(
rows, rows,
function (row, nodeCallback) { function (row, nodeCallback) {
var mac = row.mac; const mac = row.mac;
Logger.tag('nodes', 'delete-offline').info('Deleting node ' + mac); Logger.tag('nodes', 'delete-offline').info('Deleting node ' + mac);
NodeService.getNodeDataByMac(mac, function (err, node) { NodeService.getNodeDataByMac(mac, function (err, node) {
if (err) { if (err) {
@ -825,5 +823,4 @@ angular.module('ffffng')
} }
); );
} }
}; }
});

View file

@ -1,22 +1,21 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('NodeService', function ( const async = require('async')
config, const crypto = require('crypto')
_, const fs = require('graceful-fs')
async, const glob = require('glob')
crypto,
fs,
glob,
Logger,
MailService,
Strings,
ErrorTypes,
UrlBuilder
) {
var MAX_PARALLEL_NODES_PARSING = 10;
var linePrefixes = { const config = require('../config').config
const ErrorTypes = require('../utils/errorTypes')
const Logger = require('../logger')
const MailService = require('../services/mailService')
const Strings = require('../utils/strings')
const UrlBuilder = require('../utils/urlBuilder')
const MAX_PARALLEL_NODES_PARSING = 10;
const linePrefixes = {
hostname: '# Knotenname: ', hostname: '# Knotenname: ',
nickname: '# Ansprechpartner: ', nickname: '# Ansprechpartner: ',
email: '# Kontakt: ', email: '# Kontakt: ',
@ -25,16 +24,16 @@ angular.module('ffffng')
token: '# Token: ', token: '# Token: ',
monitoring: '# Monitoring: ', monitoring: '# Monitoring: ',
monitoringToken: '# Monitoring-Token: ' monitoringToken: '# Monitoring-Token: '
}; };
var filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken']; const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken'];
function generateToken() { function generateToken() {
return crypto.randomBytes(8).toString('hex'); return crypto.randomBytes(8).toString('hex');
} }
function toNodeFilesPattern(filter) { function toNodeFilesPattern(filter) {
var pattern = _.join( const pattern = _.join(
_.map(filenameParts, function (field) { _.map(filenameParts, function (field) {
return filter.hasOwnProperty(field) ? filter[field] : '*'; return filter.hasOwnProperty(field) ? filter[field] : '*';
}), }),
@ -42,17 +41,17 @@ angular.module('ffffng')
); );
return config.server.peersPath + '/' + pattern.toLowerCase(); return config.server.peersPath + '/' + pattern.toLowerCase();
} }
function findNodeFiles(filter, callback) { function findNodeFiles(filter, callback) {
glob(toNodeFilesPattern(filter), callback); glob(toNodeFilesPattern(filter), callback);
} }
function findNodeFilesSync(filter) { function findNodeFilesSync(filter) {
return glob.sync(toNodeFilesPattern(filter)); return glob.sync(toNodeFilesPattern(filter));
} }
function findFilesInPeersPath(callback) { function findFilesInPeersPath(callback) {
glob(config.server.peersPath + '/*', function (err, files) { glob(config.server.peersPath + '/*', function (err, files) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -72,19 +71,19 @@ angular.module('ffffng')
}); });
}, callback); }, callback);
}); });
} }
function parseNodeFilename(filename) { function parseNodeFilename(filename) {
var parts = _.split(filename, '@', filenameParts.length); const parts = _.split(filename, '@', filenameParts.length);
var parsed = {}; const parsed = {};
_.each(_.zip(filenameParts, parts), function (part) { _.each(_.zip(filenameParts, parts), function (part) {
parsed[part[0]] = part[1]; parsed[part[0]] = part[1];
}); });
return parsed; return parsed;
} }
function isDuplicate(filter, token) { function isDuplicate(filter, token) {
var files = findNodeFilesSync(filter); const files = findNodeFilesSync(filter);
if (files.length === 0) { if (files.length === 0) {
return false; return false;
} }
@ -94,9 +93,9 @@ angular.module('ffffng')
} }
return parseNodeFilename(files[0]).token !== token; return parseNodeFilename(files[0]).token !== token;
} }
function checkNoDuplicates(token, node, nodeSecrets) { function checkNoDuplicates(token, node, nodeSecrets) {
if (isDuplicate({ hostname: node.hostname }, token)) { if (isDuplicate({ hostname: node.hostname }, token)) {
return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict}; return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict};
} }
@ -116,9 +115,9 @@ angular.module('ffffng')
} }
return null; return null;
} }
function toNodeFilename(token, node, nodeSecrets) { function toNodeFilename(token, node, nodeSecrets) {
return config.server.peersPath + '/' + return config.server.peersPath + '/' +
( (
(node.hostname || '') + '@' + (node.hostname || '') + '@' +
@ -127,13 +126,13 @@ angular.module('ffffng')
(token || '') + '@' + (token || '') + '@' +
(nodeSecrets.monitoringToken || '') (nodeSecrets.monitoringToken || '')
).toLowerCase(); ).toLowerCase();
} }
function writeNodeFile(isUpdate, token, node, nodeSecrets, callback) { function writeNodeFile(isUpdate, token, node, nodeSecrets, callback) {
var filename = toNodeFilename(token, node, nodeSecrets); const filename = toNodeFilename(token, node, nodeSecrets);
var data = ''; let data = '';
_.each(linePrefixes, function (prefix, key) { _.each(linePrefixes, function (prefix, key) {
var value; let value;
switch (key) { switch (key) {
case 'monitoring': case 'monitoring':
if (node.monitoring && node.monitoringConfirmed) { if (node.monitoring && node.monitoringConfirmed) {
@ -164,10 +163,10 @@ angular.module('ffffng')
// since node.js is single threaded we don't need a lock // since node.js is single threaded we don't need a lock
var error; let error;
if (isUpdate) { if (isUpdate) {
var files = findNodeFilesSync({ token: token }); const files = findNodeFilesSync({ token: token });
if (files.length !== 1) { if (files.length !== 1) {
return callback({data: 'Node not found.', type: ErrorTypes.notFound}); return callback({data: 'Node not found.', type: ErrorTypes.notFound});
} }
@ -177,7 +176,7 @@ angular.module('ffffng')
return callback(error); return callback(error);
} }
var file = files[0]; const file = files[0];
try { try {
fs.unlinkSync(file); fs.unlinkSync(file);
} }
@ -201,9 +200,9 @@ angular.module('ffffng')
} }
return callback(null, token, node); return callback(null, token, node);
} }
function deleteNodeFile(token, callback) { function deleteNodeFile(token, callback) {
findNodeFiles({ token: token }, function (err, files) { findNodeFiles({ token: token }, function (err, files) {
if (err) { if (err) {
Logger.tag('node', 'delete').error('Could not find node file: ' + files, err); Logger.tag('node', 'delete').error('Could not find node file: ' + files, err);
@ -224,25 +223,25 @@ angular.module('ffffng')
return callback(null); return callback(null);
}); });
} }
function parseNodeFile(file, callback) { function parseNodeFile(file, callback) {
fs.readFile(file, function (err, contents) { fs.readFile(file, function (err, contents) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var lines = contents.toString(); const lines = contents.toString();
var node = {}; const node = {};
var nodeSecrets = {}; const nodeSecrets = {};
_.each(lines.split('\n'), function (line) { _.each(lines.split('\n'), function (line) {
var entries = {}; const entries = {};
for (var key in linePrefixes) { for (const key in linePrefixes) {
if (linePrefixes.hasOwnProperty(key)) { if (linePrefixes.hasOwnProperty(key)) {
var prefix = linePrefixes[key]; const prefix = linePrefixes[key];
if (line.substring(0, prefix.length) === prefix) { if (line.substring(0, prefix.length) === prefix) {
entries[key] = Strings.normalizeString(line.substr(prefix.length)); entries[key] = Strings.normalizeString(line.substr(prefix.length));
break; break;
@ -259,8 +258,8 @@ angular.module('ffffng')
node.mac = value; node.mac = value;
node.mapId = _.toLower(value).replace(/:/g, ''); node.mapId = _.toLower(value).replace(/:/g, '');
} else if (key === 'monitoring') { } else if (key === 'monitoring') {
var active = value === 'aktiv'; const active = value === 'aktiv';
var pending = value === 'pending'; const pending = value === 'pending';
node.monitoring = active || pending; node.monitoring = active || pending;
node.monitoringConfirmed = active; node.monitoringConfirmed = active;
node.monitoringState = active ? 'active' : (pending ? 'pending' : 'disabled'); node.monitoringState = active ? 'active' : (pending ? 'pending' : 'disabled');
@ -274,9 +273,9 @@ angular.module('ffffng')
callback(null, node, nodeSecrets); callback(null, node, nodeSecrets);
}); });
} }
function findNodeDataByFilePattern(filter, callback) { function findNodeDataByFilePattern(filter, callback) {
findNodeFiles(filter, function (err, files) { findNodeFiles(filter, function (err, files) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -286,12 +285,12 @@ angular.module('ffffng')
return callback(null); return callback(null);
} }
var file = files[0]; const file = files[0];
return parseNodeFile(file, callback); return parseNodeFile(file, callback);
}); });
} }
function getNodeDataByFilePattern(filter, callback) { function getNodeDataByFilePattern(filter, callback) {
findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) { findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -303,11 +302,11 @@ angular.module('ffffng')
callback(null, node, nodeSecrets); callback(null, node, nodeSecrets);
}); });
} }
function sendMonitoringConfirmationMail(node, nodeSecrets, callback) { function sendMonitoringConfirmationMail(node, nodeSecrets, callback) {
var confirmUrl = UrlBuilder.monitoringConfirmUrl(nodeSecrets); const confirmUrl = UrlBuilder.monitoringConfirmUrl(nodeSecrets);
var disableUrl = UrlBuilder.monitoringDisableUrl(nodeSecrets); const disableUrl = UrlBuilder.monitoringDisableUrl(nodeSecrets);
MailService.enqueue( MailService.enqueue(
config.server.email.from, config.server.email.from,
@ -327,12 +326,12 @@ angular.module('ffffng')
callback(null); callback(null);
} }
); );
} }
return { module.exports = {
createNode: function (node, callback) { createNode: function (node, callback) {
var token = generateToken(); const token = generateToken();
var nodeSecrets = {}; const nodeSecrets = {};
node.monitoringConfirmed = false; node.monitoringConfirmed = false;
@ -365,8 +364,8 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var monitoringConfirmed = false; let monitoringConfirmed = false;
var monitoringToken = ''; let monitoringToken = '';
if (node.monitoring) { if (node.monitoring) {
if (!currentNode.monitoring) { if (!currentNode.monitoring) {
@ -471,7 +470,7 @@ angular.module('ffffng')
return fileCallback(err); return fileCallback(err);
} }
var expectedFilename = toNodeFilename(node.token, node, nodeSecrets); const expectedFilename = toNodeFilename(node.token, node, nodeSecrets);
if (file !== expectedFilename) { if (file !== expectedFilename) {
return fs.rename(file, expectedFilename, function (err) { return fs.rename(file, expectedFilename, function (err) {
if (err) { if (err) {
@ -498,7 +497,7 @@ angular.module('ffffng')
return callback(err); return callback(err);
} }
var nodeStatistics = { const nodeStatistics = {
registered: _.size(nodes), registered: _.size(nodes),
withVPN: 0, withVPN: 0,
withCoords: 0, withCoords: 0,
@ -530,5 +529,4 @@ angular.module('ffffng')
callback(null, nodeStatistics); callback(null, nodeStatistics);
}); });
} }
}; }
});

View file

@ -1,12 +1,12 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('DatabaseUtil', function (_) { const _ = require('lodash')
return {
inCondition: function (field, list) { module.exports = {
inCondition (field, list) {
return { return {
query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))', query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))',
params: list params: list
};
} }
}; }
}); }

View file

@ -1,10 +1,8 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('ErrorTypes', function () { module.exports = {
return {
badRequest: {code: 400}, badRequest: {code: 400},
notFound: {code: 404}, notFound: {code: 404},
conflict: {code: 409}, conflict: {code: 409},
internalError: {code: 500} internalError: {code: 500}
}; }
});

View file

@ -1,7 +1,13 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Resources', function (_, Constraints, Validator, ErrorTypes, Logger) { const _ = require('lodash')
function respond(res, httpCode, data, type) {
const Constraints = require('../../shared/validation/constraints')
const ErrorTypes = require('../utils/errorTypes')
const Logger = require('../logger')
const Validator = require('../validation/validator')
function respond(res, httpCode, data, type) {
switch (type) { switch (type) {
case 'html': case 'html':
res.writeHead(httpCode, {'Content-Type': 'text/html'}); res.writeHead(httpCode, {'Content-Type': 'text/html'});
@ -13,10 +19,10 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
res.end(JSON.stringify(data)); res.end(JSON.stringify(data));
break; break;
} }
} }
function orderByClause(restParams, defaultSortField, allowedSortFields) { function orderByClause(restParams, defaultSortField, allowedSortFields) {
var sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; let sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined;
if (!sortField) { if (!sortField) {
sortField = defaultSortField; sortField = defaultSortField;
} }
@ -25,26 +31,26 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'), query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'),
params: [] params: []
}; };
} }
function limitOffsetClause(restParams) { function limitOffsetClause(restParams) {
var page = restParams._page; const page = restParams._page;
var perPage = restParams._perPage; const perPage = restParams._perPage;
return { return {
query: 'LIMIT ? OFFSET ?', query: 'LIMIT ? OFFSET ?',
params: [perPage, ((page - 1) * perPage)] params: [perPage, ((page - 1) * perPage)]
}; };
} }
function escapeForLikePattern(str) { function escapeForLikePattern(str) {
return str return str
.replace(/\\/g, '\\\\') .replace(/\\/g, '\\\\')
.replace(/%/g, '\\%') .replace(/%/g, '\\%')
.replace(/_/g, '\\_'); .replace(/_/g, '\\_');
} }
function filterCondition(restParams, filterFields) { function filterCondition(restParams, filterFields) {
if (_.isEmpty(filterFields)) { if (_.isEmpty(filterFields)) {
return { return {
query: '1 = 1', query: '1 = 1',
@ -52,7 +58,7 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
}; };
} }
var query = _.join( let query = _.join(
_.map(filterFields, function (field) { _.map(filterFields, function (field) {
return 'LOWER(' + field + ') LIKE ?'; return 'LOWER(' + field + ') LIKE ?';
}), }),
@ -61,38 +67,38 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
query += ' ESCAPE \'\\\''; query += ' ESCAPE \'\\\'';
var search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%'; const search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%';
var params = _.times(filterFields.length, _.constant(search)); const params = _.times(filterFields.length, _.constant(search));
return { return {
query: query, query: query,
params: params params: params
}; };
} }
function getConstrainedValues(data, constraints) { function getConstrainedValues(data, constraints) {
var values = {}; const values = {};
_.each(_.keys(constraints), function (key) { _.each(_.keys(constraints), function (key) {
var value = data[key]; const value = data[key];
values[key] = values[key] =
_.isUndefined(value) && !_.isUndefined(constraints[key].default) ? constraints[key].default : value; _.isUndefined(value) && !_.isUndefined(constraints[key].default) ? constraints[key].default : value;
}); });
return values; return values;
} }
return { module.exports = {
getData: function (req) { getData (req) {
return _.extend({}, req.body, req.params, req.query); return _.extend({}, req.body, req.params, req.query);
}, },
getValidRestParams: function(type, subtype, req, callback) { getValidRestParams(type, subtype, req, callback) {
var constraints = Constraints.rest[type]; const constraints = Constraints.rest[type];
if (!_.isPlainObject(constraints)) { if (!_.isPlainObject(constraints)) {
Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type); Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type);
return callback({data: 'Internal error.', type: ErrorTypes.internalError}); return callback({data: 'Internal error.', type: ErrorTypes.internalError});
} }
var filterConstraints = {}; let filterConstraints = {};
if (subtype) { if (subtype) {
filterConstraints = Constraints[subtype + 'Filters']; filterConstraints = Constraints[subtype + 'Filters'];
if (!_.isPlainObject(filterConstraints)) { if (!_.isPlainObject(filterConstraints)) {
@ -101,13 +107,13 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
} }
} }
var data = this.getData(req); const data = this.getData(req);
var restParams = getConstrainedValues(data, constraints); const restParams = getConstrainedValues(data, constraints);
var filterParams = getConstrainedValues(data, filterConstraints); const filterParams = getConstrainedValues(data, filterConstraints);
var areValidParams = Validator.forConstraints(constraints); const areValidParams = Validator.forConstraints(constraints);
var areValidFilters = Validator.forConstraints(filterConstraints); const areValidFilters = Validator.forConstraints(filterConstraints);
if (!areValidParams(restParams) || !areValidFilters(filterParams)) { if (!areValidParams(restParams) || !areValidFilters(filterParams)) {
return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest}); return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest});
} }
@ -117,8 +123,8 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
callback(null, restParams); callback(null, restParams);
}, },
filter: function (entities, allowedFilterFields, restParams) { filter (entities, allowedFilterFields, restParams) {
var query = restParams.q; let query = restParams.q;
if (query) { if (query) {
query = _.toLower(query.trim()); query = _.toLower(query.trim());
} }
@ -128,7 +134,7 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
return true; return true;
} }
return _.some(allowedFilterFields, function (field) { return _.some(allowedFilterFields, function (field) {
var value = entity[field]; let value = entity[field];
if (_.isNumber(value)) { if (_.isNumber(value)) {
value = value.toString(); value = value.toString();
} }
@ -146,7 +152,7 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
}); });
} }
var filters = restParams.filters; const filters = restParams.filters;
function filtersMatch(entity) { function filtersMatch(entity) {
if (_.isEmpty(filters)) { if (_.isEmpty(filters)) {
@ -158,7 +164,7 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
return true; return true;
} }
if (_.startsWith(key, 'has')) { if (_.startsWith(key, 'has')) {
var entityKey = key.substr(3, 1).toLowerCase() + key.substr(4); const entityKey = key.substr(3, 1).toLowerCase() + key.substr(4);
return _.isEmpty(entity[entityKey]).toString() !== value; return _.isEmpty(entity[entityKey]).toString() !== value;
} }
return entity[key] === value; return entity[key] === value;
@ -170,35 +176,35 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
}); });
}, },
sort: function (entities, allowedSortFields, restParams) { sort (entities, allowedSortFields, restParams) {
var sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; const sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined;
if (!sortField) { if (!sortField) {
return entities; return entities;
} }
var sorted = _.sortBy(entities, [sortField]); const sorted = _.sortBy(entities, [sortField]);
return restParams._sortDir === 'ASC' ? sorted : _.reverse(sorted); return restParams._sortDir === 'ASC' ? sorted : _.reverse(sorted);
}, },
getPageEntities: function (entities, restParams) { getPageEntities (entities, restParams) {
var page = restParams._page; const page = restParams._page;
var perPage = restParams._perPage; const perPage = restParams._perPage;
return entities.slice((page - 1) * perPage, page * perPage); return entities.slice((page - 1) * perPage, page * perPage);
}, },
whereCondition: filterCondition, whereCondition: filterCondition,
filterClause: function (restParams, defaultSortField, allowedSortFields, filterFields) { filterClause (restParams, defaultSortField, allowedSortFields, filterFields) {
var orderBy = orderByClause( const orderBy = orderByClause(
restParams, restParams,
defaultSortField, defaultSortField,
allowedSortFields allowedSortFields
); );
var limitOffset = limitOffsetClause(restParams); const limitOffset = limitOffsetClause(restParams);
var filter = filterCondition( const filter = filterCondition(
restParams, restParams,
filterFields filterFields
); );
@ -209,16 +215,15 @@ angular.module('ffffng').factory('Resources', function (_, Constraints, Validato
}; };
}, },
success: function (res, data) { success (res, data) {
respond(res, 200, data, 'json'); respond(res, 200, data, 'json');
}, },
successHtml: function (res, html) { successHtml (res, html) {
respond(res, 200, html, 'html'); respond(res, 200, html, 'html');
}, },
error: function (res, err) { error (res, err) {
respond(res, err.type.code, err.data, 'json'); respond(res, err.type.code, err.data, 'json');
} }
}; }
});

View file

@ -1,27 +1,27 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Strings', function (_) { const _ = require('lodash')
return {
normalizeString: function (str) { module.exports = {
normalizeString (str) {
return _.isString(str) ? str.trim().replace(/\s+/g, ' ') : str; return _.isString(str) ? str.trim().replace(/\s+/g, ' ') : str;
}, },
normalizeMac: function (mac) { normalizeMac (mac) {
// parts only contains values at odd indexes // parts only contains values at odd indexes
var parts = mac.toUpperCase().replace(/:/g, '').split(/([A-F0-9]{2})/); const parts = mac.toUpperCase().replace(/:/g, '').split(/([A-F0-9]{2})/);
var macParts = []; const macParts = [];
for (var i = 1; i < parts.length; i += 2) { for (let i = 1; i < parts.length; i += 2) {
macParts.push(parts[i]); macParts.push(parts[i]);
} }
return macParts.join(':'); return macParts.join(':');
}, },
parseInt: function (str) { parseInt (str) {
var parsed = _.parseInt(str, 10); const parsed = _.parseInt(str, 10);
return parsed.toString() === str ? parsed : undefined; return parsed.toString() === str ? parsed : undefined;
} }
}; }
});

View file

@ -1,8 +1,11 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('UrlBuilder', function (_, config) { const _ = require('lodash')
function formUrl(route, queryParams) {
var url = config.server.baseUrl; const config = require('../config').config
function formUrl(route, queryParams) {
let url = config.server.baseUrl;
if (route || queryParams) { if (route || queryParams) {
url += '/#/'; url += '/#/';
} }
@ -22,18 +25,17 @@ angular.module('ffffng').factory('UrlBuilder', function (_, config) {
); );
} }
return url; return url;
} }
return { module.exports = {
editNodeUrl: function () { editNodeUrl () {
return formUrl('update'); return formUrl('update');
}, },
monitoringConfirmUrl: function (nodeSecrets) { monitoringConfirmUrl (nodeSecrets) {
return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken }); return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken });
}, },
monitoringDisableUrl: function (nodeSecrets) { monitoringDisableUrl (nodeSecrets) {
return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken }); return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken });
} }
}; }
});

View file

@ -1,13 +1,17 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Validator', function (_, Strings, Logger) { const _ = require('lodash')
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
function isValidBoolean(value) { const Strings = require('../utils/strings')
const Logger = require('../logger')
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
function isValidBoolean(value) {
return _.isBoolean(value) || value === 'true' || value === 'false'; return _.isBoolean(value) || value === 'true' || value === 'false';
} }
function isValidNumber(constraint, value) { function isValidNumber(constraint, value) {
if (_.isString(value)) { if (_.isString(value)) {
value = Strings.parseInt(value); value = Strings.parseInt(value);
} }
@ -29,26 +33,26 @@ angular.module('ffffng').factory('Validator', function (_, Strings, Logger) {
} }
return true; return true;
} }
function isValidEnum(constraint, value) { function isValidEnum(constraint, value) {
if (!_.isString(value)) { if (!_.isString(value)) {
return false; return false;
} }
return _.indexOf(constraint.allowed, value) >= 0; return _.indexOf(constraint.allowed, value) >= 0;
} }
function isValidString(constraint, value) { function isValidString(constraint, value) {
if (!_.isString(value)) { if (!_.isString(value)) {
return false; return false;
} }
var trimmed = value.trim(); const trimmed = value.trim();
return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex); return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex);
} }
function isValid(constraint, acceptUndefined, value) { function isValid(constraint, acceptUndefined, value) {
if (value === undefined) { if (value === undefined) {
return acceptUndefined || constraint.optional; return acceptUndefined || constraint.optional;
} }
@ -69,25 +73,24 @@ angular.module('ffffng').factory('Validator', function (_, Strings, Logger) {
Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type); Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type);
return false; return false;
} }
function areValid(constraints, acceptUndefined, values) { function areValid(constraints, acceptUndefined, values) {
var fields = Object.keys(constraints); const fields = Object.keys(constraints);
for (var i = 0; i < fields.length; i ++) { for (let i = 0; i < fields.length; i ++) {
var field = fields[i]; const field = fields[i];
if (!isValid(constraints[field], acceptUndefined, values[field])) { if (!isValid(constraints[field], acceptUndefined, values[field])) {
return false; return false;
} }
} }
return true; return true;
} }
return { module.exports = {
forConstraint: function (constraint, acceptUndefined) { forConstraint (constraint, acceptUndefined) {
return _.partial(isValid, constraint, acceptUndefined); return _.partial(isValid, constraint, acceptUndefined);
}, },
forConstraints: function (constraints, acceptUndefined) { forConstraints (constraints, acceptUndefined) {
return _.partial(areValid, constraints, acceptUndefined); return _.partial(areValid, constraints, acceptUndefined);
} }
}; }
});

View file

@ -1,6 +1,7 @@
'use strict'; 'use strict';
angular.module('ffffng').constant('Constraints', { (function () {
var constraints = {
id:{ id:{
type: 'string', type: 'string',
regex: /^[1-9][0-9]*/, regex: /^[1-9][0-9]*/,
@ -110,4 +111,29 @@ angular.module('ffffng').constant('Constraints', {
} }
} }
} }
}); }
let _angular = null
try {
_angular = angular
}
catch (error) {
// ReferenceError, as angular is not defined.
}
let _module = null
try {
_module = module
}
catch (error) {
// ReferenceError, as module is not defined.
}
if (_angular) {
angular.module('ffffng').constant('Constraints', constraints)
}
if (_module) {
module.exports = constraints
}
})()