From 8697d79ba5b7fbd690ba3e24345222d0ae09f1b8 Mon Sep 17 00:00:00 2001 From: baldo Date: Mon, 17 Dec 2018 22:49:54 +0100 Subject: [PATCH] Removing ng-di on the server. --- package.json | 1 - server/app.js | 37 +- server/config.js | 258 ++-- server/db/database.js | 7 +- server/jobs/FixNodeFilenamesJob.js | 18 + server/jobs/MailQueueJob.js | 18 + server/jobs/MonitoringMailsSendingJob.js | 18 + server/jobs/NodeInformationRetrievalJob.js | 18 + server/jobs/OfflineNodesDeletionJob.js | 18 + server/jobs/fixNodeFilenamesJob.js | 17 - server/jobs/mailQueueJob.js | 17 - server/jobs/monitoringMailsSendingJob.js | 17 - server/jobs/nodeInformationRetrievalJob.js | 17 - server/jobs/offlineNodesDeletionJob.js | 17 - server/jobs/scheduler.js | 173 +-- server/libs.js | 24 - server/logger.js | 48 +- server/main.js | 59 +- server/resources/frontendResource.js | 51 +- server/resources/mailResource.js | 150 +- server/resources/monitoringResource.js | 143 +- server/resources/nodeResource.js | 319 +++-- server/resources/statisticsResource.js | 44 +- server/resources/taskResource.js | 207 ++- server/resources/versionResource.js | 28 +- server/router.js | 81 +- server/services/mailService.js | 443 +++--- server/services/mailTemplateService.js | 229 ++-- server/services/monitoringService.js | 1443 ++++++++++---------- server/services/nodeService.js | 852 ++++++------ server/utils/databaseUtil.js | 18 +- server/utils/errorTypes.js | 14 +- server/utils/resources.js | 415 +++--- server/utils/strings.js | 40 +- server/utils/urlBuilder.js | 70 +- server/validation/validator.js | 167 +-- shared/validation/constraints.js | 220 +-- 37 files changed, 2838 insertions(+), 2878 deletions(-) create mode 100644 server/jobs/FixNodeFilenamesJob.js create mode 100644 server/jobs/MailQueueJob.js create mode 100644 server/jobs/MonitoringMailsSendingJob.js create mode 100644 server/jobs/NodeInformationRetrievalJob.js create mode 100644 server/jobs/OfflineNodesDeletionJob.js delete mode 100644 server/jobs/fixNodeFilenamesJob.js delete mode 100644 server/jobs/mailQueueJob.js delete mode 100644 server/jobs/monitoringMailsSendingJob.js delete mode 100644 server/jobs/nodeInformationRetrievalJob.js delete mode 100644 server/jobs/offlineNodesDeletionJob.js delete mode 100644 server/libs.js diff --git a/package.json b/package.json index db59bfb..6e54c62 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,6 @@ "lodash": "^4.17.11", "moment": "^2.22.2", "ng-admin": "^1.0.13", - "ng-di": "^0.2.1", "node-cron": "^2.0.1", "nodemailer": "^4.6.8", "nodemailer-html-to-text": "^3.0.0", diff --git a/server/app.js b/server/app.js index ca6959c..12b32a6 100644 --- a/server/app.js +++ b/server/app.js @@ -1,16 +1,21 @@ 'use strict'; -angular.module('ffffng').factory('app', function (fs, config, _) { - var express = require('express'); - var auth = require('http-auth'); - var bodyParser = require('body-parser'); - var compress = require('compression'); +const _ = require('lodash') +const auth = require('http-auth'); +const bodyParser = require('body-parser'); +const compress = require('compression'); +const express = require('express'); +const fs = require('graceful-fs') - var app = express(); - var router = express.Router(); +const config = require('./config').config + +const app = express(); + +module.exports = (() => { + const router = express.Router(); // urls beneath /internal are protected - var internalAuth = auth.basic( + const internalAuth = auth.basic( { realm: 'Knotenformular - Intern' }, @@ -27,24 +32,24 @@ angular.module('ffffng').factory('app', function (fs, config, _) { router.use(bodyParser.json()); router.use(bodyParser.urlencoded({ extended: true })); - var adminDir = __dirname + '/../admin'; - var clientDir = __dirname + '/../client'; - var templateDir = __dirname + '/templates'; + const adminDir = __dirname + '/../admin'; + const clientDir = __dirname + '/../client'; + const templateDir = __dirname + '/templates'; - var jsTemplateFiles = [ + const jsTemplateFiles = [ '/config.js' ]; 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) { if (err) { return next(err); } - res.writeHead(200, {'Content-Type': mimeType}); - res.end(_.template(body)( { config: config.client })); + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(_.template(body)({ config: config.client })); return null; // to suppress warning }); @@ -63,4 +68,4 @@ angular.module('ffffng').factory('app', function (fs, config, _) { app.use(config.server.rootPath, router); return app; -}); +})() diff --git a/server/config.js b/server/config.js index 157a948..c605508 100644 --- a/server/config.js +++ b/server/config.js @@ -1,156 +1,158 @@ 'use strict'; -var commandLineArgs = require('command-line-args'); -var commandLineUsage = require('command-line-usage'); +module.exports = (() => { + const commandLineArgs = require('command-line-args'); + const commandLineUsage = require('command-line-usage'); -var commandLineDefs = [ - { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, - { name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, - { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } -]; + const commandLineDefs = [ + { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, + { name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, + { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } + ]; -var commandLineOptions; -try { - commandLineOptions = commandLineArgs(commandLineDefs); -} catch (e) { - console.error(e.message); - console.error('Try \'--help\' for more information.'); - process.exit(1); -} + let commandLineOptions; + try { + commandLineOptions = commandLineArgs(commandLineDefs); + } catch (e) { + console.error(e.message); + console.error('Try \'--help\' for more information.'); + process.exit(1); + } -var fs = require('graceful-fs'); + const fs = require('graceful-fs'); -var packageJsonFile = __dirname + '/../package.json'; -var version = 'unknown'; -if (fs.existsSync(packageJsonFile)) { - version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; -} + const packageJsonFile = __dirname + '/../package.json'; + let version = 'unknown'; + if (fs.existsSync(packageJsonFile)) { + version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; + } -function usage() { - console.log(commandLineUsage([ - { - header: 'ffffng - ' + version + ' - Freifunk node management form', - optionList: commandLineDefs - } - ])); -} + function usage () { + console.log(commandLineUsage([ + { + header: 'ffffng - ' + version + ' - Freifunk node management form', + optionList: commandLineDefs + } + ])); + } -if (commandLineOptions.help) { - usage(); - process.exit(0); -} + if (commandLineOptions.help) { + usage(); + process.exit(0); + } -if (commandLineOptions.version) { - console.log('ffffng - ' + version); - process.exit(0); -} + if (commandLineOptions.version) { + console.log('ffffng - ' + version); + process.exit(0); + } -if (!commandLineOptions.config) { - usage(); - process.exit(1); -} + if (!commandLineOptions.config) { + usage(); + process.exit(1); + } -var deepExtend = require('deep-extend'); + const deepExtend = require('deep-extend'); -var defaultConfig = { - server: { - baseUrl: 'http://localhost:8080', - port: 8080, + const defaultConfig = { + server: { + baseUrl: 'http://localhost:8080', + port: 8080, - databaseFile: '/tmp/ffffng.sqlite', - peersPath: '/tmp/peers', + databaseFile: '/tmp/ffffng.sqlite', + peersPath: '/tmp/peers', - logging: { - directory: '/tmp/logs', - debug: false, - profile: false, - logRequests: false - }, + logging: { + directory: '/tmp/logs', + debug: false, + profile: false, + logRequests: false + }, - internal: { - active: false, - user: 'admin', - password: 'secret' - }, + internal: { + active: false, + user: 'admin', + password: 'secret' + }, - email: { - from: 'Freifunk Knotenformular ', + email: { + from: 'Freifunk Knotenformular ', - // For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/ - smtp: { - host: 'mail.example.com', - port: '465', - secure: true, - auth: { - user: 'user@example.com', - pass: 'pass' + // For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/ + smtp: { + host: 'mail.example.com', + port: '465', + secure: true, + auth: { + user: 'user@example.com', + pass: 'pass' + } } + }, + + map: { + nodesJsonUrl: ['http://map.musterstadt.freifunk.net/nodes.json'] } }, - - map: { - nodesJsonUrl: ['http://map.musterstadt.freifunk.net/nodes.json'] + client: { + community: { + name: 'Freifunk Musterstadt', + domain: 'musterstadt.freifunk.net', + contactEmail: 'kontakt@musterstadt.freifunk.net', + sites: [], + domains: [] + }, + legal: { + privacyUrl: null, + imprintUrl: null + }, + map: { + mapUrl: 'http://map.musterstadt.freifunk.net' + }, + monitoring: { + enabled: false + }, + coordsSelector: { + showInfo: false, + showBorderForDebugging: false, + localCommunityPolygon: [], + lat: 53.565278, + lng: 10.001389, + defaultZoom: 10, + layers: {} + } } - }, - client: { - community: { - name: 'Freifunk Musterstadt', - domain: 'musterstadt.freifunk.net', - contactEmail: 'kontakt@musterstadt.freifunk.net', - sites: [], - domains: [] - }, - legal: { - privacyUrl: null, - imprintUrl: null - }, - map: { - mapUrl: 'http://map.musterstadt.freifunk.net' - }, - monitoring: { - enabled: false - }, - coordsSelector: { - showInfo: false, - showBorderForDebugging: false, - localCommunityPolygon: [], - lat: 53.565278, - lng: 10.001389, - defaultZoom: 10, - layers: {} + }; + + const configJSONFile = commandLineOptions.config; + let configJSON = {}; + + if (fs.existsSync(configJSONFile)) { + configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); + } else { + console.error('config.json not found: ' + configJSONFile); + process.exit(1); + } + + const _ = require('lodash'); + + function stripTrailingSlash (obj, field) { + const url = obj[field]; + if (_.isString(url) && _.last(url) === '/') { + obj[field] = url.substr(0, url.length - 1); } } -}; -var configJSONFile = commandLineOptions.config; -var configJSON = {}; + const config = deepExtend({}, defaultConfig, configJSON); -if (fs.existsSync(configJSONFile)) { - configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); -} else { - console.error('config.json not found: ' + configJSONFile); - process.exit(1); -} + stripTrailingSlash(config.server, 'baseUrl'); + stripTrailingSlash(config.client.map, 'mapUrl'); -var _ = require('lodash'); + const url = require('url'); + config.server.rootPath = url.parse(config.server.baseUrl).pathname; + config.client.rootPath = config.server.rootPath; -function stripTrailingSlash(obj, field) { - var url = obj[field]; - if (_.isString(url) && _.last(url) === '/') { - obj[field] = url.substr(0, url.length - 1); + return { + config, + version } -} - -var config = deepExtend({}, defaultConfig, configJSON); - -stripTrailingSlash(config.server, 'baseUrl'); -stripTrailingSlash(config.client.map, 'mapUrl'); - -var url = require('url'); -config.server.rootPath = url.parse(config.server.baseUrl).pathname; -config.client.rootPath = config.server.rootPath; - -module.exports = config; - -angular.module('ffffng').constant('config', config); -angular.module('ffffng').constant('version', version); +})() diff --git a/server/db/database.js b/server/db/database.js index 788b16d..6791326 100644 --- a/server/db/database.js +++ b/server/db/database.js @@ -5,7 +5,7 @@ const fs = require('graceful-fs'); const glob = util.promisify(require('glob')); const path = require('path'); -const config = require('../config'); +const config = require('../config').config; const Logger = require('../logger'); async function applyPatch(db, file) { @@ -72,10 +72,7 @@ async function init() { throw error; } - // WARNING: We have to use funtion() syntax here, to satisfy ng-di. m( - return angular.module('ffffng').factory('Database', function () { - return db; - }); + module.exports.db = db; } module.exports = { diff --git a/server/jobs/FixNodeFilenamesJob.js b/server/jobs/FixNodeFilenamesJob.js new file mode 100644 index 0000000..b47bf43 --- /dev/null +++ b/server/jobs/FixNodeFilenamesJob.js @@ -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(); + }); + } +} diff --git a/server/jobs/MailQueueJob.js b/server/jobs/MailQueueJob.js new file mode 100644 index 0000000..5f69763 --- /dev/null +++ b/server/jobs/MailQueueJob.js @@ -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(); + }); + } +} diff --git a/server/jobs/MonitoringMailsSendingJob.js b/server/jobs/MonitoringMailsSendingJob.js new file mode 100644 index 0000000..973ce74 --- /dev/null +++ b/server/jobs/MonitoringMailsSendingJob.js @@ -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(); + }); + } +} diff --git a/server/jobs/NodeInformationRetrievalJob.js b/server/jobs/NodeInformationRetrievalJob.js new file mode 100644 index 0000000..f0914e9 --- /dev/null +++ b/server/jobs/NodeInformationRetrievalJob.js @@ -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(); + }); + } +} diff --git a/server/jobs/OfflineNodesDeletionJob.js b/server/jobs/OfflineNodesDeletionJob.js new file mode 100644 index 0000000..8b0ca73 --- /dev/null +++ b/server/jobs/OfflineNodesDeletionJob.js @@ -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(); + }); + } +} diff --git a/server/jobs/fixNodeFilenamesJob.js b/server/jobs/fixNodeFilenamesJob.js deleted file mode 100644 index 652f277..0000000 --- a/server/jobs/fixNodeFilenamesJob.js +++ /dev/null @@ -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(); - }); - } - }; -}); diff --git a/server/jobs/mailQueueJob.js b/server/jobs/mailQueueJob.js deleted file mode 100644 index 799e878..0000000 --- a/server/jobs/mailQueueJob.js +++ /dev/null @@ -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(); - }); - } - }; -}); diff --git a/server/jobs/monitoringMailsSendingJob.js b/server/jobs/monitoringMailsSendingJob.js deleted file mode 100644 index 2c2d101..0000000 --- a/server/jobs/monitoringMailsSendingJob.js +++ /dev/null @@ -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(); - }); - } - }; -}); diff --git a/server/jobs/nodeInformationRetrievalJob.js b/server/jobs/nodeInformationRetrievalJob.js deleted file mode 100644 index 0248f30..0000000 --- a/server/jobs/nodeInformationRetrievalJob.js +++ /dev/null @@ -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(); - }); - } - }; -}); diff --git a/server/jobs/offlineNodesDeletionJob.js b/server/jobs/offlineNodesDeletionJob.js deleted file mode 100644 index 3d68797..0000000 --- a/server/jobs/offlineNodesDeletionJob.js +++ /dev/null @@ -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(); - }); - } - }; -}); diff --git a/server/jobs/scheduler.js b/server/jobs/scheduler.js index 6d9e9a3..5706155 100644 --- a/server/jobs/scheduler.js +++ b/server/jobs/scheduler.js @@ -1,98 +1,99 @@ 'use strict'; -var glob = require('glob'); -var _ = require('lodash'); +const _ = 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) { require(jobFile); }); -angular.module('ffffng').factory('Scheduler', function ($injector, Logger, config, moment) { - var cron = require('node-cron'); +const tasks = {}; - var tasks = {}; +let taskId = 1; +function nextTaskId() { + const id = taskId; + taskId += 1; + return id; +} - var taskId = 1; - function nextTaskId() { - var id = taskId; - taskId += 1; - return id; +function schedule(expr, jobName) { + Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName); + + var job = require(`../jobs/${jobName}`); + + if (!_.isFunction(job.run)) { + throw new Error('The job ' + jobName + ' does not provide a "run" function.'); } - function schedule(expr, jobName) { - Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName); - - var job = $injector.get(jobName); - - if (!_.isFunction(job.run)) { - throw new Error('The job ' + jobName + ' does not provide a "run" function.'); - } - - var id = nextTaskId(); - var task = { - id: id, - name: jobName, - description: job.description, - schedule: expr, - job: job, - runningSince: false, - lastRunStarted: false, - lastRunDuration: null, - state: 'idle', - enabled: true - }; - - task.run = function () { - if (task.runningSince || !task.enabled) { - // job is still running, skip execution - return; - } - - task.runningSince = moment(); - task.lastRunStarted = task.runningSince; - task.state = 'running'; - - job.run(function () { - var now = moment(); - var duration = now.diff(task.runningSince); - Logger.tag('jobs').profile('[%sms]\t%s', duration, task.name); - - task.runningSince = false; - task.lastRunDuration = duration; - task.state = 'idle'; - }); - }; - - cron.schedule(expr, task.run); - - tasks['' + id] = task; - } - - return { - init: function () { - Logger.tag('jobs').info('Scheduling background jobs...'); - - try { - schedule('0 */1 * * * *', 'MailQueueJob'); - schedule('15 */1 * * * *', 'FixNodeFilenamesJob'); - - if (config.client.monitoring.enabled) { - schedule('30 */15 * * * *', 'NodeInformationRetrievalJob'); - schedule('45 */5 * * * *', 'MonitoringMailsSendingJob'); - schedule('0 0 3 * * *', 'OfflineNodesDeletionJob'); // every night at 3:00 - } - } - catch (error) { - Logger.tag('jobs').error('Error during scheduling of background jobs:', error); - throw error; - } - - Logger.tag('jobs').info('Scheduling of background jobs done.'); - }, - - getTasks: function () { - return tasks; - } + var id = nextTaskId(); + var task = { + id: id, + name: jobName, + description: job.description, + schedule: expr, + job: job, + runningSince: false, + lastRunStarted: false, + lastRunDuration: null, + state: 'idle', + enabled: true }; -}); + + task.run = function () { + if (task.runningSince || !task.enabled) { + // job is still running, skip execution + return; + } + + task.runningSince = moment(); + task.lastRunStarted = task.runningSince; + task.state = 'running'; + + job.run(function () { + var now = moment(); + var duration = now.diff(task.runningSince); + Logger.tag('jobs').profile('[%sms]\t%s', duration, task.name); + + task.runningSince = false; + task.lastRunDuration = duration; + task.state = 'idle'; + }); + }; + + cron.schedule(expr, task.run); + + tasks['' + id] = task; +} + +module.exports = { + init: function () { + Logger.tag('jobs').info('Scheduling background jobs...'); + + try { + schedule('0 */1 * * * *', 'MailQueueJob'); + schedule('15 */1 * * * *', 'FixNodeFilenamesJob'); + + if (config.client.monitoring.enabled) { + schedule('30 */15 * * * *', 'NodeInformationRetrievalJob'); + schedule('45 */5 * * * *', 'MonitoringMailsSendingJob'); + schedule('0 0 3 * * *', 'OfflineNodesDeletionJob'); // every night at 3:00 + } + } + catch (error) { + Logger.tag('jobs').error('Error during scheduling of background jobs:', error); + throw error; + } + + Logger.tag('jobs').info('Scheduling of background jobs done.'); + }, + + getTasks: function () { + return tasks; + } +} diff --git a/server/libs.js b/server/libs.js deleted file mode 100644 index 1076083..0000000 --- a/server/libs.js +++ /dev/null @@ -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'); -})(); diff --git a/server/logger.js b/server/logger.js index 8663343..7d6de63 100644 --- a/server/logger.js +++ b/server/logger.js @@ -1,7 +1,7 @@ 'use strict'; -var config = require('./config'); - +const app = require('./app'); +const config = require('./config').config; // Hack to allow proper logging of Error. 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, }); @@ -33,30 +33,26 @@ function addLogger(name, color, active) { addLogger('debug', 'grey', config.server.logging.debug); addLogger('profile', 'blue', config.server.logging.profile); -angular.module('ffffng').factory('Logger', function (app) { - if (config.server.logging.logRequests) { - app.use(scribe.express.logger()); - } - if (config.server.internal.active) { - var prefix = config.server.rootPath === '/' ? '' : config.server.rootPath; - app.use(prefix + '/internal/logs', scribe.webPanel()); - } +if (config.server.logging.logRequests) { + app.use(scribe.express.logger()); +} +if (config.server.internal.active) { + const prefix = config.server.rootPath === '/' ? '' : config.server.rootPath; + app.use(prefix + '/internal/logs', scribe.webPanel()); +} - // Hack to allow correct logging of node.js Error objects. - // See: https://github.com/bluejamesbond/Scribe.js/issues/70 - Object.defineProperty(Error.prototype, 'toJSON', { - configurable: true, - value: function () { - var alt = {}; - var storeKey = function (key) { - alt[key] = this[key]; - }; - Object.getOwnPropertyNames(this).forEach(storeKey, this); - return alt; - } - }); - - return process.console; +// Hack to allow correct logging of node.js Error objects. +// See: https://github.com/bluejamesbond/Scribe.js/issues/70 +Object.defineProperty(Error.prototype, 'toJSON', { + configurable: true, + value: function () { + const alt = {}; + const storeKey = function (key) { + alt[key] = this[key]; + }; + Object.getOwnPropertyNames(this).forEach(storeKey, this); + return alt; + } }); module.exports = process.console; diff --git a/server/main.js b/server/main.js index 8e000c6..3e0263b 100755 --- a/server/main.js +++ b/server/main.js @@ -2,11 +2,6 @@ /*jslint node: true */ 'use strict'; -// Dirty hack to allow usage of angular modules. -global.angular = require('ng-di'); - -angular.module('ffffng', []); - (function () { // Use graceful-fs instead of fs also in all libraries to have more robust fs handling. const realFs = require('fs'); @@ -14,52 +9,24 @@ angular.module('ffffng', []); 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('./router'); -require('./libs'); +require('./db/database').init() +.then(() => { + Logger.tag('main').info('Initializing...'); -require('./utils/databaseUtil'); -require('./utils/errorTypes'); -require('./utils/resources'); -require('./utils/strings'); -require('./utils/urlBuilder'); + const app = require('./app'); -require('./resources/versionResource'); -require('./resources/statisticsResource'); -require('./resources/frontendResource'); -require('./resources/taskResource'); -require('./resources/mailResource'); -require('./resources/nodeResource'); -require('./resources/monitoringResource'); + require('./jobs/scheduler').init(); + require('./router').init(); -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...'); - - Scheduler.init(); - Router.init(); - - app.listen(config.server.port, '::'); - module.exports = app; - }); -}).catch(error => { + app.listen(config.server.port, '::'); + module.exports = app; +}) +.catch(error => { console.error('Could not init database: ', error); process.exit(1); }); diff --git a/server/resources/frontendResource.js b/server/resources/frontendResource.js index 4f4161a..0a7d9b0 100644 --- a/server/resources/frontendResource.js +++ b/server/resources/frontendResource.js @@ -1,31 +1,30 @@ 'use strict'; -angular.module('ffffng').factory('FrontendResource', function ( - Logger, - Resources, - ErrorTypes, - fs -) { - var indexHtml = __dirname + '/../../client/index.html'; +const fs = require('graceful-fs') - return { - render: function (req, res) { - var data = Resources.getData(req); +const ErrorTypes = require('../utils/errorTypes') +const Logger = require('../logger') +const Resources = require('../utils/resources') - fs.readFile(indexHtml, 'utf8', function (err, body) { - if (err) { - Logger.tag('frontend').error('Could not read file: ', indexHtml, err); - return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); - } +const indexHtml = __dirname + '/../../client/index.html'; - return Resources.successHtml( - res, - body.replace( - /window.__nodeToken = \''+ data.token + '\';window.__nodeToken = \''+ data.token + '\'; { + if (err) { + Logger.tag('statistics').error('Error getting statistics:', err); + return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); + } + + return Resources.success( + res, + { + nodes: nodeStatistics + } + ); + }); + } +} diff --git a/server/resources/taskResource.js b/server/resources/taskResource.js index 74317b9..5969816 100644 --- a/server/resources/taskResource.js +++ b/server/resources/taskResource.js @@ -1,127 +1,126 @@ 'use strict'; -angular.module('ffffng').factory('TaskResource', function ( - Constraints, - Validator, - _, - Strings, - Resources, - ErrorTypes, - Scheduler -) { - var isValidId = Validator.forConstraint(Constraints.id); +const _ = require('lodash') - function toExternalTask(task) { - return { - id: task.id, - name: task.name, - description: task.description, - schedule: task.schedule, - runningSince: task.runningSince && task.runningSince.unix(), - lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), - lastRunDuration: task.lastRunDuration || undefined, - state: task.state, - enabled: task.enabled - }; +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 { + id: task.id, + name: task.name, + description: task.description, + schedule: task.schedule, + runningSince: task.runningSince && task.runningSince.unix(), + lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), + lastRunDuration: task.lastRunDuration || undefined, + state: task.state, + enabled: task.enabled + }; +} + +function withValidTaskId(req, res, callback) { + const id = Strings.normalizeString(Resources.getData(req).id); + + if (!isValidId(id)) { + return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest}); } - function withValidTaskId(req, res, callback) { - var id = Strings.normalizeString(Resources.getData(req).id); + callback(null, id); +} - if (!isValidId(id)) { - return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest}); +function getTask(id, callback) { + const tasks = Scheduler.getTasks(); + const task = tasks[id]; + + if (!task) { + return callback({data: 'Task not found.', type: ErrorTypes.notFound}); + } + + callback(null, task); +} + +function withTask(req, res, callback) { + withValidTaskId(req, res, function (err, id) { + if (err) { + return callback(err); } - callback(null, id); - } - - function getTask(id, callback) { - var tasks = Scheduler.getTasks(); - var task = tasks[id]; - - if (!task) { - return callback({data: 'Task not found.', type: ErrorTypes.notFound}); - } - - callback(null, task); - } - - function withTask(req, res, callback) { - withValidTaskId(req, res, function (err, id) { + getTask(id, function (err, task) { if (err) { return callback(err); } - getTask(id, function (err, task) { - if (err) { - return callback(err); - } - - callback(null, task); - }); + callback(null, task); }); - } + }); +} - function setTaskEnabled(req, res, enable) { +function setTaskEnabled(req, res, enable) { + withTask(req, res, function (err, task) { + if (err) { + return Resources.error(res, err); + } + + task.enabled = !!enable; // ensure boolean + + return Resources.success(res, toExternalTask(task)); + }); +} + +module.exports = { + getAll (req, res) { + Resources.getValidRestParams('list', null, req, function (err, restParams) { + if (err) { + return Resources.error(res, err); + } + + const tasks = Resources.sort( + _.values(Scheduler.getTasks()), + ['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], + restParams + ); + const filteredTasks = Resources.filter( + tasks, + ['id', 'name', 'schedule', 'state'], + restParams + ); + const total = filteredTasks.length; + + const pageTasks = Resources.getPageEntities(filteredTasks, restParams); + + res.set('X-Total-Count', total); + return Resources.success(res, _.map(pageTasks, toExternalTask)); + }); + }, + + run (req, res) { withTask(req, res, function (err, task) { if (err) { return Resources.error(res, err); } - task.enabled = !!enable; // ensure boolean + if (task.runningSince) { + return Resources.error(res, {data: 'Task already running.', type: ErrorTypes.conflict}); + } + + task.run(); return Resources.success(res, toExternalTask(task)); }); + }, + + enable (req, res) { + setTaskEnabled(req, res, true); + }, + + disable (req, res) { + setTaskEnabled(req, res, false); } - - return { - getAll: function (req, res) { - Resources.getValidRestParams('list', null, req, function (err, restParams) { - if (err) { - return Resources.error(res, err); - } - - var tasks = Resources.sort( - _.values(Scheduler.getTasks()), - ['id', 'name', 'schedule', 'state', 'runningSince', 'lastRunStarted'], - restParams - ); - var filteredTasks = Resources.filter( - tasks, - ['id', 'name', 'schedule', 'state'], - restParams - ); - var total = filteredTasks.length; - - var pageTasks = Resources.getPageEntities(filteredTasks, restParams); - - res.set('X-Total-Count', total); - return Resources.success(res, _.map(pageTasks, toExternalTask)); - }); - }, - - run: function (req, res) { - withTask(req, res, function (err, task) { - if (err) { - return Resources.error(res, err); - } - - if (task.runningSince) { - return Resources.error(res, {data: 'Task already running.', type: ErrorTypes.conflict}); - } - - task.run(); - - return Resources.success(res, toExternalTask(task)); - }); - }, - - enable: function (req, res) { - setTaskEnabled(req, res, true); - }, - - disable: function (req, res) { - setTaskEnabled(req, res, false); - } - }; -}); +} diff --git a/server/resources/versionResource.js b/server/resources/versionResource.js index 0cf47be..d323afb 100644 --- a/server/resources/versionResource.js +++ b/server/resources/versionResource.js @@ -1,17 +1,15 @@ 'use strict'; -angular.module('ffffng').factory('VersionResource', function ( - version, - Resources -) { - return { - get: function (req, res) { - return Resources.success( - res, - { - version: version - } - ); - } - }; -}); +const Resources = require('../utils/resources') +const version = require('../config').version + +module.exports = { + get (req, res) { + return Resources.success( + res, + { + version + } + ); + } +} diff --git a/server/router.js b/server/router.js index e13d5e8..32ebade 100644 --- a/server/router.js +++ b/server/router.js @@ -1,53 +1,52 @@ 'use strict'; -angular.module('ffffng').factory('Router', function ( - app, - VersionResource, - StatisticsResource, - FrontendResource, - NodeResource, - MonitoringResource, - TaskResource, - MailResource, - config -) { - return { - init: function () { - var express = require('express'); - var router = express.Router(); +const express = require('express'); - router.post('/', FrontendResource.render); +const app = require('./app') +const config = require('./config').config +const VersionResource = require('./resources/versionResource') +const StatisticsResource = require('./resources/statisticsResource') +const FrontendResource = require('./resources/frontendResource') +const NodeResource = require('./resources/nodeResource') +const MonitoringResource = require('./resources/monitoringResource') +const TaskResource = require('./resources/taskResource') +const MailResource = require('./resources/mailResource') - router.get('/api/version', VersionResource.get); +module.exports = { + init () { + const router = express.Router(); - router.post('/api/node', NodeResource.create); - router.put('/api/node/:token', NodeResource.update); - router.delete('/api/node/:token', NodeResource.delete); - router.get('/api/node/:token', NodeResource.get); + router.post('/', FrontendResource.render); - router.put('/api/monitoring/confirm/:token', MonitoringResource.confirm); - router.put('/api/monitoring/disable/:token', MonitoringResource.disable); + router.get('/api/version', VersionResource.get); - router.get('/internal/api/statistics', StatisticsResource.get); + router.post('/api/node', NodeResource.create); + router.put('/api/node/:token', NodeResource.update); + router.delete('/api/node/:token', NodeResource.delete); + router.get('/api/node/:token', NodeResource.get); - router.get('/internal/api/tasks', TaskResource.getAll); - router.put('/internal/api/tasks/run/:id', TaskResource.run); - router.put('/internal/api/tasks/enable/:id', TaskResource.enable); - router.put('/internal/api/tasks/disable/:id', TaskResource.disable); + router.put('/api/monitoring/confirm/:token', MonitoringResource.confirm); + router.put('/api/monitoring/disable/:token', MonitoringResource.disable); - router.get('/internal/api/monitoring', MonitoringResource.getAll); + router.get('/internal/api/statistics', StatisticsResource.get); - router.get('/internal/api/mails', MailResource.getAll); - router.get('/internal/api/mails/:id', MailResource.get); - router.delete('/internal/api/mails/:id', MailResource.delete); - router.put('/internal/api/mails/reset/:id', MailResource.resetFailures); + router.get('/internal/api/tasks', TaskResource.getAll); + router.put('/internal/api/tasks/run/:id', TaskResource.run); + router.put('/internal/api/tasks/enable/:id', TaskResource.enable); + router.put('/internal/api/tasks/disable/:id', TaskResource.disable); - router.put('/internal/api/nodes/:token', NodeResource.update); - router.delete('/internal/api/nodes/:token', NodeResource.delete); - router.get('/internal/api/nodes', NodeResource.getAll); - router.get('/internal/api/nodes/:token', NodeResource.get); + router.get('/internal/api/monitoring', MonitoringResource.getAll); - app.use(config.server.rootPath, router); - } - }; -}); + router.get('/internal/api/mails', MailResource.getAll); + router.get('/internal/api/mails/:id', MailResource.get); + router.delete('/internal/api/mails/:id', MailResource.delete); + router.put('/internal/api/mails/reset/:id', MailResource.resetFailures); + + router.put('/internal/api/nodes/:token', NodeResource.update); + router.delete('/internal/api/nodes/:token', NodeResource.delete); + router.get('/internal/api/nodes', NodeResource.getAll); + router.get('/internal/api/nodes/:token', NodeResource.get); + + app.use(config.server.rootPath, router); + } +} diff --git a/server/services/mailService.js b/server/services/mailService.js index 531370f..74626c2 100644 --- a/server/services/mailService.js +++ b/server/services/mailService.js @@ -1,238 +1,235 @@ 'use strict'; -angular.module('ffffng') -.service('MailService', function ( - Database, - MailTemplateService, - config, - _, - async, - deepExtend, - fs, - moment, - Logger, - Resources -) { - var MAIL_QUEUE_DB_BATCH_SIZE = 50; - var MAIL_QUEUE_MAX_PARALLEL_SENDING = 3; +const _ = require('lodash') +const async = require('async') +const deepExtend = require('deep-extend') +const moment = require('moment') - var transporter = require('nodemailer').createTransport(deepExtend( - {}, - config.server.email.smtp, - { - transport: 'smtp', - pool: true - } - )); +const config = require('../config').config +const Database = require('../db/database').db +const Logger = require('../logger') +const MailTemplateService = require('./mailTemplateService') +const Resources = require('../utils/resources') - MailTemplateService.configureTransporter(transporter); +const MAIL_QUEUE_DB_BATCH_SIZE = 50; +const MAIL_QUEUE_MAX_PARALLEL_SENDING = 3; - function sendMail(options, callback) { - Logger - .tag('mail', 'queue') - .info( - 'Sending pending mail[%d] of type %s. ' + - 'Had %d failures before.', - options.id, options.email, options.failures - ); +const transporter = require('nodemailer').createTransport(deepExtend( + {}, + config.server.email.smtp, + { + transport: 'smtp', + pool: true + } +)); - MailTemplateService.render(options, function (err, renderedTemplate) { - if (err) { - return callback(err); - } +MailTemplateService.configureTransporter(transporter); - var mailOptions = { - from: options.sender, - to: options.recipient, - subject: renderedTemplate.subject, - html: renderedTemplate.body - }; - - transporter.sendMail(mailOptions, function (err) { - if (err) { - return callback(err); - } - - Logger.tag('mail', 'queue').info('Mail[%d] has been send.', options.id); - - callback(null); - }); - } +function sendMail(options, callback) { + Logger + .tag('mail', 'queue') + .info( + 'Sending pending mail[%d] of type %s. ' + + 'Had %d failures before.', + options.id, options.email, options.failures ); - } - function findPendingMailsBefore(beforeMoment, limit, callback) { - Database.all( - 'SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?', - [beforeMoment.unix(), 5, limit], - function (err, rows) { - if (err) { - return callback(err); - } - - var pendingMails; - try { - pendingMails = _.map(rows, function (row) { - return deepExtend( - {}, - row, - { - data: JSON.parse(row.data) - } - ); - }); - } - catch (error) { - return callback(error); - } - - callback(null, pendingMails); - } - ); - } - - function removePendingMailFromQueue(id, callback) { - Database.run('DELETE FROM email_queue WHERE id = ?', [id], callback); - } - - function incrementFailureCounterForPendingEmail(id, callback) { - var now = moment(); - Database.run( - 'UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?', - [now.unix(), id], - callback - ); - } - - function sendPendingMail(pendingMail, callback) { - sendMail(pendingMail, function (err) { + MailTemplateService.render(options, function (err, renderedTemplate) { if (err) { - // we only log the error and increment the failure counter as we want to continue with pending mails - Logger.tag('mail', 'queue').error('Error sending pending mail[' + pendingMail.id + ']:', err); - - return incrementFailureCounterForPendingEmail(pendingMail.id, function (err) { - if (err) { - return callback(err); - } - return callback(null); - }); + return callback(err); } - removePendingMailFromQueue(pendingMail.id, callback); - }); - } - - function doGetMail(id, callback) { - Database.get('SELECT * FROM email_queue WHERE id = ?', [id], callback); - } - - return { - enqueue: function (sender, recipient, email, data, callback) { - if (!_.isPlainObject(data)) { - return callback(new Error('Unexpected data: ' + data)); - } - Database.run( - 'INSERT INTO email_queue ' + - '(failures, sender, recipient, email, data) ' + - 'VALUES (?, ?, ?, ?, ?)', - [0, sender, recipient, email, JSON.stringify(data)], - function (err, res) { - callback(err, res); - } - ); - }, - - getMail: function (id, callback) { - doGetMail(id, callback); - }, - - getPendingMails: function (restParams, callback) { - Database.get( - 'SELECT count(*) AS total FROM email_queue', - [], - function (err, row) { - if (err) { - return callback(err); - } - - var total = row.total; - - var filter = Resources.filterClause( - restParams, - 'id', - ['id', 'failures', 'sender', 'recipient', 'email', 'created_at', 'modified_at'], - ['id', 'failures', 'sender', 'recipient', 'email'] - ); - - Database.all( - 'SELECT * FROM email_queue WHERE ' + filter.query, - _.concat([], filter.params), - function (err, rows) { - if (err) { - return callback(err); - } - - callback(null, rows, total); - } - ); - } - ); - }, - - deleteMail: function (id, callback) { - removePendingMailFromQueue(id, callback); - }, - - resetFailures: function (id, callback) { - Database.run( - 'UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?', - [moment().unix(), id], - function (err) { - if (err) { - return callback(err); - } - - if (!this.changes) { - return callback('Error: could not reset failure count for mail: ' + id); - } - - doGetMail(id, callback); - } - ); - }, - - sendPendingMails: function (callback) { - Logger.tag('mail', 'queue').debug('Start sending pending mails...'); - - var startTime = moment(); - - var sendNextBatch = function (err) { - if (err) { - return callback(err); - } - - Logger.tag('mail', 'queue').debug('Sending next batch...'); - - findPendingMailsBefore(startTime, MAIL_QUEUE_DB_BATCH_SIZE, function (err, pendingMails) { - if (err) { - return callback(err); - } - - if (_.isEmpty(pendingMails)) { - Logger.tag('mail', 'queue').debug('Done sending pending mails.'); - return callback(null); - } - - async.eachLimit( - pendingMails, - MAIL_QUEUE_MAX_PARALLEL_SENDING, - sendPendingMail, - sendNextBatch - ); - }); + const mailOptions = { + from: options.sender, + to: options.recipient, + subject: renderedTemplate.subject, + html: renderedTemplate.body }; - sendNextBatch(null); + transporter.sendMail(mailOptions, function (err) { + if (err) { + return callback(err); + } + + Logger.tag('mail', 'queue').info('Mail[%d] has been send.', options.id); + + callback(null); + }); } - }; -}); + ); +} + +function findPendingMailsBefore(beforeMoment, limit, callback) { + Database.all( + 'SELECT * FROM email_queue WHERE modified_at < ? AND failures < ? ORDER BY id ASC LIMIT ?', + [beforeMoment.unix(), 5, limit], + function (err, rows) { + if (err) { + return callback(err); + } + + let pendingMails; + try { + pendingMails = _.map(rows, function (row) { + return deepExtend( + {}, + row, + { + data: JSON.parse(row.data) + } + ); + }); + } + catch (error) { + return callback(error); + } + + callback(null, pendingMails); + } + ); +} + +function removePendingMailFromQueue(id, callback) { + Database.run('DELETE FROM email_queue WHERE id = ?', [id], callback); +} + +function incrementFailureCounterForPendingEmail(id, callback) { + const now = moment(); + Database.run( + 'UPDATE email_queue SET failures = failures + 1, modified_at = ? WHERE id = ?', + [now.unix(), id], + callback + ); +} + +function sendPendingMail(pendingMail, callback) { + sendMail(pendingMail, function (err) { + if (err) { + // we only log the error and increment the failure counter as we want to continue with pending mails + Logger.tag('mail', 'queue').error('Error sending pending mail[' + pendingMail.id + ']:', err); + + return incrementFailureCounterForPendingEmail(pendingMail.id, function (err) { + if (err) { + return callback(err); + } + return callback(null); + }); + } + + removePendingMailFromQueue(pendingMail.id, callback); + }); +} + +function doGetMail(id, callback) { + Database.get('SELECT * FROM email_queue WHERE id = ?', [id], callback); +} + +module.exports = { + enqueue (sender, recipient, email, data, callback) { + if (!_.isPlainObject(data)) { + return callback(new Error('Unexpected data: ' + data)); + } + Database.run( + 'INSERT INTO email_queue ' + + '(failures, sender, recipient, email, data) ' + + 'VALUES (?, ?, ?, ?, ?)', + [0, sender, recipient, email, JSON.stringify(data)], + function (err, res) { + callback(err, res); + } + ); + }, + + getMail (id, callback) { + doGetMail(id, callback); + }, + + getPendingMails (restParams, callback) { + Database.get( + 'SELECT count(*) AS total FROM email_queue', + [], + function (err, row) { + if (err) { + return callback(err); + } + + const total = row.total; + + const filter = Resources.filterClause( + restParams, + 'id', + ['id', 'failures', 'sender', 'recipient', 'email', 'created_at', 'modified_at'], + ['id', 'failures', 'sender', 'recipient', 'email'] + ); + + Database.all( + 'SELECT * FROM email_queue WHERE ' + filter.query, + _.concat([], filter.params), + function (err, rows) { + if (err) { + return callback(err); + } + + callback(null, rows, total); + } + ); + } + ); + }, + + deleteMail (id, callback) { + removePendingMailFromQueue(id, callback); + }, + + resetFailures (id, callback) { + Database.run( + 'UPDATE email_queue SET failures = 0, modified_at = ? WHERE id = ?', + [moment().unix(), id], + function (err) { + if (err) { + return callback(err); + } + + if (!this.changes) { + return callback('Error: could not reset failure count for mail: ' + id); + } + + doGetMail(id, callback); + } + ); + }, + + sendPendingMails (callback) { + Logger.tag('mail', 'queue').debug('Start sending pending mails...'); + + const startTime = moment(); + + const sendNextBatch = function (err) { + if (err) { + return callback(err); + } + + Logger.tag('mail', 'queue').debug('Sending next batch...'); + + findPendingMailsBefore(startTime, MAIL_QUEUE_DB_BATCH_SIZE, function (err, pendingMails) { + if (err) { + return callback(err); + } + + if (_.isEmpty(pendingMails)) { + Logger.tag('mail', 'queue').debug('Done sending pending mails.'); + return callback(null); + } + + async.eachLimit( + pendingMails, + MAIL_QUEUE_MAX_PARALLEL_SENDING, + sendPendingMail, + sendNextBatch + ); + }); + }; + + sendNextBatch(null); + } +} diff --git a/server/services/mailTemplateService.js b/server/services/mailTemplateService.js index cdecc1f..c7db65b 100644 --- a/server/services/mailTemplateService.js +++ b/server/services/mailTemplateService.js @@ -1,125 +1,122 @@ 'use strict'; -angular.module('ffffng') -.service('MailTemplateService', function ( - UrlBuilder, - config, - _, - async, - deepExtend, - fs, - moment, - Logger -) { - var templateBasePath = __dirname + '/../mailTemplates'; - var snippetsBasePath = templateBasePath + '/snippets'; +const _ = require('lodash') +const async = require('async') +const deepExtend = require('deep-extend') +const fs = require('graceful-fs') +const moment = require('moment') - var templateFunctions = {}; +const config = require('../config').config +const Logger = require('../logger') +const UrlBuilder = require('../utils/urlBuilder') - function renderSnippet(name, data) { - var snippetFile = snippetsBasePath + '/' + name + '.html'; +const templateBasePath = __dirname + '/../mailTemplates'; +const snippetsBasePath = templateBasePath + '/snippets'; - return _.template(fs.readFileSync(snippetFile).toString())(deepExtend( - {}, - // jshint -W040 - this, // parent data - // jshint +W040 - data, - templateFunctions - )); - } +const templateFunctions = {}; - function snippet(name) { - return function (data) { - return renderSnippet.bind(this)(name, data); - }; - } +function renderSnippet(name, data) { + const snippetFile = snippetsBasePath + '/' + name + '.html'; - function renderLink(href, text) { - return _.template( - '<%- text %>' - )({ - href: href, - text: text || href - }); - } + return _.template(fs.readFileSync(snippetFile).toString())(deepExtend( + {}, + // jshint -W040 + this, // parent data + // jshint +W040 + data, + templateFunctions + )); +} - function renderHR() { - return '
'; - } - - function formatDateTime(unix) { - return moment.unix(unix).locale('de').local().format('DD.MM.YYYY HH:mm'); - } - - function formatFromNow(unix) { - return moment.unix(unix).locale('de').fromNow(); - } - - templateFunctions.header = snippet('header'); - templateFunctions.footer = snippet('footer'); - - templateFunctions.monitoringFooter = snippet('monitoring-footer'); - - templateFunctions.snippet = renderSnippet; - - templateFunctions.link = renderLink; - templateFunctions.hr = renderHR; - - templateFunctions.formatDateTime = formatDateTime; - templateFunctions.formatFromNow = formatFromNow; - - return { - configureTransporter: function (transporter) { - var htmlToText = require('nodemailer-html-to-text').htmlToText; - transporter.use('compile', htmlToText({ - tables: ['.table'] - })); - }, - - render: function (mailOptions, callback) { - var templatePathPrefix = templateBasePath + '/' + mailOptions.email; - - async.parallel({ - subject: _.partial(fs.readFile, templatePathPrefix + '.subject.txt'), - body: _.partial(fs.readFile, templatePathPrefix + '.body.html') - }, - function (err, templates) { - if (err) { - return callback(err); - } - - var data = deepExtend( - {}, - mailOptions.data, - { - community: config.client.community, - editNodeUrl: UrlBuilder.editNodeUrl() - }, - templateFunctions - ); - - function render(field) { - return _.template(templates[field].toString())(data); - } - - var renderedTemplate; - try { - renderedTemplate = { - subject: _.trim(render('subject')), - body: render('body') - }; - } - catch (error) { - Logger - .tag('mail', 'template') - .error('Error rendering template for mail[' + mailOptions.id + ']:', error); - return callback(error); - } - - callback(null, renderedTemplate); - } - ); - } +function snippet(name) { + return function (data) { + return renderSnippet.bind(this)(name, data); }; -}); +} + +function renderLink(href, text) { + return _.template( + '<%- text %>' + )({ + href: href, + text: text || href + }); +} + +function renderHR() { + return '
'; +} + +function formatDateTime(unix) { + return moment.unix(unix).locale('de').local().format('DD.MM.YYYY HH:mm'); +} + +function formatFromNow(unix) { + return moment.unix(unix).locale('de').fromNow(); +} + +templateFunctions.header = snippet('header'); +templateFunctions.footer = snippet('footer'); + +templateFunctions.monitoringFooter = snippet('monitoring-footer'); + +templateFunctions.snippet = renderSnippet; + +templateFunctions.link = renderLink; +templateFunctions.hr = renderHR; + +templateFunctions.formatDateTime = formatDateTime; +templateFunctions.formatFromNow = formatFromNow; + +module.exports = { + configureTransporter (transporter) { + const htmlToText = require('nodemailer-html-to-text').htmlToText; + transporter.use('compile', htmlToText({ + tables: ['.table'] + })); + }, + + render (mailOptions, callback) { + const templatePathPrefix = templateBasePath + '/' + mailOptions.email; + + async.parallel({ + subject: _.partial(fs.readFile, templatePathPrefix + '.subject.txt'), + body: _.partial(fs.readFile, templatePathPrefix + '.body.html') + }, + function (err, templates) { + if (err) { + return callback(err); + } + + const data = deepExtend( + {}, + mailOptions.data, + { + community: config.client.community, + editNodeUrl: UrlBuilder.editNodeUrl() + }, + templateFunctions + ); + + function render (field) { + return _.template(templates[field].toString())(data); + } + + let renderedTemplate; + try { + renderedTemplate = { + subject: _.trim(render('subject')), + body: render('body') + }; + } catch (error) { + Logger + .tag('mail', 'template') + .error('Error rendering template for mail[' + mailOptions.id + ']:', error); + return callback(error); + } + + callback(null, renderedTemplate); + } + ); + } +} diff --git a/server/services/monitoringService.js b/server/services/monitoringService.js index 7a17039..a5a8ece 100644 --- a/server/services/monitoringService.js +++ b/server/services/monitoringService.js @@ -1,829 +1,826 @@ 'use strict'; -angular.module('ffffng') -.service('MonitoringService', function ( - _, - async, - config, - deepExtend, - Database, - DatabaseUtil, - ErrorTypes, - Logger, - moment, - MailService, - NodeService, - request, - Strings, - UrlBuilder, - Validator, - Constraints, - Resources -) { - 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 - */ - var MONITORING_OFFLINE_MAILS_SCHEDULE = { - 1: { amount: 3, unit: 'hours' }, - 2: { amount: 1, unit: 'days' }, - 3: { amount: 7, unit: 'days' } - }; - var DELETE_OFFLINE_NODES_AFTER_DURATION = { - amount: 100, - unit: 'days' - }; +const _ = require('lodash') +const async = require('async') +const moment = require('moment') +const request = require('request') - var previousImportTimestamp = null; +const config = require('../config').config +const Constraints = require('../../shared/validation/constraints') +const Database = require('../db/database').db +const DatabaseUtil = require('../utils/databaseUtil') +const ErrorTypes = require('../utils/errorTypes') +const Logger = require('../logger') +const MailService = require('../services/mailService') +const NodeService = require('../services/nodeService') +const Resources = require('../utils/resources') +const Strings = require('../utils/strings') +const UrlBuilder = require('../utils/urlBuilder') +const Validator = require('../validation/validator') - function insertNodeInformation(nodeData, node, callback) { +const MONITORING_STATE_MACS_CHUNK_SIZE = 100; +const MONITORING_MAILS_DB_BATCH_SIZE = 50; +/** + * Defines the intervals emails are sent if a node is offline + */ +const MONITORING_OFFLINE_MAILS_SCHEDULE = { + 1: { amount: 3, unit: 'hours' }, + 2: { amount: 1, unit: 'days' }, + 3: { amount: 7, unit: 'days' } +}; +const DELETE_OFFLINE_NODES_AFTER_DURATION = { + amount: 100, + unit: 'days' +}; + +let previousImportTimestamp = null; + +function insertNodeInformation(nodeData, node, callback) { + Logger + .tag('monitoring', 'information-retrieval') + .debug('Node is new in monitoring, creating data: %s', nodeData.mac); + + return Database.run( + 'INSERT INTO node_state ' + + '(hostname, mac, site, domain, monitoring_state, state, last_seen, import_timestamp, last_status_mail_sent, last_status_mail_type) ' + + 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + [ + node.hostname, + node.mac, + nodeData.site, + nodeData.domain, + node.monitoringState, + nodeData.state, + nodeData.lastSeen.unix(), + nodeData.importTimestamp.unix(), + null, // new node so we haven't send a mail yet + null // new node so we haven't send a mail yet + ], + callback + ); +} + +function updateNodeInformation(nodeData, node, row, callback) { + Logger + .tag('monitoring', 'information-retrieval') + .debug('Node is known in monitoring: %s', nodeData.mac); + + // jshint -W106 + if (!moment(row.import_timestamp).isBefore(nodeData.importTimestamp)) { + // jshint +W106 Logger .tag('monitoring', 'information-retrieval') - .debug('Node is new in monitoring, creating data: %s', nodeData.mac); + .debug('No new data for node, skipping: %s', nodeData.mac); + return callback(); + } - return Database.run( - 'INSERT INTO node_state ' + - '(hostname, mac, site, domain, monitoring_state, state, last_seen, import_timestamp, last_status_mail_sent, last_status_mail_type) ' + - 'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', - [ - node.hostname, - node.mac, - nodeData.site, - nodeData.domain, - node.monitoringState, - nodeData.state, - nodeData.lastSeen.unix(), - nodeData.importTimestamp.unix(), - null, // new node so we haven't send a mail yet - null // new node so we haven't send a mail yet - ], - callback + Logger + .tag('monitoring', 'information-retrieval') + .debug('New data for node, updating: %s', nodeData.mac); + + return Database.run( + 'UPDATE node_state ' + + 'SET ' + + 'hostname = ?, ' + + 'site = ?, ' + + 'domain = ?, ' + + 'monitoring_state = ?, ' + + 'state = ?, ' + + 'last_seen = ?, ' + + 'import_timestamp = ?, ' + + 'modified_at = ? ' + + 'WHERE id = ? AND mac = ?', + [ + node.hostname, + nodeData.site || row.site, + nodeData.domain || row.domain, + node.monitoringState, + nodeData.state, + nodeData.lastSeen.unix(), + nodeData.importTimestamp.unix(), + moment().unix(), + + row.id, + node.mac + ], + callback + ); +} + +function storeNodeInformation(nodeData, node, callback) { + 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) { + if (err) { + return callback(err); + } + + let nodeDataForStoring; + if (nodeData === 'missing') { + nodeDataForStoring = { + mac: node.mac, + site: _.isUndefined(row) ? null : row.site, + domain: _.isUndefined(row) ? null : row.domain, + state: 'OFFLINE', + // jshint -W106 + lastSeen: _.isUndefined(row) ? moment() : moment.unix(row.last_seen), + // jshint +W106 + importTimestamp: moment() + }; + } else { + nodeDataForStoring = nodeData; + } + + if (_.isUndefined(row)) { + return insertNodeInformation(nodeDataForStoring, node, callback); + } else { + return updateNodeInformation(nodeDataForStoring, node, row, callback); + } + }); +} + +const isValidMac = Validator.forConstraint(Constraints.node.mac); + +function parseTimestamp (timestamp) { + if (!_.isString(timestamp)) { + return moment.invalid(); + } + return moment.utc(timestamp); +} + +function parseNode (importTimestamp, nodeData, nodeId) { + if (!_.isPlainObject(nodeData)) { + throw new Error( + 'Node ' + nodeId + ': Unexpected node type: ' + (typeof nodeData) ); } - function updateNodeInformation(nodeData, node, row, callback) { - Logger - .tag('monitoring', 'information-retrieval') - .debug('Node is known in monitoring: %s', nodeData.mac); - - // jshint -W106 - if (!moment(row.import_timestamp).isBefore(nodeData.importTimestamp)) { - // jshint +W106 - Logger - .tag('monitoring', 'information-retrieval') - .debug('No new data for node, skipping: %s', nodeData.mac); - return callback(); - } - - Logger - .tag('monitoring', 'information-retrieval') - .debug('New data for node, updating: %s', nodeData.mac); - - return Database.run( - 'UPDATE node_state ' + - 'SET ' + - 'hostname = ?, ' + - 'site = ?, ' + - 'domain = ?, ' + - 'monitoring_state = ?, ' + - 'state = ?, ' + - 'last_seen = ?, ' + - 'import_timestamp = ?, ' + - 'modified_at = ? ' + - 'WHERE id = ? AND mac = ?', - [ - node.hostname, - nodeData.site || row.site, - nodeData.domain || row.domain, - node.monitoringState, - nodeData.state, - nodeData.lastSeen.unix(), - nodeData.importTimestamp.unix(), - moment().unix(), - - row.id, - node.mac - ], - callback + if (!_.isPlainObject(nodeData.nodeinfo)) { + throw new Error( + 'Node ' + nodeId + ': Unexpected nodeinfo type: ' + (typeof nodeData.nodeinfo) + ); + } + if (!_.isPlainObject(nodeData.nodeinfo.network)) { + throw new Error( + 'Node ' + nodeId + ': Unexpected nodeinfo.network type: ' + (typeof nodeData.nodeinfo.network) ); } - function storeNodeInformation(nodeData, node, callback) { - Logger.tag('monitoring', 'information-retrieval').debug('Storing status for node: %s', nodeData.mac); + if (!isValidMac(nodeData.nodeinfo.network.mac)) { + throw new Error( + 'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac + ); + } + const mac = Strings.normalizeMac(nodeData.nodeinfo.network.mac); - return Database.get('SELECT * FROM node_state WHERE mac = ?', [node.mac], function (err, row) { - if (err) { - return callback(err); - } + if (!_.isPlainObject(nodeData.flags)) { + throw new Error( + 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags) + ); + } + if (!_.isBoolean(nodeData.flags.online)) { + throw new Error( + 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) + ); + } + const isOnline = nodeData.flags.online; - var nodeDataForStoring; - if (nodeData === 'missing') { - nodeDataForStoring = { - mac: node.mac, - site: _.isUndefined(row) ? null : row.site, - domain: _.isUndefined(row) ? null : row.domain, - state: 'OFFLINE', - // jshint -W106 - lastSeen: _.isUndefined(row) ? moment() : moment.unix(row.last_seen), - // jshint +W106 - importTimestamp: moment() - }; - } else { - nodeDataForStoring = nodeData; - } - - if (_.isUndefined(row)) { - return insertNodeInformation(nodeDataForStoring, node, callback); - } else { - return updateNodeInformation(nodeDataForStoring, node, row, callback); - } - }); + const lastSeen = parseTimestamp(nodeData.lastseen); + if (!lastSeen.isValid()) { + throw new Error( + 'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen + ); } - var isValidMac = Validator.forConstraint(Constraints.node.mac); - - function parseTimestamp (timestamp) { - if (!_.isString(timestamp)) { - return moment.invalid(); - } - return moment.utc(timestamp); + let site = null; + // jshint -W106 + if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.site_code)) { + site = nodeData.nodeinfo.system.site_code; } + // jshint +W106 - function parseNode (importTimestamp, nodeData, nodeId) { - if (!_.isPlainObject(nodeData)) { - throw new Error( - 'Node ' + nodeId + ': Unexpected node type: ' + (typeof nodeData) - ); - } - - if (!_.isPlainObject(nodeData.nodeinfo)) { - throw new Error( - 'Node ' + nodeId + ': Unexpected nodeinfo type: ' + (typeof nodeData.nodeinfo) - ); - } - if (!_.isPlainObject(nodeData.nodeinfo.network)) { - throw new Error( - 'Node ' + nodeId + ': Unexpected nodeinfo.network type: ' + (typeof nodeData.nodeinfo.network) - ); - } - - if (!isValidMac(nodeData.nodeinfo.network.mac)) { - throw new Error( - 'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac - ); - } - var mac = Strings.normalizeMac(nodeData.nodeinfo.network.mac); - - if (!_.isPlainObject(nodeData.flags)) { - throw new Error( - 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags) - ); - } - if (!_.isBoolean(nodeData.flags.online)) { - throw new Error( - 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online) - ); - } - var isOnline = nodeData.flags.online; - - var lastSeen = parseTimestamp(nodeData.lastseen); - if (!lastSeen.isValid()) { - throw new Error( - 'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen - ); - } - - var site = null; - // jshint -W106 - if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.site_code)) { - site = nodeData.nodeinfo.system.site_code; - } - // jshint +W106 - - var domain = null; - // jshint -W106 - if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.domain_code)) { - domain = nodeData.nodeinfo.system.domain_code; - } - // jshint +W106 - - return { - mac: mac, - importTimestamp: importTimestamp, - state: isOnline ? 'ONLINE' : 'OFFLINE', - lastSeen: lastSeen, - site: site, - domain: domain - }; + let domain = null; + // jshint -W106 + if (_.isPlainObject(nodeData.nodeinfo.system) && _.isString(nodeData.nodeinfo.system.domain_code)) { + domain = nodeData.nodeinfo.system.domain_code; } + // jshint +W106 - function parseNodesJson (body, callback) { - Logger.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...'); + return { + mac: mac, + importTimestamp: importTimestamp, + state: isOnline ? 'ONLINE' : 'OFFLINE', + lastSeen: lastSeen, + site: site, + domain: domain + }; +} - var data = {}; +function parseNodesJson (body, callback) { + Logger.tag('monitoring', 'information-retrieval').debug('Parsing nodes.json...'); - try { - var json = JSON.parse(body); + const data = {}; - if (json.version !== 1) { - return callback(new Error('Unexpected nodes.json version: ' + json.version)); - } - data.importTimestamp = parseTimestamp(json.timestamp); + try { + const json = JSON.parse(body); - if (!data.importTimestamp.isValid()) { - return callback(new Error('Invalid timestamp: ' + json.timestamp)); - } + if (json.version !== 1) { + return callback(new Error('Unexpected nodes.json version: ' + json.version)); + } + data.importTimestamp = parseTimestamp(json.timestamp); - if (!_.isPlainObject(json.nodes)) { - return callback(new Error('Invalid nodes object type: ' + (typeof json.nodes))); - } + if (!data.importTimestamp.isValid()) { + return callback(new Error('Invalid timestamp: ' + json.timestamp)); + } - data.nodes = _.filter( - _.values( - _.map( - json.nodes, - function (nodeData, nodeId) { - try { - return parseNode(data.importTimestamp, nodeData, nodeId); - } - catch (error) { - Logger.tag('monitoring', 'information-retrieval').error(error); - return null; - } + if (!_.isPlainObject(json.nodes)) { + return callback(new Error('Invalid nodes object type: ' + (typeof json.nodes))); + } + + data.nodes = _.filter( + _.values( + _.map( + json.nodes, + function (nodeData, nodeId) { + try { + return parseNode(data.importTimestamp, nodeData, nodeId); } - ) - ), - function (node) { - return node !== null; - } - ); - } - catch (error) { - return callback(error); - } - - callback(null, data); - } - - function updateSkippedNode(id, node, callback) { - Database.run( - 'UPDATE node_state ' + - 'SET hostname = ?, monitoring_state = ?, modified_at = ?' + - 'WHERE id = ?', - [ - node ? node.hostname : '', node ? node.monitoringState : '', moment().unix(), - id - ], - callback + catch (error) { + Logger.tag('monitoring', 'information-retrieval').error(error); + return null; + } + } + ) + ), + function (node) { + return node !== null; + } ); } + catch (error) { + return callback(error); + } - function sendMonitoringMailsBatched(name, mailType, findBatchFun, callback) { - Logger.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name); + callback(null, data); +} - var sendNextBatch = function (err) { +function updateSkippedNode(id, node, callback) { + Database.run( + 'UPDATE node_state ' + + 'SET hostname = ?, monitoring_state = ?, modified_at = ?' + + 'WHERE id = ?', + [ + node ? node.hostname : '', node ? node.monitoringState : '', moment().unix(), + id + ], + callback + ); +} + +function sendMonitoringMailsBatched(name, mailType, findBatchFun, callback) { + Logger.tag('monitoring', 'mail-sending').debug('Sending "%s" mails...', name); + + const sendNextBatch = function (err) { + if (err) { + return callback(err); + } + + Logger.tag('monitoring', 'mail-sending').debug('Sending next batch...'); + + findBatchFun(function (err, nodeStates) { if (err) { return callback(err); } - Logger.tag('monitoring', 'mail-sending').debug('Sending next batch...'); - - findBatchFun(function (err, nodeStates) { - if (err) { - return callback(err); - } - - if (_.isEmpty(nodeStates)) { - Logger.tag('monitoring', 'mail-sending').debug('Done sending "%s" mails.', name); - return callback(null); - } - - async.each( - nodeStates, - function (nodeState, mailCallback) { - var mac = nodeState.mac; - Logger.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac); - NodeService.getNodeDataByMac(mac, function (err, node, nodeSecrets) { - if (err) { - Logger - .tag('monitoring', 'mail-sending') - .error('Error sending "' + name + '" mail for node: ' + mac, err); - return mailCallback(err); - } - - if (!node) { - Logger - .tag('monitoring', 'mail-sending') - .debug( - 'Node not found. Skipping sending of "' + name + '" mail: ' + mac - ); - return updateSkippedNode(nodeState.id, {}, mailCallback); - } - - if (node.monitoring && node.monitoringConfirmed) { - Logger - .tag('monitoring', 'mail-sending') - .info('Sending "%s" mail for: %s', name, mac); - MailService.enqueue( - config.server.email.from, - node.nickname + ' <' + node.email + '>', - mailType, - { - node: node, - // jshint -W106 - lastSeen: nodeState.last_seen, - // jshint +W106 - disableUrl: UrlBuilder.monitoringDisableUrl(nodeSecrets) - - }, - function (err) { - if (err) { - Logger - .tag('monitoring', 'mail-sending') - .error('Error sending "' + name + '" mail for node: ' + mac, err); - return mailCallback(err); - } - - Logger - .tag('monitoring', 'mail-sending') - .debug('Updating node state: ', mac); - - var now = moment().unix(); - Database.run( - 'UPDATE node_state ' + - 'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' + - 'WHERE id = ?', - [ - node.hostname, node.monitoringState, now, now, mailType, - nodeState.id - ], - mailCallback - ); - } - ); - } else { - Logger - .tag('monitoring', 'mail-sending') - .debug('Monitoring disabled, skipping "%s" mail for: %s', name, mac); - return updateSkippedNode(nodeState.id, {}, mailCallback); - } - }); - }, - sendNextBatch - ); - }); - }; - - sendNextBatch(null); - } - - function sendOnlineAgainMails(startTime, callback) { - sendMonitoringMailsBatched( - 'online again', - 'monitoring-online-again', - function (findBatchCallback) { - Database.all( - 'SELECT * FROM node_state ' + - 'WHERE modified_at < ? AND state = ? AND last_status_mail_type IN (' + - '\'monitoring-offline-1\', \'monitoring-offline-2\', \'monitoring-offline-3\'' + - ')' + - 'ORDER BY id ASC LIMIT ?', - [ - startTime.unix(), - 'ONLINE', - - MONITORING_MAILS_DB_BATCH_SIZE - ], - findBatchCallback - ); - }, - callback - ); - } - - /** - * sends one of three mails if a node is offline - * @param {moment} startTime the moment the job started - * @param {Number} mailNumber which of three mails - * @param {Function} callback gets all nodes that are offline - */ - function sendOfflineMails(startTime, mailNumber, callback) { - sendMonitoringMailsBatched( - 'offline ' + mailNumber, - 'monitoring-offline-' + mailNumber, - function (findBatchCallback) { - /** - * descriptive string that stores, which was the last mail type, stored in the database as last_status_mail_type - */ - var previousType = - mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1)); - - // 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' : ''; - - var schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber]; - var scheduledTimeBefore = moment().subtract(schedule.amount, schedule.unit); - - Database.all( - 'SELECT * FROM node_state ' + - 'WHERE modified_at < ? AND state = ? AND (last_status_mail_type = ?' + allowNull + ') AND ' + - 'last_seen <= ? AND (last_status_mail_sent <= ? OR last_status_mail_sent IS NULL) ' + - 'ORDER BY id ASC LIMIT ?', - [ - startTime.unix(), - 'OFFLINE', - previousType, - scheduledTimeBefore.unix(), - scheduledTimeBefore.unix(), - - MONITORING_MAILS_DB_BATCH_SIZE - ], - findBatchCallback - ); - }, - callback - ); - } - - function withUrlsData(urls, callback) { - async.map(urls, function (url, urlCallback) { - Logger.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url); - request(url, function (err, response, body) { - if (err) { - return urlCallback(err); - } - - if (response.statusCode !== 200) { - return urlCallback(new Error( - 'Could not download nodes.json from ' + url + ': ' + - response.statusCode + ' - ' + response.statusMessage - )); - } - - parseNodesJson(body, urlCallback); - }); - }, callback); - } - - function retrieveNodeInformationForUrls(urls, callback) { - withUrlsData(urls, function (err, datas) { - if (err) { - return callback(err); + if (_.isEmpty(nodeStates)) { + Logger.tag('monitoring', 'mail-sending').debug('Done sending "%s" mails.', name); + return callback(null); } - var maxTimestamp = datas[0].importTimestamp; - var minTimestamp = maxTimestamp; - _.each(datas, function (data) { - if (data.importTimestamp.isAfter(maxTimestamp)) { - maxTimestamp = data.importTimestamp; - } - if (data.importTimestamp.isBefore(minTimestamp)) { - minTimestamp = data.importTimestamp; - } - }); - - if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) { - Logger - .tag('monitoring', 'information-retrieval') - .debug( - 'No new data, skipping. Current timestamp: %s, previous timestamp: %s', - maxTimestamp.format(), - previousImportTimestamp.format() - ); - return callback(); - } - previousImportTimestamp = maxTimestamp; - - // 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. - var allNodes = _.flatMap(datas, function (data) { - return data.nodes; - }); - - // Get rid of duplicates from different nodes.json files. Always use the one with the newest - var sortedNodes = _.orderBy(allNodes, [function (node) { - return node.lastSeen.unix(); - }], ['desc']); - var uniqueNodes = _.uniqBy(sortedNodes, function (node) { - return node.mac; - }); - async.eachSeries( - uniqueNodes, - function (nodeData, nodeCallback) { - Logger.tag('monitoring', 'information-retrieval').debug('Importing: %s', nodeData.mac); - - NodeService.getNodeDataByMac(nodeData.mac, function (err, node) { + async.each( + nodeStates, + function (nodeState, mailCallback) { + const mac = nodeState.mac; + Logger.tag('monitoring', 'mail-sending').debug('Loading node data for: %s', mac); + NodeService.getNodeDataByMac(mac, function (err, node, nodeSecrets) { if (err) { Logger - .tag('monitoring', 'information-retrieval') - .error('Error importing: ' + nodeData.mac, err); - return nodeCallback(err); + .tag('monitoring', 'mail-sending') + .error('Error sending "' + name + '" mail for node: ' + mac, err); + return mailCallback(err); } if (!node) { Logger - .tag('monitoring', 'information-retrieval') - .debug('Unknown node, skipping: %s', nodeData.mac); - return nodeCallback(null); + .tag('monitoring', 'mail-sending') + .debug( + 'Node not found. Skipping sending of "' + name + '" mail: ' + mac + ); + return updateSkippedNode(nodeState.id, {}, mailCallback); } - storeNodeInformation(nodeData, node, function (err) { - if (err) { - Logger - .tag('monitoring', 'information-retrieval') - .debug('Could not update / deleting node data: %s', nodeData.mac, err); - return nodeCallback(err); - } - + if (node.monitoring && node.monitoringConfirmed) { Logger - .tag('monitoring', 'information-retrieval') - .debug('Updating / deleting node data done: %s', nodeData.mac); + .tag('monitoring', 'mail-sending') + .info('Sending "%s" mail for: %s', name, mac); + MailService.enqueue( + config.server.email.from, + node.nickname + ' <' + node.email + '>', + mailType, + { + node: node, + // jshint -W106 + lastSeen: nodeState.last_seen, + // jshint +W106 + disableUrl: UrlBuilder.monitoringDisableUrl(nodeSecrets) - nodeCallback(); - }); + }, + function (err) { + if (err) { + Logger + .tag('monitoring', 'mail-sending') + .error('Error sending "' + name + '" mail for node: ' + mac, err); + return mailCallback(err); + } + + Logger + .tag('monitoring', 'mail-sending') + .debug('Updating node state: ', mac); + + const now = moment().unix(); + Database.run( + 'UPDATE node_state ' + + 'SET hostname = ?, monitoring_state = ?, modified_at = ?, last_status_mail_sent = ?, last_status_mail_type = ?' + + 'WHERE id = ?', + [ + node.hostname, node.monitoringState, now, now, mailType, + nodeState.id + ], + mailCallback + ); + } + ); + } else { + Logger + .tag('monitoring', 'mail-sending') + .debug('Monitoring disabled, skipping "%s" mail for: %s', name, mac); + return updateSkippedNode(nodeState.id, {}, mailCallback); + } }); }, - function (err) { - if (err) { - return callback(err); - } - - Logger - .tag('monitoring', 'information-retrieval') - .debug('Marking missing nodes as offline.'); - - // Mark nodes as offline that haven't been imported in this run. - Database.run( - 'UPDATE node_state ' + - 'SET state = ?, modified_at = ?' + - 'WHERE import_timestamp < ?', - [ - 'OFFLINE', moment().unix(), - minTimestamp.unix() - ], - callback - ); - } + sendNextBatch ); }); - } + }; - return { - getAll: function (restParams, callback) { - var sortFields = [ - 'id', - 'hostname', - 'mac', - 'site', - 'domain', - 'monitoring_state', - 'state', - 'last_seen', - 'import_timestamp', - 'last_status_mail_type', - 'last_status_mail_sent', - 'created_at', - 'modified_at' - ]; - var filterFields = [ - 'hostname', - 'mac', - 'monitoring_state', - 'state', - 'last_status_mail_type' - ]; + sendNextBatch(null); +} - var where = Resources.whereCondition(restParams, filterFields); +function sendOnlineAgainMails(startTime, callback) { + sendMonitoringMailsBatched( + 'online again', + 'monitoring-online-again', + function (findBatchCallback) { + Database.all( + 'SELECT * FROM node_state ' + + 'WHERE modified_at < ? AND state = ? AND last_status_mail_type IN (' + + '\'monitoring-offline-1\', \'monitoring-offline-2\', \'monitoring-offline-3\'' + + ')' + + 'ORDER BY id ASC LIMIT ?', + [ + startTime.unix(), + 'ONLINE', - Database.get( - 'SELECT count(*) AS total FROM node_state WHERE ' + where.query, - _.concat([], where.params), - function (err, row) { - if (err) { - return callback(err); - } - - var total = row.total; - - var filter = Resources.filterClause( - restParams, - 'id', - sortFields, - filterFields - ); - - Database.all( - 'SELECT * FROM node_state WHERE ' + filter.query, - _.concat([], filter.params), - function (err, rows) { - if (err) { - return callback(err); - } - - callback(null, rows, total); - } - ); - } + MONITORING_MAILS_DB_BATCH_SIZE + ], + findBatchCallback ); }, + callback + ); +} - getByMacs: function (macs, callback) { - if (_.isEmpty(macs)) { - return callback(null, {}); +/** + * sends one of three mails if a node is offline + * @param {moment} startTime the moment the job started + * @param {Number} mailNumber which of three mails + * @param {Function} callback gets all nodes that are offline + */ +function sendOfflineMails(startTime, mailNumber, callback) { + sendMonitoringMailsBatched( + 'offline ' + mailNumber, + 'monitoring-offline-' + mailNumber, + function (findBatchCallback) { + /** + * descriptive string that stores, which was the last mail type, stored in the database as last_status_mail_type + */ + const previousType = + mailNumber === 1 ? 'monitoring-online-again' : ('monitoring-offline-' + (mailNumber - 1)); + + // the first time the first offline mail is send, there was no mail before + const allowNull = mailNumber === 1 ? ' OR last_status_mail_type IS NULL' : ''; + + const schedule = MONITORING_OFFLINE_MAILS_SCHEDULE[mailNumber]; + const scheduledTimeBefore = moment().subtract(schedule.amount, schedule.unit); + + Database.all( + 'SELECT * FROM node_state ' + + 'WHERE modified_at < ? AND state = ? AND (last_status_mail_type = ?' + allowNull + ') AND ' + + 'last_seen <= ? AND (last_status_mail_sent <= ? OR last_status_mail_sent IS NULL) ' + + 'ORDER BY id ASC LIMIT ?', + [ + startTime.unix(), + 'OFFLINE', + previousType, + scheduledTimeBefore.unix(), + scheduledTimeBefore.unix(), + + MONITORING_MAILS_DB_BATCH_SIZE + ], + findBatchCallback + ); + }, + callback + ); +} + +function withUrlsData(urls, callback) { + async.map(urls, function (url, urlCallback) { + Logger.tag('monitoring', 'information-retrieval').debug('Retrieving nodes.json: %s', url); + request(url, function (err, response, body) { + if (err) { + return urlCallback(err); } - async.map( - _.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE), - function (subMacs, subCallback) { - var inCondition = DatabaseUtil.inCondition('mac', subMacs); + if (response.statusCode !== 200) { + return urlCallback(new Error( + 'Could not download nodes.json from ' + url + ': ' + + response.statusCode + ' - ' + response.statusMessage + )); + } - Database.all( - 'SELECT * FROM node_state WHERE ' + inCondition.query, - _.concat([], inCondition.params), - subCallback - ); - }, - function (err, rowsArrays) { + parseNodesJson(body, urlCallback); + }); + }, callback); +} + +function retrieveNodeInformationForUrls(urls, callback) { + withUrlsData(urls, function (err, datas) { + if (err) { + return callback(err); + } + + let maxTimestamp = datas[0].importTimestamp; + let minTimestamp = maxTimestamp; + _.each(datas, function (data) { + if (data.importTimestamp.isAfter(maxTimestamp)) { + maxTimestamp = data.importTimestamp; + } + if (data.importTimestamp.isBefore(minTimestamp)) { + minTimestamp = data.importTimestamp; + } + }); + + if (previousImportTimestamp !== null && !maxTimestamp.isAfter(previousImportTimestamp)) { + Logger + .tag('monitoring', 'information-retrieval') + .debug( + 'No new data, skipping. Current timestamp: %s, previous timestamp: %s', + maxTimestamp.format(), + previousImportTimestamp.format() + ); + return callback(); + } + previousImportTimestamp = maxTimestamp; + + // 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. + const allNodes = _.flatMap(datas, function (data) { + return data.nodes; + }); + + // Get rid of duplicates from different nodes.json files. Always use the one with the newest + const sortedNodes = _.orderBy(allNodes, [function (node) { + return node.lastSeen.unix(); + }], ['desc']); + const uniqueNodes = _.uniqBy(sortedNodes, function (node) { + return node.mac; + }); + async.eachSeries( + uniqueNodes, + function (nodeData, nodeCallback) { + Logger.tag('monitoring', 'information-retrieval').debug('Importing: %s', nodeData.mac); + + NodeService.getNodeDataByMac(nodeData.mac, function (err, node) { if (err) { - return callback(err); + Logger + .tag('monitoring', 'information-retrieval') + .error('Error importing: ' + nodeData.mac, err); + return nodeCallback(err); } - var nodeStateByMac = {}; - _.each(_.flatten(rowsArrays), function (row) { - nodeStateByMac[row.mac] = row; + if (!node) { + Logger + .tag('monitoring', 'information-retrieval') + .debug('Unknown node, skipping: %s', nodeData.mac); + return nodeCallback(null); + } + + storeNodeInformation(nodeData, node, function (err) { + if (err) { + Logger + .tag('monitoring', 'information-retrieval') + .debug('Could not update / deleting node data: %s', nodeData.mac, err); + return nodeCallback(err); + } + + Logger + .tag('monitoring', 'information-retrieval') + .debug('Updating / deleting node data done: %s', nodeData.mac); + + nodeCallback(); }); - - return callback(null, nodeStateByMac); - } - ); - }, - - confirm: function (token, callback) { - NodeService.getNodeDataByMonitoringToken(token, function (err, node, nodeSecrets) { + }); + }, + function (err) { if (err) { return callback(err); } - if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { - return callback({data: 'Invalid token.', type: ErrorTypes.badRequest}); - } + Logger + .tag('monitoring', 'information-retrieval') + .debug('Marking missing nodes as offline.'); - if (node.monitoringConfirmed) { - return callback(null, node); - } - - node.monitoringConfirmed = true; - NodeService.internalUpdateNode(node.token, node, nodeSecrets, function (err, token, node) { - if (err) { - return callback(err); - } - callback(null, node); - }); - }); - }, - - disable: function (token, callback) { - NodeService.getNodeDataByMonitoringToken(token, function (err, node, nodeSecrets) { - if (err) { - return callback(err); - } - - if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { - return callback({data: 'Invalid token.', type: ErrorTypes.badRequest}); - } - - node.monitoring = false; - node.monitoringConfirmed = false; - nodeSecrets.monitoringToken = ''; - - NodeService.internalUpdateNode(node.token, node, nodeSecrets, function (err, token, node) { - if (err) { - return callback(err); - } - callback(null, node); - }); - }); - }, - - retrieveNodeInformation: function (callback) { - var urls = config.server.map.nodesJsonUrl; - if (_.isEmpty(urls)) { - return callback( - new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl') + // Mark nodes as offline that haven't been imported in this run. + Database.run( + 'UPDATE node_state ' + + 'SET state = ?, modified_at = ?' + + 'WHERE import_timestamp < ?', + [ + 'OFFLINE', moment().unix(), + minTimestamp.unix() + ], + callback ); } - if (_.isString(urls)) { - urls = [urls]; + ); + }); +} + +module.exports = { + getAll: function (restParams, callback) { + const sortFields = [ + 'id', + 'hostname', + 'mac', + 'site', + 'domain', + 'monitoring_state', + 'state', + 'last_seen', + 'import_timestamp', + 'last_status_mail_type', + 'last_status_mail_sent', + 'created_at', + 'modified_at' + ]; + const filterFields = [ + 'hostname', + 'mac', + 'monitoring_state', + 'state', + 'last_status_mail_type' + ]; + + const where = Resources.whereCondition(restParams, filterFields); + + Database.get( + 'SELECT count(*) AS total FROM node_state WHERE ' + where.query, + _.concat([], where.params), + function (err, row) { + if (err) { + return callback(err); + } + + const total = row.total; + + const filter = Resources.filterClause( + restParams, + 'id', + sortFields, + filterFields + ); + + Database.all( + 'SELECT * FROM node_state WHERE ' + filter.query, + _.concat([], filter.params), + function (err, rows) { + if (err) { + return callback(err); + } + + callback(null, rows, total); + } + ); + } + ); + }, + + getByMacs: function (macs, callback) { + if (_.isEmpty(macs)) { + return callback(null, {}); + } + + async.map( + _.chunk(macs, MONITORING_STATE_MACS_CHUNK_SIZE), + function (subMacs, subCallback) { + const inCondition = DatabaseUtil.inCondition('mac', subMacs); + + Database.all( + 'SELECT * FROM node_state WHERE ' + inCondition.query, + _.concat([], inCondition.params), + subCallback + ); + }, + function (err, rowsArrays) { + if (err) { + return callback(err); + } + + const nodeStateByMac = {}; + _.each(_.flatten(rowsArrays), function (row) { + nodeStateByMac[row.mac] = row; + }); + + return callback(null, nodeStateByMac); + } + ); + }, + + confirm: function (token, callback) { + NodeService.getNodeDataByMonitoringToken(token, function (err, node, nodeSecrets) { + if (err) { + return callback(err); } - retrieveNodeInformationForUrls(urls, callback); - }, + if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { + return callback({data: 'Invalid token.', type: ErrorTypes.badRequest}); + } - sendMonitoringMails: function (callback) { - Logger.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...'); + if (node.monitoringConfirmed) { + return callback(null, node); + } - var startTime = moment(); + node.monitoringConfirmed = true; + NodeService.internalUpdateNode(node.token, node, nodeSecrets, function (err, token, node) { + if (err) { + return callback(err); + } + callback(null, node); + }); + }); + }, - sendOnlineAgainMails(startTime, function (err) { + disable: function (token, callback) { + NodeService.getNodeDataByMonitoringToken(token, function (err, node, nodeSecrets) { + if (err) { + return callback(err); + } + + if (!node.monitoring || !nodeSecrets.monitoringToken || nodeSecrets.monitoringToken !== token) { + return callback({data: 'Invalid token.', type: ErrorTypes.badRequest}); + } + + node.monitoring = false; + node.monitoringConfirmed = false; + nodeSecrets.monitoringToken = ''; + + NodeService.internalUpdateNode(node.token, node, nodeSecrets, function (err, token, node) { + if (err) { + return callback(err); + } + callback(null, node); + }); + }); + }, + + retrieveNodeInformation: function (callback) { + let urls = config.server.map.nodesJsonUrl; + if (_.isEmpty(urls)) { + return callback( + new Error('No nodes.json-URLs set. Please adjust config.json: server.map.nodesJsonUrl') + ); + } + if (_.isString(urls)) { + urls = [urls]; + } + + retrieveNodeInformationForUrls(urls, callback); + }, + + sendMonitoringMails: function (callback) { + Logger.tag('monitoring', 'mail-sending').debug('Sending monitoring mails...'); + + const startTime = moment(); + + sendOnlineAgainMails(startTime, function (err) { + if (err) { + // only logging an continuing with next type + Logger + .tag('monitoring', 'mail-sending') + .error('Error sending "online again" mails.', err); + } + + sendOfflineMails(startTime, 1, function (err) { if (err) { // only logging an continuing with next type Logger .tag('monitoring', 'mail-sending') - .error('Error sending "online again" mails.', err); + .error('Error sending "offline 1" mails.', err); } - sendOfflineMails(startTime, 1, function (err) { + sendOfflineMails(startTime, 2, function (err) { if (err) { // only logging an continuing with next type Logger .tag('monitoring', 'mail-sending') - .error('Error sending "offline 1" mails.', err); + .error('Error sending "offline 2" mails.', err); } - sendOfflineMails(startTime, 2, function (err) { + sendOfflineMails(startTime, 3, function (err) { if (err) { // only logging an continuing with next type Logger .tag('monitoring', 'mail-sending') - .error('Error sending "offline 2" mails.', err); + .error('Error sending "offline 3" mails.', err); } - sendOfflineMails(startTime, 3, function (err) { - if (err) { - // only logging an continuing with next type - Logger - .tag('monitoring', 'mail-sending') - .error('Error sending "offline 3" mails.', err); - } - - callback(null); - }); + callback(null); }); }); }); - }, + }); + }, - deleteOfflineNodes: function (callback) { - Logger - .tag('nodes', 'delete-offline') - .info( - 'Deleting offline nodes older than ' + - DELETE_OFFLINE_NODES_AFTER_DURATION.amount + ' ' + + deleteOfflineNodes: function (callback) { + Logger + .tag('nodes', 'delete-offline') + .info( + 'Deleting offline nodes older than ' + + DELETE_OFFLINE_NODES_AFTER_DURATION.amount + ' ' + + DELETE_OFFLINE_NODES_AFTER_DURATION.unit + ); + + Database.all( + 'SELECT * FROM node_state WHERE state = ? AND last_seen < ?', + [ + 'OFFLINE', + moment().subtract( + DELETE_OFFLINE_NODES_AFTER_DURATION.amount, DELETE_OFFLINE_NODES_AFTER_DURATION.unit - ); + ).unix() + ], + function (err, rows) { + async.eachSeries( + rows, + function (row, nodeCallback) { + const mac = row.mac; + Logger.tag('nodes', 'delete-offline').info('Deleting node ' + mac); + NodeService.getNodeDataByMac(mac, function (err, node) { + if (err) { + Logger.tag('nodes', 'delete-offline').error('Error getting node ' + mac, err); + return nodeCallback(err); + } - Database.all( - 'SELECT * FROM node_state WHERE state = ? AND last_seen < ?', - [ - 'OFFLINE', - moment().subtract( - DELETE_OFFLINE_NODES_AFTER_DURATION.amount, - DELETE_OFFLINE_NODES_AFTER_DURATION.unit - ).unix() - ], - function (err, rows) { - async.eachSeries( - rows, - function (row, nodeCallback) { - var mac = row.mac; - Logger.tag('nodes', 'delete-offline').info('Deleting node ' + mac); - NodeService.getNodeDataByMac(mac, function (err, node) { + async.seq( + function (callback) { + if (node && node.token) { + // If the node has no token it is a special node (e.g. a gateway) + // we need to skip. + return NodeService.deleteNode(node.token, callback); + } + return callback(null); + }, + function (callback) { + Database.run( + 'DELETE FROM node_state WHERE mac = ? AND state = ?', + [mac, 'OFFLINE'], + callback + ); + } + )(function (err) { if (err) { - Logger.tag('nodes', 'delete-offline').error('Error getting node ' + mac, err); + Logger.tag('nodes', 'delete-offline').error('Error deleting node ' + mac, err); return nodeCallback(err); } - async.seq( - function (callback) { - if (node && node.token) { - // If the node has no token it is a special node (e.g. a gateway) - // we need to skip. - return NodeService.deleteNode(node.token, callback); - } - return callback(null); - }, - function (callback) { - Database.run( - 'DELETE FROM node_state WHERE mac = ? AND state = ?', - [mac, 'OFFLINE'], - callback - ); - } - )(function (err) { - if (err) { - Logger.tag('nodes', 'delete-offline').error('Error deleting node ' + mac, err); - return nodeCallback(err); - } - - nodeCallback(null); - }); + nodeCallback(null); }); - }, - callback - ); - } - ); - } - }; -}); + }); + }, + callback + ); + } + ); + } +} diff --git a/server/services/nodeService.js b/server/services/nodeService.js index a2b2479..8e343f0 100644 --- a/server/services/nodeService.js +++ b/server/services/nodeService.js @@ -1,346 +1,398 @@ 'use strict'; -angular.module('ffffng') -.service('NodeService', function ( - config, - _, - async, - crypto, - fs, - glob, - Logger, - MailService, - Strings, - ErrorTypes, - UrlBuilder -) { - var MAX_PARALLEL_NODES_PARSING = 10; +const _ = require('lodash') +const async = require('async') +const crypto = require('crypto') +const fs = require('graceful-fs') +const glob = require('glob') - var linePrefixes = { - hostname: '# Knotenname: ', - nickname: '# Ansprechpartner: ', - email: '# Kontakt: ', - coords: '# Koordinaten: ', - mac: '# MAC: ', - token: '# Token: ', - monitoring: '# Monitoring: ', - monitoringToken: '# Monitoring-Token: ' - }; +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') - var filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken']; +const MAX_PARALLEL_NODES_PARSING = 10; - function generateToken() { - return crypto.randomBytes(8).toString('hex'); - } +const linePrefixes = { + hostname: '# Knotenname: ', + nickname: '# Ansprechpartner: ', + email: '# Kontakt: ', + coords: '# Koordinaten: ', + mac: '# MAC: ', + token: '# Token: ', + monitoring: '# Monitoring: ', + monitoringToken: '# Monitoring-Token: ' +}; - function toNodeFilesPattern(filter) { - var pattern = _.join( - _.map(filenameParts, function (field) { - return filter.hasOwnProperty(field) ? filter[field] : '*'; - }), - '@' - ); +const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken']; - return config.server.peersPath + '/' + pattern.toLowerCase(); - } +function generateToken() { + return crypto.randomBytes(8).toString('hex'); +} - function findNodeFiles(filter, callback) { - glob(toNodeFilesPattern(filter), callback); - } +function toNodeFilesPattern(filter) { + const pattern = _.join( + _.map(filenameParts, function (field) { + return filter.hasOwnProperty(field) ? filter[field] : '*'; + }), + '@' + ); - function findNodeFilesSync(filter) { - return glob.sync(toNodeFilesPattern(filter)); - } + return config.server.peersPath + '/' + pattern.toLowerCase(); +} - function findFilesInPeersPath(callback) { - glob(config.server.peersPath + '/*', function (err, files) { - if (err) { - return callback(err); +function findNodeFiles(filter, callback) { + glob(toNodeFilesPattern(filter), callback); +} + +function findNodeFilesSync(filter) { + return glob.sync(toNodeFilesPattern(filter)); +} + +function findFilesInPeersPath(callback) { + glob(config.server.peersPath + '/*', function (err, files) { + if (err) { + return callback(err); + } + + async.filter(files, function (file, fileCallback) { + if (file[0] === '.') { + return fileCallback(null, false); } - async.filter(files, function (file, fileCallback) { - if (file[0] === '.') { - return fileCallback(null, false); + fs.lstat(file, function (err, stats) { + if (err) { + return fileCallback(err); } - fs.lstat(file, function (err, stats) { - if (err) { - return fileCallback(err); - } + fileCallback(null, stats.isFile()); + }); + }, callback); + }); +} - fileCallback(null, stats.isFile()); - }); - }, callback); - }); +function parseNodeFilename(filename) { + const parts = _.split(filename, '@', filenameParts.length); + const parsed = {}; + _.each(_.zip(filenameParts, parts), function (part) { + parsed[part[0]] = part[1]; + }); + return parsed; +} + +function isDuplicate(filter, token) { + const files = findNodeFilesSync(filter); + if (files.length === 0) { + return false; } - function parseNodeFilename(filename) { - var parts = _.split(filename, '@', filenameParts.length); - var parsed = {}; - _.each(_.zip(filenameParts, parts), function (part) { - parsed[part[0]] = part[1]; - }); - return parsed; + if (files.length > 1 || !token /* node is being created*/) { + return true; } - function isDuplicate(filter, token) { - var files = findNodeFilesSync(filter); - if (files.length === 0) { - return false; - } + return parseNodeFilename(files[0]).token !== token; +} - if (files.length > 1 || !token /* node is being created*/) { - return true; - } - - return parseNodeFilename(files[0]).token !== token; +function checkNoDuplicates(token, node, nodeSecrets) { + if (isDuplicate({ hostname: node.hostname }, token)) { + return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict}; } - function checkNoDuplicates(token, node, nodeSecrets) { - if (isDuplicate({ hostname: node.hostname }, token)) { - return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict}; + if (node.key) { + if (isDuplicate({ key: node.key }, token)) { + return {data: {msg: 'Already exists.', field: 'key'}, type: ErrorTypes.conflict}; } - - if (node.key) { - if (isDuplicate({ key: node.key }, token)) { - return {data: {msg: 'Already exists.', field: 'key'}, type: ErrorTypes.conflict}; - } - } - - if (isDuplicate({ mac: node.mac }, token)) { - return {data: {msg: 'Already exists.', field: 'mac'}, type: ErrorTypes.conflict}; - } - - if (nodeSecrets.monitoringToken && isDuplicate({ monitoringToken: nodeSecrets.monitoringToken }, token)) { - return {data: {msg: 'Already exists.', field: 'monitoringToken'}, type: ErrorTypes.conflict}; - } - - return null; } - function toNodeFilename(token, node, nodeSecrets) { - return config.server.peersPath + '/' + - ( - (node.hostname || '') + '@' + - (node.mac || '') + '@' + - (node.key || '') + '@' + - (token || '') + '@' + - (nodeSecrets.monitoringToken || '') - ).toLowerCase(); + if (isDuplicate({ mac: node.mac }, token)) { + return {data: {msg: 'Already exists.', field: 'mac'}, type: ErrorTypes.conflict}; } - function writeNodeFile(isUpdate, token, node, nodeSecrets, callback) { - var filename = toNodeFilename(token, node, nodeSecrets); - var data = ''; - _.each(linePrefixes, function (prefix, key) { - var value; - switch (key) { - case 'monitoring': - if (node.monitoring && node.monitoringConfirmed) { - value = 'aktiv'; - } else if (node.monitoring && !node.monitoringConfirmed) { - value = 'pending'; - } else { - value = ''; - } - break; + if (nodeSecrets.monitoringToken && isDuplicate({ monitoringToken: nodeSecrets.monitoringToken }, token)) { + return {data: {msg: 'Already exists.', field: 'monitoringToken'}, type: ErrorTypes.conflict}; + } - case 'monitoringToken': - value = nodeSecrets.monitoringToken || ''; - break; + return null; +} - default: - value = key === 'token' ? token : node[key]; - if (_.isUndefined(value)) { - value = _.isUndefined(nodeSecrets[key]) ? '' : nodeSecrets[key]; - } - break; - } - data += prefix + value + '\n'; - }); - if (node.key) { - data += 'key "' + node.key + '";\n'; +function toNodeFilename(token, node, nodeSecrets) { + return config.server.peersPath + '/' + + ( + (node.hostname || '') + '@' + + (node.mac || '') + '@' + + (node.key || '') + '@' + + (token || '') + '@' + + (nodeSecrets.monitoringToken || '') + ).toLowerCase(); +} + +function writeNodeFile(isUpdate, token, node, nodeSecrets, callback) { + const filename = toNodeFilename(token, node, nodeSecrets); + let data = ''; + _.each(linePrefixes, function (prefix, key) { + let value; + switch (key) { + case 'monitoring': + if (node.monitoring && node.monitoringConfirmed) { + value = 'aktiv'; + } else if (node.monitoring && !node.monitoringConfirmed) { + value = 'pending'; + } else { + value = ''; + } + break; + + case 'monitoringToken': + value = nodeSecrets.monitoringToken || ''; + break; + + default: + value = key === 'token' ? token : node[key]; + if (_.isUndefined(value)) { + value = _.isUndefined(nodeSecrets[key]) ? '' : nodeSecrets[key]; + } + break; + } + data += prefix + value + '\n'; + }); + if (node.key) { + data += 'key "' + node.key + '";\n'; + } + + // since node.js is single threaded we don't need a lock + + let error; + + if (isUpdate) { + const files = findNodeFilesSync({ token: token }); + if (files.length !== 1) { + return callback({data: 'Node not found.', type: ErrorTypes.notFound}); } - // since node.js is single threaded we don't need a lock + error = checkNoDuplicates(token, node, nodeSecrets); + if (error) { + return callback(error); + } - var error; + const file = files[0]; + try { + fs.unlinkSync(file); + } + catch (error) { + Logger.tag('node', 'save').error('Could not delete old node file: ' + file, error); + return callback({data: 'Could not remove old node data.', type: ErrorTypes.internalError}); + } + } else { + error = checkNoDuplicates(null, node, nodeSecrets); + if (error) { + return callback(error); + } + } - if (isUpdate) { - var files = findNodeFilesSync({ token: token }); - if (files.length !== 1) { - return callback({data: 'Node not found.', type: ErrorTypes.notFound}); - } + try { + fs.writeFileSync(filename, data, 'utf8'); + } + catch (error) { + Logger.tag('node', 'save').error('Could not write node file: ' + filename, error); + return callback({data: 'Could not write node data.', type: ErrorTypes.internalError}); + } - error = checkNoDuplicates(token, node, nodeSecrets); - if (error) { - return callback(error); - } + return callback(null, token, node); +} - var file = files[0]; - try { - fs.unlinkSync(file); - } - catch (error) { - Logger.tag('node', 'save').error('Could not delete old node file: ' + file, error); - return callback({data: 'Could not remove old node data.', type: ErrorTypes.internalError}); - } - } else { - error = checkNoDuplicates(null, node, nodeSecrets); - if (error) { - return callback(error); - } +function deleteNodeFile(token, callback) { + findNodeFiles({ token: token }, function (err, files) { + if (err) { + Logger.tag('node', 'delete').error('Could not find node file: ' + files, err); + return callback({data: 'Could not delete node.', type: ErrorTypes.internalError}); + } + + if (files.length !== 1) { + return callback({data: 'Node not found.', type: ErrorTypes.notFound}); } try { - fs.writeFileSync(filename, data, 'utf8'); + fs.unlinkSync(files[0]); } catch (error) { - Logger.tag('node', 'save').error('Could not write node file: ' + filename, error); - return callback({data: 'Could not write node data.', type: ErrorTypes.internalError}); + Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error); + return callback({data: 'Could not delete node.', type: ErrorTypes.internalError}); } - return callback(null, token, node); - } + return callback(null); + }); +} - function deleteNodeFile(token, callback) { - findNodeFiles({ token: token }, function (err, files) { - if (err) { - Logger.tag('node', 'delete').error('Could not find node file: ' + files, err); - return callback({data: 'Could not delete node.', type: ErrorTypes.internalError}); - } +function parseNodeFile(file, callback) { + fs.readFile(file, function (err, contents) { + if (err) { + return callback(err); + } - if (files.length !== 1) { - return callback({data: 'Node not found.', type: ErrorTypes.notFound}); - } + const lines = contents.toString(); - try { - fs.unlinkSync(files[0]); - } - catch (error) { - Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error); - return callback({data: 'Could not delete node.', type: ErrorTypes.internalError}); - } + const node = {}; + const nodeSecrets = {}; - return callback(null); - }); - } + _.each(lines.split('\n'), function (line) { + const entries = {}; - function parseNodeFile(file, callback) { - fs.readFile(file, function (err, contents) { - if (err) { - return callback(err); - } - - var lines = contents.toString(); - - var node = {}; - var nodeSecrets = {}; - - _.each(lines.split('\n'), function (line) { - var entries = {}; - - for (var key in linePrefixes) { - if (linePrefixes.hasOwnProperty(key)) { - var prefix = linePrefixes[key]; - if (line.substring(0, prefix.length) === prefix) { - entries[key] = Strings.normalizeString(line.substr(prefix.length)); - break; - } + for (const key in linePrefixes) { + if (linePrefixes.hasOwnProperty(key)) { + const prefix = linePrefixes[key]; + if (line.substring(0, prefix.length) === prefix) { + entries[key] = Strings.normalizeString(line.substr(prefix.length)); + break; } } + } - if (_.isEmpty(entries) && line.substring(0, 5) === 'key "') { - entries.key = Strings.normalizeString(line.split('"')[1]); + if (_.isEmpty(entries) && line.substring(0, 5) === 'key "') { + entries.key = Strings.normalizeString(line.split('"')[1]); + } + + _.each(entries, function (value, key) { + if (key === 'mac') { + node.mac = value; + node.mapId = _.toLower(value).replace(/:/g, ''); + } else if (key === 'monitoring') { + const active = value === 'aktiv'; + const pending = value === 'pending'; + node.monitoring = active || pending; + node.monitoringConfirmed = active; + node.monitoringState = active ? 'active' : (pending ? 'pending' : 'disabled'); + } else if (key === 'monitoringToken') { + nodeSecrets.monitoringToken = value; + } else { + node[key] = value; } - - _.each(entries, function (value, key) { - if (key === 'mac') { - node.mac = value; - node.mapId = _.toLower(value).replace(/:/g, ''); - } else if (key === 'monitoring') { - var active = value === 'aktiv'; - var pending = value === 'pending'; - node.monitoring = active || pending; - node.monitoringConfirmed = active; - node.monitoringState = active ? 'active' : (pending ? 'pending' : 'disabled'); - } else if (key === 'monitoringToken') { - nodeSecrets.monitoringToken = value; - } else { - node[key] = value; - } - }); }); - - callback(null, node, nodeSecrets); }); - } - function findNodeDataByFilePattern(filter, callback) { - findNodeFiles(filter, function (err, files) { + callback(null, node, nodeSecrets); + }); +} + +function findNodeDataByFilePattern(filter, callback) { + findNodeFiles(filter, function (err, files) { + if (err) { + return callback(err); + } + + if (files.length !== 1) { + return callback(null); + } + + const file = files[0]; + return parseNodeFile(file, callback); + }); +} + +function getNodeDataByFilePattern(filter, callback) { + findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) { + if (err) { + return callback(err); + } + + if (!node) { + return callback({data: 'Node not found.', type: ErrorTypes.notFound}); + } + + callback(null, node, nodeSecrets); + }); +} + +function sendMonitoringConfirmationMail(node, nodeSecrets, callback) { + const confirmUrl = UrlBuilder.monitoringConfirmUrl(nodeSecrets); + const disableUrl = UrlBuilder.monitoringDisableUrl(nodeSecrets); + + MailService.enqueue( + config.server.email.from, + node.nickname + ' <' + node.email + '>', + 'monitoring-confirmation', + { + node: node, + confirmUrl: confirmUrl, + disableUrl: disableUrl + }, + function (err) { + if (err) { + Logger.tag('monitoring', 'confirmation').error('Could not enqueue confirmation mail.', err); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + } + + callback(null); + } + ); +} + +module.exports = { + createNode: function (node, callback) { + const token = generateToken(); + const nodeSecrets = {}; + + node.monitoringConfirmed = false; + + if (node.monitoring) { + nodeSecrets.monitoringToken = generateToken(); + } + + writeNodeFile(false, token, node, nodeSecrets, function (err, token, node) { if (err) { return callback(err); } - if (files.length !== 1) { - return callback(null); + if (node.monitoring && !node.monitoringConfirmed) { + return sendMonitoringConfirmationMail(node, nodeSecrets, function (err) { + if (err) { + return callback(err); + } + + return callback(null, token, node); + }); } - var file = files[0]; - return parseNodeFile(file, callback); + return callback(null, token, node); }); - } + }, - function getNodeDataByFilePattern(filter, callback) { - findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) { + updateNode: function (token, node, callback) { + this.getNodeDataByToken(token, function (err, currentNode, nodeSecrets) { if (err) { return callback(err); } - if (!node) { - return callback({data: 'Node not found.', type: ErrorTypes.notFound}); - } - - callback(null, node, nodeSecrets); - }); - } - - function sendMonitoringConfirmationMail(node, nodeSecrets, callback) { - var confirmUrl = UrlBuilder.monitoringConfirmUrl(nodeSecrets); - var disableUrl = UrlBuilder.monitoringDisableUrl(nodeSecrets); - - MailService.enqueue( - config.server.email.from, - node.nickname + ' <' + node.email + '>', - 'monitoring-confirmation', - { - node: node, - confirmUrl: confirmUrl, - disableUrl: disableUrl - }, - function (err) { - if (err) { - Logger.tag('monitoring', 'confirmation').error('Could not enqueue confirmation mail.', err); - return callback({data: 'Internal error.', type: ErrorTypes.internalError}); - } - - callback(null); - } - ); - } - - return { - createNode: function (node, callback) { - var token = generateToken(); - var nodeSecrets = {}; - - node.monitoringConfirmed = false; + let monitoringConfirmed = false; + let monitoringToken = ''; if (node.monitoring) { - nodeSecrets.monitoringToken = generateToken(); + if (!currentNode.monitoring) { + // monitoring just has been enabled + monitoringConfirmed = false; + monitoringToken = generateToken(); + + } else { + // monitoring is still enabled + + if (currentNode.email !== node.email) { + // new email so we need a new token and a reconfirmation + monitoringConfirmed = false; + monitoringToken = generateToken(); + + } else { + // email unchanged, keep token (fix if not set) and confirmation state + monitoringConfirmed = currentNode.monitoringConfirmed; + monitoringToken = nodeSecrets.monitoringToken || generateToken(); + } + } } - writeNodeFile(false, token, node, nodeSecrets, function (err, token, node) { + node.monitoringConfirmed = monitoringConfirmed; + nodeSecrets.monitoringToken = monitoringToken; + + writeNodeFile(true, token, node, nodeSecrets, function (err, token, node) { if (err) { return callback(err); } @@ -357,178 +409,124 @@ angular.module('ffffng') return callback(null, token, node); }); - }, + }); + }, - updateNode: function (token, node, callback) { - this.getNodeDataByToken(token, function (err, currentNode, nodeSecrets) { - if (err) { - return callback(err); - } + internalUpdateNode: function (token, node, nodeSecrets, callback) { + writeNodeFile(true, token, node, nodeSecrets, callback); + }, - var monitoringConfirmed = false; - var monitoringToken = ''; + deleteNode: function (token, callback) { + deleteNodeFile(token, callback); + }, - if (node.monitoring) { - if (!currentNode.monitoring) { - // monitoring just has been enabled - monitoringConfirmed = false; - monitoringToken = generateToken(); + getAllNodes: function (callback) { + findNodeFiles({}, function (err, files) { + if (err) { + Logger.tag('nodes').error('Error getting all nodes:', err); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + } - } else { - // monitoring is still enabled - - if (currentNode.email !== node.email) { - // new email so we need a new token and a reconfirmation - monitoringConfirmed = false; - monitoringToken = generateToken(); - - } else { - // email unchanged, keep token (fix if not set) and confirmation state - monitoringConfirmed = currentNode.monitoringConfirmed; - monitoringToken = nodeSecrets.monitoringToken || generateToken(); - } - } - } - - node.monitoringConfirmed = monitoringConfirmed; - nodeSecrets.monitoringToken = monitoringToken; - - writeNodeFile(true, token, node, nodeSecrets, function (err, token, node) { + async.mapLimit( + files, + MAX_PARALLEL_NODES_PARSING, + parseNodeFile, + function (err, nodes) { if (err) { - return callback(err); + Logger.tag('nodes').error('Error getting all nodes:', err); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); } - if (node.monitoring && !node.monitoringConfirmed) { - return sendMonitoringConfirmationMail(node, nodeSecrets, function (err) { - if (err) { - return callback(err); - } - - return callback(null, token, node); - }); - } - - return callback(null, token, node); - }); - }); - }, - - internalUpdateNode: function (token, node, nodeSecrets, callback) { - writeNodeFile(true, token, node, nodeSecrets, callback); - }, - - deleteNode: function (token, callback) { - deleteNodeFile(token, callback); - }, - - getAllNodes: function (callback) { - findNodeFiles({}, function (err, files) { - if (err) { - Logger.tag('nodes').error('Error getting all nodes:', err); - return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + return callback(null, nodes); } + ); + }); + }, - async.mapLimit( - files, - MAX_PARALLEL_NODES_PARSING, - parseNodeFile, - function (err, nodes) { + getNodeDataByMac: function (mac, callback) { + return findNodeDataByFilePattern({ mac: mac }, callback); + }, + + getNodeDataByToken: function (token, callback) { + return getNodeDataByFilePattern({ token: token }, callback); + }, + + getNodeDataByMonitoringToken: function (monitoringToken, callback) { + return getNodeDataByFilePattern({ monitoringToken: monitoringToken }, callback); + }, + + fixNodeFilenames: function (callback) { + findFilesInPeersPath(function (err, files) { + if (err) { + return callback(err); + } + + async.mapLimit( + files, + MAX_PARALLEL_NODES_PARSING, + function (file, fileCallback) { + parseNodeFile(file, function (err, node, nodeSecrets) { if (err) { - Logger.tag('nodes').error('Error getting all nodes:', err); - return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + return fileCallback(err); } - return callback(null, nodes); - } - ); - }); - }, + const expectedFilename = toNodeFilename(node.token, node, nodeSecrets); + if (file !== expectedFilename) { + return fs.rename(file, expectedFilename, function (err) { + if (err) { + return fileCallback(new Error( + 'Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + err + )); + } - getNodeDataByMac: function (mac, callback) { - return findNodeDataByFilePattern({ mac: mac }, callback); - }, + fileCallback(null); + }); + } - getNodeDataByToken: function (token, callback) { - return getNodeDataByFilePattern({ token: token }, callback); - }, + fileCallback(null); + }); + }, + callback + ); + }); + }, - getNodeDataByMonitoringToken: function (monitoringToken, callback) { - return getNodeDataByFilePattern({ monitoringToken: monitoringToken }, callback); - }, + getNodeStatistics: function (callback) { + this.getAllNodes(function (err, nodes) { + if (err) { + return callback(err); + } - fixNodeFilenames: function (callback) { - findFilesInPeersPath(function (err, files) { - if (err) { - return callback(err); + const nodeStatistics = { + registered: _.size(nodes), + withVPN: 0, + withCoords: 0, + monitoring: { + active: 0, + pending: 0 + } + }; + + _.each(nodes, function (node) { + if (node.key) { + nodeStatistics.withVPN += 1; } - async.mapLimit( - files, - MAX_PARALLEL_NODES_PARSING, - function (file, fileCallback) { - parseNodeFile(file, function (err, node, nodeSecrets) { - if (err) { - return fileCallback(err); - } - - var expectedFilename = toNodeFilename(node.token, node, nodeSecrets); - if (file !== expectedFilename) { - return fs.rename(file, expectedFilename, function (err) { - if (err) { - return fileCallback(new Error( - 'Cannot rename file ' + file + ' to ' + expectedFilename + ' => ' + err - )); - } - - fileCallback(null); - }); - } - - fileCallback(null); - }); - }, - callback - ); - }); - }, - - getNodeStatistics: function (callback) { - this.getAllNodes(function (err, nodes) { - if (err) { - return callback(err); + if (node.coords) { + nodeStatistics.withCoords += 1; } - var nodeStatistics = { - registered: _.size(nodes), - withVPN: 0, - withCoords: 0, - monitoring: { - active: 0, - pending: 0 - } - }; - - _.each(nodes, function (node) { - if (node.key) { - nodeStatistics.withVPN += 1; - } - - if (node.coords) { - nodeStatistics.withCoords += 1; - } - - switch (node.monitoringState) { - case 'active': - nodeStatistics.monitoring.active += 1; - break; - case 'pending': - nodeStatistics.monitoring.pending += 1; - break; - } - }); - - callback(null, nodeStatistics); + switch (node.monitoringState) { + case 'active': + nodeStatistics.monitoring.active += 1; + break; + case 'pending': + nodeStatistics.monitoring.pending += 1; + break; + } }); - } - }; -}); + + callback(null, nodeStatistics); + }); + } +} diff --git a/server/utils/databaseUtil.js b/server/utils/databaseUtil.js index 5f1ea96..10b4bbc 100644 --- a/server/utils/databaseUtil.js +++ b/server/utils/databaseUtil.js @@ -1,12 +1,12 @@ 'use strict'; -angular.module('ffffng').factory('DatabaseUtil', function (_) { - return { - inCondition: function (field, list) { - return { - query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))', - params: list - }; +const _ = require('lodash') + +module.exports = { + inCondition (field, list) { + return { + query: '(' + field + ' IN (' + _.join(_.times(list.length, _.constant('?')), ', ') + '))', + params: list } - }; -}); + } +} diff --git a/server/utils/errorTypes.js b/server/utils/errorTypes.js index 7c544aa..f93e5e4 100644 --- a/server/utils/errorTypes.js +++ b/server/utils/errorTypes.js @@ -1,10 +1,8 @@ 'use strict'; -angular.module('ffffng').factory('ErrorTypes', function () { - return { - badRequest: {code: 400}, - notFound: {code: 404}, - conflict: {code: 409}, - internalError: {code: 500} - }; -}); +module.exports = { + badRequest: {code: 400}, + notFound: {code: 404}, + conflict: {code: 409}, + internalError: {code: 500} +} diff --git a/server/utils/resources.js b/server/utils/resources.js index be125e9..7b0e82a 100644 --- a/server/utils/resources.js +++ b/server/utils/resources.js @@ -1,224 +1,229 @@ 'use strict'; -angular.module('ffffng').factory('Resources', function (_, Constraints, Validator, ErrorTypes, Logger) { - function respond(res, httpCode, data, type) { - switch (type) { - case 'html': - res.writeHead(httpCode, {'Content-Type': 'text/html'}); - res.end(data); - break; +const _ = require('lodash') - default: - res.writeHead(httpCode, {'Content-Type': 'application/json'}); - res.end(JSON.stringify(data)); - break; - } +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) { + case 'html': + res.writeHead(httpCode, {'Content-Type': 'text/html'}); + res.end(data); + break; + + default: + res.writeHead(httpCode, {'Content-Type': 'application/json'}); + res.end(JSON.stringify(data)); + break; + } +} + +function orderByClause(restParams, defaultSortField, allowedSortFields) { + let sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; + if (!sortField) { + sortField = defaultSortField; } - function orderByClause(restParams, defaultSortField, allowedSortFields) { - var sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; - if (!sortField) { - sortField = defaultSortField; - } + return { + query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'), + params: [] + }; +} +function limitOffsetClause(restParams) { + const page = restParams._page; + const perPage = restParams._perPage; + + return { + query: 'LIMIT ? OFFSET ?', + params: [perPage, ((page - 1) * perPage)] + }; +} + +function escapeForLikePattern(str) { + return str + .replace(/\\/g, '\\\\') + .replace(/%/g, '\\%') + .replace(/_/g, '\\_'); +} + +function filterCondition(restParams, filterFields) { + if (_.isEmpty(filterFields)) { return { - query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'), + query: '1 = 1', params: [] }; } - function limitOffsetClause(restParams) { - var page = restParams._page; - var perPage = restParams._perPage; + let query = _.join( + _.map(filterFields, function (field) { + return 'LOWER(' + field + ') LIKE ?'; + }), + ' OR ' + ); - return { - query: 'LIMIT ? OFFSET ?', - params: [perPage, ((page - 1) * perPage)] - }; - } + query += ' ESCAPE \'\\\''; - function escapeForLikePattern(str) { - return str - .replace(/\\/g, '\\\\') - .replace(/%/g, '\\%') - .replace(/_/g, '\\_'); - } - - function filterCondition(restParams, filterFields) { - if (_.isEmpty(filterFields)) { - return { - query: '1 = 1', - params: [] - }; - } - - var query = _.join( - _.map(filterFields, function (field) { - return 'LOWER(' + field + ') LIKE ?'; - }), - ' OR ' - ); - - query += ' ESCAPE \'\\\''; - - var search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%'; - var params = _.times(filterFields.length, _.constant(search)); - - return { - query: query, - params: params - }; - } - - function getConstrainedValues(data, constraints) { - var values = {}; - _.each(_.keys(constraints), function (key) { - var value = data[key]; - values[key] = - _.isUndefined(value) && !_.isUndefined(constraints[key].default) ? constraints[key].default : value; - }); - return values; - } + const search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%'; + const params = _.times(filterFields.length, _.constant(search)); return { - getData: function (req) { - return _.extend({}, req.body, req.params, req.query); - }, + query: query, + params: params + }; +} - getValidRestParams: function(type, subtype, req, callback) { - var constraints = Constraints.rest[type]; - if (!_.isPlainObject(constraints)) { - Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type); +function getConstrainedValues(data, constraints) { + const values = {}; + _.each(_.keys(constraints), function (key) { + const value = data[key]; + values[key] = + _.isUndefined(value) && !_.isUndefined(constraints[key].default) ? constraints[key].default : value; + }); + return values; +} + +module.exports = { + getData (req) { + return _.extend({}, req.body, req.params, req.query); + }, + + getValidRestParams(type, subtype, req, callback) { + const constraints = Constraints.rest[type]; + if (!_.isPlainObject(constraints)) { + Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type); + return callback({data: 'Internal error.', type: ErrorTypes.internalError}); + } + + let filterConstraints = {}; + if (subtype) { + filterConstraints = Constraints[subtype + 'Filters']; + if (!_.isPlainObject(filterConstraints)) { + Logger.tag('validation', 'rest').error('Unknown REST resource subtype: {}', subtype); return callback({data: 'Internal error.', type: ErrorTypes.internalError}); } - - var filterConstraints = {}; - if (subtype) { - filterConstraints = Constraints[subtype + 'Filters']; - if (!_.isPlainObject(filterConstraints)) { - Logger.tag('validation', 'rest').error('Unknown REST resource subtype: {}', subtype); - return callback({data: 'Internal error.', type: ErrorTypes.internalError}); - } - } - - var data = this.getData(req); - - var restParams = getConstrainedValues(data, constraints); - var filterParams = getConstrainedValues(data, filterConstraints); - - var areValidParams = Validator.forConstraints(constraints); - var areValidFilters = Validator.forConstraints(filterConstraints); - if (!areValidParams(restParams) || !areValidFilters(filterParams)) { - return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest}); - } - - restParams.filters = filterParams; - - callback(null, restParams); - }, - - filter: function (entities, allowedFilterFields, restParams) { - var query = restParams.q; - if (query) { - query = _.toLower(query.trim()); - } - - function queryMatches(entity) { - if (!query) { - return true; - } - return _.some(allowedFilterFields, function (field) { - var value = entity[field]; - if (_.isNumber(value)) { - value = value.toString(); - } - - if (!_.isString(value) || _.isEmpty(value)) { - return false; - } - - value = _.toLower(value); - if (field === 'mac') { - return _.includes(value.replace(/:/g, ''), query.replace(/:/g, '')); - } - - return _.includes(value, query); - }); - } - - var filters = restParams.filters; - - function filtersMatch(entity) { - if (_.isEmpty(filters)) { - return true; - } - - return _.every(filters, function (value, key) { - if (_.isUndefined(value)) { - return true; - } - if (_.startsWith(key, 'has')) { - var entityKey = key.substr(3, 1).toLowerCase() + key.substr(4); - return _.isEmpty(entity[entityKey]).toString() !== value; - } - return entity[key] === value; - }); - } - - return _.filter(entities, function (entity) { - return queryMatches(entity) && filtersMatch(entity); - }); - }, - - sort: function (entities, allowedSortFields, restParams) { - var sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; - if (!sortField) { - return entities; - } - - var sorted = _.sortBy(entities, [sortField]); - - return restParams._sortDir === 'ASC' ? sorted : _.reverse(sorted); - }, - - getPageEntities: function (entities, restParams) { - var page = restParams._page; - var perPage = restParams._perPage; - - return entities.slice((page - 1) * perPage, page * perPage); - }, - - whereCondition: filterCondition, - - filterClause: function (restParams, defaultSortField, allowedSortFields, filterFields) { - var orderBy = orderByClause( - restParams, - defaultSortField, - allowedSortFields - ); - var limitOffset = limitOffsetClause(restParams); - - var filter = filterCondition( - restParams, - filterFields - ); - - return { - query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query, - params: _.concat(filter.params, orderBy.params, limitOffset.params) - }; - }, - - success: function (res, data) { - respond(res, 200, data, 'json'); - }, - - successHtml: function (res, html) { - respond(res, 200, html, 'html'); - }, - - error: function (res, err) { - respond(res, err.type.code, err.data, 'json'); } - }; -}); + + const data = this.getData(req); + + const restParams = getConstrainedValues(data, constraints); + const filterParams = getConstrainedValues(data, filterConstraints); + + const areValidParams = Validator.forConstraints(constraints); + const areValidFilters = Validator.forConstraints(filterConstraints); + if (!areValidParams(restParams) || !areValidFilters(filterParams)) { + return callback({data: 'Invalid REST parameters.', type: ErrorTypes.badRequest}); + } + + restParams.filters = filterParams; + + callback(null, restParams); + }, + + filter (entities, allowedFilterFields, restParams) { + let query = restParams.q; + if (query) { + query = _.toLower(query.trim()); + } + + function queryMatches(entity) { + if (!query) { + return true; + } + return _.some(allowedFilterFields, function (field) { + let value = entity[field]; + if (_.isNumber(value)) { + value = value.toString(); + } + + if (!_.isString(value) || _.isEmpty(value)) { + return false; + } + + value = _.toLower(value); + if (field === 'mac') { + return _.includes(value.replace(/:/g, ''), query.replace(/:/g, '')); + } + + return _.includes(value, query); + }); + } + + const filters = restParams.filters; + + function filtersMatch(entity) { + if (_.isEmpty(filters)) { + return true; + } + + return _.every(filters, function (value, key) { + if (_.isUndefined(value)) { + return true; + } + if (_.startsWith(key, 'has')) { + const entityKey = key.substr(3, 1).toLowerCase() + key.substr(4); + return _.isEmpty(entity[entityKey]).toString() !== value; + } + return entity[key] === value; + }); + } + + return _.filter(entities, function (entity) { + return queryMatches(entity) && filtersMatch(entity); + }); + }, + + sort (entities, allowedSortFields, restParams) { + const sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; + if (!sortField) { + return entities; + } + + const sorted = _.sortBy(entities, [sortField]); + + return restParams._sortDir === 'ASC' ? sorted : _.reverse(sorted); + }, + + getPageEntities (entities, restParams) { + const page = restParams._page; + const perPage = restParams._perPage; + + return entities.slice((page - 1) * perPage, page * perPage); + }, + + whereCondition: filterCondition, + + filterClause (restParams, defaultSortField, allowedSortFields, filterFields) { + const orderBy = orderByClause( + restParams, + defaultSortField, + allowedSortFields + ); + const limitOffset = limitOffsetClause(restParams); + + const filter = filterCondition( + restParams, + filterFields + ); + + return { + query: filter.query + ' ' + orderBy.query + ' ' + limitOffset.query, + params: _.concat(filter.params, orderBy.params, limitOffset.params) + }; + }, + + success (res, data) { + respond(res, 200, data, 'json'); + }, + + successHtml (res, html) { + respond(res, 200, html, 'html'); + }, + + error (res, err) { + respond(res, err.type.code, err.data, 'json'); + } +} diff --git a/server/utils/strings.js b/server/utils/strings.js index dcf3b24..e48f612 100644 --- a/server/utils/strings.js +++ b/server/utils/strings.js @@ -1,27 +1,27 @@ 'use strict'; -angular.module('ffffng').factory('Strings', function (_) { - return { - normalizeString: function (str) { - return _.isString(str) ? str.trim().replace(/\s+/g, ' ') : str; - }, +const _ = require('lodash') - normalizeMac: function (mac) { - // parts only contains values at odd indexes - var parts = mac.toUpperCase().replace(/:/g, '').split(/([A-F0-9]{2})/); +module.exports = { + normalizeString (str) { + return _.isString(str) ? str.trim().replace(/\s+/g, ' ') : str; + }, - var macParts = []; + normalizeMac (mac) { + // parts only contains values at odd indexes + const parts = mac.toUpperCase().replace(/:/g, '').split(/([A-F0-9]{2})/); - for (var i = 1; i < parts.length; i += 2) { - macParts.push(parts[i]); - } + const macParts = []; - return macParts.join(':'); - }, - - parseInt: function (str) { - var parsed = _.parseInt(str, 10); - return parsed.toString() === str ? parsed : undefined; + for (let i = 1; i < parts.length; i += 2) { + macParts.push(parts[i]); } - }; -}); + + return macParts.join(':'); + }, + + parseInt (str) { + const parsed = _.parseInt(str, 10); + return parsed.toString() === str ? parsed : undefined; + } +} diff --git a/server/utils/urlBuilder.js b/server/utils/urlBuilder.js index ac8a8a5..04bba70 100644 --- a/server/utils/urlBuilder.js +++ b/server/utils/urlBuilder.js @@ -1,39 +1,41 @@ 'use strict'; -angular.module('ffffng').factory('UrlBuilder', function (_, config) { - function formUrl(route, queryParams) { - var url = config.server.baseUrl; - if (route || queryParams) { - url += '/#/'; - } - if (route) { - url += route; - } - if (queryParams) { - url += '?'; - url += _.join( - _.map( - queryParams, - function (value, key) { - return encodeURIComponent(key) + '=' + encodeURIComponent(value); - } - ), - '&' - ); - } - return url; +const _ = require('lodash') + +const config = require('../config').config + +function formUrl(route, queryParams) { + let url = config.server.baseUrl; + if (route || queryParams) { + url += '/#/'; } + if (route) { + url += route; + } + if (queryParams) { + url += '?'; + url += _.join( + _.map( + queryParams, + function (value, key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(value); + } + ), + '&' + ); + } + return url; +} - return { - editNodeUrl: function () { - return formUrl('update'); - }, +module.exports = { + editNodeUrl () { + return formUrl('update'); + }, - monitoringConfirmUrl: function (nodeSecrets) { - return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken }); - }, - monitoringDisableUrl: function (nodeSecrets) { - return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken }); - } - }; -}); + monitoringConfirmUrl (nodeSecrets) { + return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken }); + }, + monitoringDisableUrl (nodeSecrets) { + return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken }); + } +} diff --git a/server/validation/validator.js b/server/validation/validator.js index 92ce7a8..92b19cd 100644 --- a/server/validation/validator.js +++ b/server/validation/validator.js @@ -1,93 +1,96 @@ 'use strict'; -angular.module('ffffng').factory('Validator', function (_, Strings, Logger) { - // TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.) +const _ = require('lodash') - function isValidBoolean(value) { - return _.isBoolean(value) || value === 'true' || value === 'false'; +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'; +} + +function isValidNumber(constraint, value) { + if (_.isString(value)) { + value = Strings.parseInt(value); } - function isValidNumber(constraint, value) { - if (_.isString(value)) { - value = Strings.parseInt(value); - } - - if (!_.isNumber(value)) { - return false; - } - - if (_.isNaN(value) || !_.isFinite(value)) { - return false; - } - - if (_.isNumber(constraint.min) && value < constraint.min) { - return false; - } - - if (_.isNumber(constraint.max) && value > constraint.max) { - return false; - } - - return true; - } - - function isValidEnum(constraint, value) { - if (!_.isString(value)) { - return false; - } - - return _.indexOf(constraint.allowed, value) >= 0; - } - - function isValidString(constraint, value) { - if (!_.isString(value)) { - return false; - } - - var trimmed = value.trim(); - return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex); - } - - function isValid(constraint, acceptUndefined, value) { - if (value === undefined) { - return acceptUndefined || constraint.optional; - } - - switch (constraint.type) { - case 'boolean': - return isValidBoolean(value); - - case 'number': - return isValidNumber(constraint, value); - - case 'enum': - return isValidEnum(constraint, value); - - case 'string': - return isValidString(constraint, value); - } - - Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type); + if (!_.isNumber(value)) { return false; } - function areValid(constraints, acceptUndefined, values) { - var fields = Object.keys(constraints); - for (var i = 0; i < fields.length; i ++) { - var field = fields[i]; - if (!isValid(constraints[field], acceptUndefined, values[field])) { - return false; - } - } - return true; + if (_.isNaN(value) || !_.isFinite(value)) { + return false; } - return { - forConstraint: function (constraint, acceptUndefined) { - return _.partial(isValid, constraint, acceptUndefined); - }, - forConstraints: function (constraints, acceptUndefined) { - return _.partial(areValid, constraints, acceptUndefined); + if (_.isNumber(constraint.min) && value < constraint.min) { + return false; + } + + if (_.isNumber(constraint.max) && value > constraint.max) { + return false; + } + + return true; +} + +function isValidEnum(constraint, value) { + if (!_.isString(value)) { + return false; + } + + return _.indexOf(constraint.allowed, value) >= 0; +} + +function isValidString(constraint, value) { + if (!_.isString(value)) { + return false; + } + + const trimmed = value.trim(); + return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex); +} + +function isValid(constraint, acceptUndefined, value) { + if (value === undefined) { + return acceptUndefined || constraint.optional; + } + + switch (constraint.type) { + case 'boolean': + return isValidBoolean(value); + + case 'number': + return isValidNumber(constraint, value); + + case 'enum': + return isValidEnum(constraint, value); + + case 'string': + return isValidString(constraint, value); + } + + Logger.tag('validation').error('No validation method for constraint type: {}', constraint.type); + return false; +} + +function areValid(constraints, acceptUndefined, values) { + const fields = Object.keys(constraints); + for (let i = 0; i < fields.length; i ++) { + const field = fields[i]; + if (!isValid(constraints[field], acceptUndefined, values[field])) { + return false; } - }; -}); + } + return true; +} + +module.exports = { + forConstraint (constraint, acceptUndefined) { + return _.partial(isValid, constraint, acceptUndefined); + }, + forConstraints (constraints, acceptUndefined) { + return _.partial(areValid, constraints, acceptUndefined); + } +} diff --git a/shared/validation/constraints.js b/shared/validation/constraints.js index f67a111..ff2ccec 100644 --- a/shared/validation/constraints.js +++ b/shared/validation/constraints.js @@ -1,113 +1,139 @@ 'use strict'; -angular.module('ffffng').constant('Constraints', { - id:{ - type: 'string', - regex: /^[1-9][0-9]*/, - optional: false - }, - token:{ - type: 'string', - regex: /^[0-9a-f]{16}$/i, - optional: false - }, - node: { - hostname: { +(function () { + var constraints = { + id:{ type: 'string', - regex: /^[-a-z0-9_]{1,32}$/i, + regex: /^[1-9][0-9]*/, optional: false }, - key: { + token:{ type: 'string', - regex: /^([a-f0-9]{64})$/i, - optional: true - }, - email: { - type: 'string', - regex: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, + regex: /^[0-9a-f]{16}$/i, optional: false }, - nickname: { - type: 'string', - regex: /^[-a-z0-9_ äöüß]{1,64}$/i, - optional: false - }, - mac: { - type: 'string', - regex: /^([a-f0-9]{12}|([a-f0-9]{2}:){5}[a-f0-9]{2})$/i, - optional: false - }, - coords: { - type: 'string', - regex: /^(-?[0-9]{1,3}(\.[0-9]{1,15})? -?[0-9]{1,3}(\.[0-9]{1,15})?)$/, - optional: true - }, - monitoring: { - type: 'boolean', - optional: false - } - }, - nodeFilters: { - hasKey: { - type: 'boolean', - optional: true - }, - hasCoords: { - type: 'boolean', - optional: true - }, - onlineState: { - type: 'string', - regex: /^(ONLINE|OFFLINE)$/, - optional: true - }, - monitoringState: { - type: 'string', - regex: /^(disabled|active|pending)$/, - optional: true - }, - site: { - type: 'string', - regex: /^[a-z0-9_-]{1,32}$/, - optional: true - }, - domain: { - type: 'string', - regex: /^[a-z0-9_-]{1,32}$/, - optional: true - } - }, - rest: { - list: { - _page: { - type: 'number', - min: 1, - optional: true, - default: 1 - }, - _perPage: { - type: 'number', - min: 1, - max: 50, - optional: true, - default: 20 - }, - _sortDir: { - type: 'enum', - allowed: ['ASC', 'DESC'], - optional: true, - default: 'ASC' - }, - _sortField: { + node: { + hostname: { type: 'string', - regex: /^[a-zA-Z0-9_]{1,32}$/, + regex: /^[-a-z0-9_]{1,32}$/i, + optional: false + }, + key: { + type: 'string', + regex: /^([a-f0-9]{64})$/i, optional: true }, - q: { + email: { type: 'string', - regex: /^[äöüß a-z0-9!#$%&@:.'*+/=?^_`{|}~-]{1,64}$/i, + regex: /^[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i, + optional: false + }, + nickname: { + type: 'string', + regex: /^[-a-z0-9_ äöüß]{1,64}$/i, + optional: false + }, + mac: { + type: 'string', + regex: /^([a-f0-9]{12}|([a-f0-9]{2}:){5}[a-f0-9]{2})$/i, + optional: false + }, + coords: { + type: 'string', + regex: /^(-?[0-9]{1,3}(\.[0-9]{1,15})? -?[0-9]{1,3}(\.[0-9]{1,15})?)$/, optional: true + }, + monitoring: { + type: 'boolean', + optional: false + } + }, + nodeFilters: { + hasKey: { + type: 'boolean', + optional: true + }, + hasCoords: { + type: 'boolean', + optional: true + }, + onlineState: { + type: 'string', + regex: /^(ONLINE|OFFLINE)$/, + optional: true + }, + monitoringState: { + type: 'string', + regex: /^(disabled|active|pending)$/, + optional: true + }, + site: { + type: 'string', + regex: /^[a-z0-9_-]{1,32}$/, + optional: true + }, + domain: { + type: 'string', + regex: /^[a-z0-9_-]{1,32}$/, + optional: true + } + }, + rest: { + list: { + _page: { + type: 'number', + min: 1, + optional: true, + default: 1 + }, + _perPage: { + type: 'number', + min: 1, + max: 50, + optional: true, + default: 20 + }, + _sortDir: { + type: 'enum', + allowed: ['ASC', 'DESC'], + optional: true, + default: 'ASC' + }, + _sortField: { + type: 'string', + regex: /^[a-zA-Z0-9_]{1,32}$/, + optional: true + }, + q: { + type: 'string', + regex: /^[äöüß a-z0-9!#$%&@:.'*+/=?^_`{|}~-]{1,64}$/i, + optional: true + } } } } -}); + + 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 + } +})()