Removing ng-di on the server.

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

View file

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

View file

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

View file

@ -1,156 +1,158 @@
'use strict'; 'use strict';
var commandLineArgs = require('command-line-args'); module.exports = (() => {
var commandLineUsage = require('command-line-usage'); const commandLineArgs = require('command-line-args');
const commandLineUsage = require('command-line-usage');
var commandLineDefs = [ const commandLineDefs = [
{ name: 'help', alias: 'h', type: Boolean, description: 'Show this help' }, { name: 'help', alias: 'h', type: Boolean, description: 'Show this help' },
{ name: 'config', alias: 'c', type: String, description: 'Location of config.json' }, { name: 'config', alias: 'c', type: String, description: 'Location of config.json' },
{ name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' } { name: 'version', alias: 'v', type: Boolean, description: 'Show ffffng version' }
]; ];
var commandLineOptions; let commandLineOptions;
try { try {
commandLineOptions = commandLineArgs(commandLineDefs); commandLineOptions = commandLineArgs(commandLineDefs);
} catch (e) { } catch (e) {
console.error(e.message); console.error(e.message);
console.error('Try \'--help\' for more information.'); console.error('Try \'--help\' for more information.');
process.exit(1); process.exit(1);
} }
var fs = require('graceful-fs'); const fs = require('graceful-fs');
var packageJsonFile = __dirname + '/../package.json'; const packageJsonFile = __dirname + '/../package.json';
var version = 'unknown'; let version = 'unknown';
if (fs.existsSync(packageJsonFile)) { if (fs.existsSync(packageJsonFile)) {
version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version; version = JSON.parse(fs.readFileSync(packageJsonFile, 'utf8')).version;
} }
function usage() { function usage () {
console.log(commandLineUsage([ console.log(commandLineUsage([
{ {
header: 'ffffng - ' + version + ' - Freifunk node management form', header: 'ffffng - ' + version + ' - Freifunk node management form',
optionList: commandLineDefs optionList: commandLineDefs
} }
])); ]));
} }
if (commandLineOptions.help) { if (commandLineOptions.help) {
usage(); usage();
process.exit(0); process.exit(0);
} }
if (commandLineOptions.version) { if (commandLineOptions.version) {
console.log('ffffng - ' + version); console.log('ffffng - ' + version);
process.exit(0); process.exit(0);
} }
if (!commandLineOptions.config) { if (!commandLineOptions.config) {
usage(); usage();
process.exit(1); process.exit(1);
} }
var deepExtend = require('deep-extend'); const deepExtend = require('deep-extend');
var defaultConfig = { const defaultConfig = {
server: { server: {
baseUrl: 'http://localhost:8080', baseUrl: 'http://localhost:8080',
port: 8080, port: 8080,
databaseFile: '/tmp/ffffng.sqlite', databaseFile: '/tmp/ffffng.sqlite',
peersPath: '/tmp/peers', peersPath: '/tmp/peers',
logging: { logging: {
directory: '/tmp/logs', directory: '/tmp/logs',
debug: false, debug: false,
profile: false, profile: false,
logRequests: false logRequests: false
}, },
internal: { internal: {
active: false, active: false,
user: 'admin', user: 'admin',
password: 'secret' password: 'secret'
}, },
email: { email: {
from: 'Freifunk Knotenformular <no-reply@musterstadt.freifunk.net>', from: 'Freifunk Knotenformular <no-reply@musterstadt.freifunk.net>',
// For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/ // For details see: https://nodemailer.com/2-0-0-beta/setup-smtp/
smtp: { smtp: {
host: 'mail.example.com', host: 'mail.example.com',
port: '465', port: '465',
secure: true, secure: true,
auth: { auth: {
user: 'user@example.com', user: 'user@example.com',
pass: 'pass' pass: 'pass'
}
} }
},
map: {
nodesJsonUrl: ['http://map.musterstadt.freifunk.net/nodes.json']
} }
}, },
client: {
map: { community: {
nodesJsonUrl: ['http://map.musterstadt.freifunk.net/nodes.json'] 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: { const configJSONFile = commandLineOptions.config;
name: 'Freifunk Musterstadt', let configJSON = {};
domain: 'musterstadt.freifunk.net',
contactEmail: 'kontakt@musterstadt.freifunk.net', if (fs.existsSync(configJSONFile)) {
sites: [], configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8'));
domains: [] } else {
}, console.error('config.json not found: ' + configJSONFile);
legal: { process.exit(1);
privacyUrl: null, }
imprintUrl: null
}, const _ = require('lodash');
map: {
mapUrl: 'http://map.musterstadt.freifunk.net' function stripTrailingSlash (obj, field) {
}, const url = obj[field];
monitoring: { if (_.isString(url) && _.last(url) === '/') {
enabled: false obj[field] = url.substr(0, url.length - 1);
},
coordsSelector: {
showInfo: false,
showBorderForDebugging: false,
localCommunityPolygon: [],
lat: 53.565278,
lng: 10.001389,
defaultZoom: 10,
layers: {}
} }
} }
};
var configJSONFile = commandLineOptions.config; const config = deepExtend({}, defaultConfig, configJSON);
var configJSON = {};
if (fs.existsSync(configJSONFile)) { stripTrailingSlash(config.server, 'baseUrl');
configJSON = JSON.parse(fs.readFileSync(configJSONFile, 'utf8')); stripTrailingSlash(config.client.map, 'mapUrl');
} else {
console.error('config.json not found: ' + configJSONFile);
process.exit(1);
}
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) { return {
var url = obj[field]; config,
if (_.isString(url) && _.last(url) === '/') { version
obj[field] = url.substr(0, url.length - 1);
} }
} })()
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);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,98 +1,99 @@
'use strict'; 'use strict';
var glob = require('glob'); const _ = require('lodash');
var _ = require('lodash'); const cron = require('node-cron');
const glob = require('glob');
const moment = require('moment');
var jobFiles = glob.sync(__dirname + '/*Job.js'); const config = require('../config').config
const Logger = require('../logger')
const jobFiles = glob.sync(__dirname + '/*Job.js');
_.each(jobFiles, function (jobFile) { _.each(jobFiles, function (jobFile) {
require(jobFile); require(jobFile);
}); });
angular.module('ffffng').factory('Scheduler', function ($injector, Logger, config, moment) { const tasks = {};
var cron = require('node-cron');
var tasks = {}; let taskId = 1;
function nextTaskId() {
const id = taskId;
taskId += 1;
return id;
}
var taskId = 1; function schedule(expr, jobName) {
function nextTaskId() { Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName);
var id = taskId;
taskId += 1; var job = require(`../jobs/${jobName}`);
return id;
if (!_.isFunction(job.run)) {
throw new Error('The job ' + jobName + ' does not provide a "run" function.');
} }
function schedule(expr, jobName) { var id = nextTaskId();
Logger.tag('jobs').info('Scheduling job: %s %s', expr, jobName); var task = {
id: id,
var job = $injector.get(jobName); name: jobName,
description: job.description,
if (!_.isFunction(job.run)) { schedule: expr,
throw new Error('The job ' + jobName + ' does not provide a "run" function.'); job: job,
} runningSince: false,
lastRunStarted: false,
var id = nextTaskId(); lastRunDuration: null,
var task = { state: 'idle',
id: id, enabled: true
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;
}
}; };
});
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;
}
}

View file

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

View file

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

View file

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

View file

@ -1,31 +1,30 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('FrontendResource', function ( const fs = require('graceful-fs')
Logger,
Resources,
ErrorTypes,
fs
) {
var indexHtml = __dirname + '/../../client/index.html';
return { const ErrorTypes = require('../utils/errorTypes')
render: function (req, res) { const Logger = require('../logger')
var data = Resources.getData(req); const Resources = require('../utils/resources')
fs.readFile(indexHtml, 'utf8', function (err, body) { const indexHtml = __dirname + '/../../client/index.html';
if (err) {
Logger.tag('frontend').error('Could not read file: ', indexHtml, err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
return Resources.successHtml( module.exports = {
res, render (req, res) {
body.replace( const data = Resources.getData(req);
/<body/,
'<script>window.__nodeToken = \''+ data.token + '\';</script><body' 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});
} }
};
}); return Resources.successHtml(
res,
body.replace(
/<body/,
'<script>window.__nodeToken = \''+ data.token + '\';</script><body'
)
);
});
}
}

View file

@ -1,101 +1,99 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('MailResource', function ( const Constraints = require('../../shared/validation/constraints')
Constraints, const ErrorTypes = require('../utils/errorTypes')
Validator, const Logger = require('../logger')
MailService, const MailService = require('../services/mailService')
Resources, const Resources = require('../utils/resources')
Logger, const Strings = require('../utils/strings')
ErrorTypes, const Validator = require('../validation/validator')
Strings
) {
var isValidId = Validator.forConstraint(Constraints.id);
function withValidMailId(req, res, callback) { const isValidId = Validator.forConstraint(Constraints.id);
var id = Strings.normalizeString(Resources.getData(req).id);
if (!isValidId(id)) { function withValidMailId(req, res, callback) {
return callback({data: 'Invalid mail id.', type: ErrorTypes.badRequest}); const id = Strings.normalizeString(Resources.getData(req).id);
}
callback(null, id); if (!isValidId(id)) {
return callback({data: 'Invalid mail id.', type: ErrorTypes.badRequest});
} }
return { callback(null, id);
get: function (req, res) { }
withValidMailId(req, res, function (err, id) {
module.exports = {
get (req, res) {
withValidMailId(req, res, function (err, id) {
if (err) {
return Resources.error(res, err);
}
MailService.getMail(id, function (err, mail) {
if (err) { if (err) {
return Resources.error(res, err); Logger.tag('mails', 'admin').error('Error getting mail:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
MailService.getMail(id, function (err, mail) { if (!mail) {
return Resources.error(res, {data: 'Mail not found.', type: ErrorTypes.notFound});
}
return Resources.success(res, mail);
});
});
},
getAll (req, res) {
Resources.getValidRestParams('list', null, req, function (err, restParams) {
if (err) {
return Resources.error(res, err);
}
return MailService.getPendingMails(
restParams,
function (err, mails, total) {
if (err) { if (err) {
Logger.tag('mails', 'admin').error('Error getting mail:', err); Logger.tag('mails', 'admin').error('Could not get pending mails:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError}); return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
if (!mail) { res.set('X-Total-Count', total);
return Resources.error(res, {data: 'Mail not found.', type: ErrorTypes.notFound}); return Resources.success(res, mails);
} }
);
});
},
return Resources.success(res, mail); delete (req, res) {
}); withValidMailId(req, res, function (err, id) {
}); if (err) {
}, return Resources.error(res, err);
}
getAll: function (req, res) { MailService.deleteMail(id, function (err) {
Resources.getValidRestParams('list', null, req, function (err, restParams) {
if (err) { if (err) {
return Resources.error(res, err); Logger.tag('mails', 'admin').error('Error deleting mail:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
return MailService.getPendingMails( return Resources.success(res);
restParams,
function (err, mails, total) {
if (err) {
Logger.tag('mails', 'admin').error('Could not get pending mails:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
res.set('X-Total-Count', total);
return Resources.success(res, mails);
}
);
}); });
}, });
},
delete: function (req, res) { resetFailures (req, res) {
withValidMailId(req, res, function (err, id) { withValidMailId(req, res, function (err, id) {
if (err) {
return Resources.error(res, err);
}
MailService.resetFailures(id, function (err, mail) {
if (err) { if (err) {
return Resources.error(res, err); Logger.tag('mails', 'admin').error('Error resetting failure count:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
MailService.deleteMail(id, function (err) { return Resources.success(res, mail);
if (err) {
Logger.tag('mails', 'admin').error('Error deleting mail:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
return Resources.success(res);
});
}); });
}, });
}
resetFailures: function (req, res) { }
withValidMailId(req, res, function (err, id) {
if (err) {
return Resources.error(res, err);
}
MailService.resetFailures(id, function (err, mail) {
if (err) {
Logger.tag('mails', 'admin').error('Error resetting failure count:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
return Resources.success(res, mail);
});
});
}
};
});

View file

@ -1,83 +1,82 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('MonitoringResource', function ( const _ = require('lodash')
Constraints,
Validator,
MonitoringService,
Logger,
_,
Strings,
Resources,
ErrorTypes
) {
var isValidToken = Validator.forConstraint(Constraints.token);
return { const Constraints = require('../../shared/validation/constraints')
getAll: function (req, res) { const ErrorTypes = require('../utils/errorTypes')
Resources.getValidRestParams('list', null, req, function (err, restParams) { const Logger = require('../logger')
if (err) { const MonitoringService = require('../services/monitoringService')
return Resources.error(res, err); const Resources = require('../utils/resources')
} const Strings = require('../utils/strings')
const Validator = require('../validation/validator')
return MonitoringService.getAll( const isValidToken = Validator.forConstraint(Constraints.token);
restParams,
function (err, monitoringStates, total) {
if (err) {
Logger.tag('monitoring', 'admin').error('Could not get monitoring states:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
res.set('X-Total-Count', total); module.exports = {
return Resources.success(res, _.map(monitoringStates, function (state) { getAll (req, res) {
state.mapId = _.toLower(state.mac).replace(/:/g, ''); Resources.getValidRestParams('list', null, req, function (err, restParams) {
return state; if (err) {
})); return Resources.error(res, err);
}
return MonitoringService.getAll(
restParams,
function (err, monitoringStates, total) {
if (err) {
Logger.tag('monitoring', 'admin').error('Could not get monitoring states:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
);
});
},
confirm: function (req, res) { res.set('X-Total-Count', total);
var data = Resources.getData(req); return Resources.success(res, _.map(monitoringStates, function (state) {
state.mapId = _.toLower(state.mac).replace(/:/g, '');
var token = Strings.normalizeString(data.token); return state;
if (!isValidToken(token)) { }));
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return MonitoringService.confirm(token, function (err, node) {
if (err) {
return Resources.error(res, err);
} }
return Resources.success(res, { );
hostname: node.hostname, });
mac: node.mac, },
email: node.email,
monitoring: node.monitoring,
monitoringConfirmed: node.monitoringConfirmed
});
});
},
disable: function (req, res) { confirm (req, res) {
var data = Resources.getData(req); const data = Resources.getData(req);
var token = Strings.normalizeString(data.token); const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) { if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return MonitoringService.disable(token, function (err, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {
hostname: node.hostname,
mac: node.mac,
email: node.email,
monitoring: node.monitoring
});
});
} }
};
}); return MonitoringService.confirm(token, function (err, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {
hostname: node.hostname,
mac: node.mac,
email: node.email,
monitoring: node.monitoring,
monitoringConfirmed: node.monitoringConfirmed
});
});
},
disable (req, res) {
const data = Resources.getData(req);
const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return MonitoringService.disable(token, function (err, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {
hostname: node.hostname,
mac: node.mac,
email: node.email,
monitoring: node.monitoring
});
});
}
}

View file

@ -1,182 +1,181 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('NodeResource', function ( const _ = require('lodash')
Constraints, const deepExtend = require('deep-extend')
Validator,
Logger,
MonitoringService,
NodeService,
_,
deepExtend,
Strings,
Resources,
ErrorTypes
) {
var nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
function getNormalizedNodeData(reqData) { const Constraints = require('../../shared/validation/constraints')
var node = {}; const ErrorTypes = require('../utils/errorTypes')
_.each(nodeFields, function (field) { const Logger = require('../logger')
var value = Strings.normalizeString(reqData[field]); const MonitoringService = require('../services/monitoringService')
if (field === 'mac') { const NodeService = require('../services/nodeService')
value = Strings.normalizeMac(value); const Strings = require('../utils/strings')
const Validator = require('../validation/validator')
const Resources = require('../utils/resources')
const nodeFields = ['hostname', 'key', 'email', 'nickname', 'mac', 'coords', 'monitoring'];
function getNormalizedNodeData(reqData) {
const node = {};
_.each(nodeFields, function (field) {
let value = Strings.normalizeString(reqData[field]);
if (field === 'mac') {
value = Strings.normalizeMac(value);
}
node[field] = value;
});
return node;
}
const isValidNode = Validator.forConstraints(Constraints.node);
const isValidToken = Validator.forConstraint(Constraints.token);
module.exports = {
create: function (req, res) {
const data = Resources.getData(req);
const node = getNormalizedNodeData(data);
if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
}
return NodeService.createNode(node, function (err, token, node) {
if (err) {
return Resources.error(res, err);
} }
node[field] = value; return Resources.success(res, {token: token, node: node});
}); });
return node; },
}
var isValidNode = Validator.forConstraints(Constraints.node); update: function (req, res) {
var isValidToken = Validator.forConstraint(Constraints.token); const data = Resources.getData(req);
return { const token = Strings.normalizeString(data.token);
create: function (req, res) { if (!isValidToken(token)) {
var data = Resources.getData(req); return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
var node = getNormalizedNodeData(data); const node = getNormalizedNodeData(data);
if (!isValidNode(node)) { if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest}); return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
}
return NodeService.updateNode(token, node, function (err, token, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {token: token, node: node});
});
},
delete: function (req, res) {
const data = Resources.getData(req);
const token = Strings.normalizeString(data.token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return NodeService.deleteNode(token, function (err) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {});
});
},
get: function (req, res) {
const token = Strings.normalizeString(Resources.getData(req).token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return NodeService.getNodeDataByToken(token, function (err, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, node);
});
},
getAll: function (req, res) {
Resources.getValidRestParams('list', 'node', req, function (err, restParams) {
if (err) {
return Resources.error(res, err);
} }
return NodeService.createNode(node, function (err, token, node) { return NodeService.getAllNodes(function (err, nodes) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {token: token, node: node});
});
},
update: function (req, res) {
var data = Resources.getData(req);
var token = Strings.normalizeString(data.token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
var node = getNormalizedNodeData(data);
if (!isValidNode(node)) {
return Resources.error(res, {data: 'Invalid node data.', type: ErrorTypes.badRequest});
}
return NodeService.updateNode(token, node, function (err, token, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {token: token, node: node});
});
},
delete: function (req, res) {
var data = Resources.getData(req);
var token = Strings.normalizeString(data.token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return NodeService.deleteNode(token, function (err) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, {});
});
},
get: function (req, res) {
var token = Strings.normalizeString(Resources.getData(req).token);
if (!isValidToken(token)) {
return Resources.error(res, {data: 'Invalid token.', type: ErrorTypes.badRequest});
}
return NodeService.getNodeDataByToken(token, function (err, node) {
if (err) {
return Resources.error(res, err);
}
return Resources.success(res, node);
});
},
getAll: function (req, res) {
Resources.getValidRestParams('list', 'node', req, function (err, restParams) {
if (err) { if (err) {
return Resources.error(res, err); return Resources.error(res, err);
} }
return NodeService.getAllNodes(function (err, nodes) { const realNodes = _.filter(nodes, function (node) {
// We ignore nodes without tokens as those are only manually added ones like gateways.
return node.token;
});
const macs = _.map(realNodes, function (node) {
return node.mac;
});
MonitoringService.getByMacs(macs, function (err, nodeStateByMac) {
if (err) { if (err) {
return Resources.error(res, err); Logger.tag('nodes', 'admin').error('Error getting nodes by MACs:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
var realNodes = _.filter(nodes, function (node) { const enhancedNodes = _.map(realNodes, function (node) {
// We ignore nodes without tokens as those are only manually added ones like gateways. const nodeState = nodeStateByMac[node.mac];
return node.token; if (nodeState) {
}); return deepExtend({}, node, {
site: nodeState.site,
var macs = _.map(realNodes, function (node) { domain: nodeState.domain,
return node.mac; onlineState: nodeState.state
}); });
MonitoringService.getByMacs(macs, function (err, nodeStateByMac) {
if (err) {
Logger.tag('nodes', 'admin').error('Error getting nodes by MACs:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
} }
var enhancedNodes = _.map(realNodes, function (node) { return node;
var nodeState = nodeStateByMac[node.mac];
if (nodeState) {
return deepExtend({}, node, {
site: nodeState.site,
domain: nodeState.domain,
onlineState: nodeState.state
});
}
return node;
});
var filteredNodes = Resources.filter(
enhancedNodes,
[
'hostname',
'nickname',
'email',
'token',
'mac',
'site',
'domain',
'key',
'onlineState'
],
restParams
);
var total = filteredNodes.length;
var sortedNodes = Resources.sort(
filteredNodes,
[
'hostname',
'nickname',
'email',
'token',
'mac',
'key',
'site',
'domain',
'coords',
'onlineState',
'monitoringState'
],
restParams
);
var pageNodes = Resources.getPageEntities(sortedNodes, restParams);
res.set('X-Total-Count', total);
return Resources.success(res, pageNodes);
}); });
const filteredNodes = Resources.filter(
enhancedNodes,
[
'hostname',
'nickname',
'email',
'token',
'mac',
'site',
'domain',
'key',
'onlineState'
],
restParams
);
const total = filteredNodes.length;
const sortedNodes = Resources.sort(
filteredNodes,
[
'hostname',
'nickname',
'email',
'token',
'mac',
'key',
'site',
'domain',
'coords',
'onlineState',
'monitoringState'
],
restParams
);
const pageNodes = Resources.getPageEntities(sortedNodes, restParams);
res.set('X-Total-Count', total);
return Resources.success(res, pageNodes);
}); });
}); });
} });
}; }
}); }

View file

@ -1,26 +1,24 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('StatisticsResource', function ( const ErrorTypes = require('../utils/errorTypes')
Logger, const Logger = require('../logger')
NodeService, const NodeService = require('../services/nodeService')
Resources, const Resources = require('../utils/resources')
ErrorTypes
) {
return {
get: function (req, res) {
NodeService.getNodeStatistics(function (err, nodeStatistics) {
if (err) {
Logger.tag('statistics').error('Error getting statistics:', err);
return Resources.error(res, {data: 'Internal error.', type: ErrorTypes.internalError});
}
return Resources.success( module.exports = {
res, get (req, res) {
{ NodeService.getNodeStatistics((err, nodeStatistics) => {
nodes: nodeStatistics 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
}
);
});
}
}

View file

@ -1,127 +1,126 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('TaskResource', function ( const _ = require('lodash')
Constraints,
Validator,
_,
Strings,
Resources,
ErrorTypes,
Scheduler
) {
var isValidId = Validator.forConstraint(Constraints.id);
function toExternalTask(task) { const Constraints = require('../../shared/validation/constraints')
return { const ErrorTypes = require('../utils/errorTypes')
id: task.id, const Resources = require('../utils/resources')
name: task.name, const Scheduler = require('../jobs/scheduler')
description: task.description, const Strings = require('../utils/strings')
schedule: task.schedule, const Validator = require('../validation/validator')
runningSince: task.runningSince && task.runningSince.unix(),
lastRunStarted: task.lastRunStarted && task.lastRunStarted.unix(), const isValidId = Validator.forConstraint(Constraints.id);
lastRunDuration: task.lastRunDuration || undefined,
state: task.state, function toExternalTask(task) {
enabled: task.enabled 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) { callback(null, id);
var id = Strings.normalizeString(Resources.getData(req).id); }
if (!isValidId(id)) { function getTask(id, callback) {
return callback({data: 'Invalid task id.', type: ErrorTypes.badRequest}); 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); getTask(id, function (err, task) {
}
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) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
getTask(id, function (err, task) { callback(null, task);
if (err) {
return callback(err);
}
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) { withTask(req, res, function (err, task) {
if (err) { if (err) {
return Resources.error(res, 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)); 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);
}
};
});

View file

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

View file

@ -1,53 +1,52 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Router', function ( const express = require('express');
app,
VersionResource,
StatisticsResource,
FrontendResource,
NodeResource,
MonitoringResource,
TaskResource,
MailResource,
config
) {
return {
init: function () {
var express = require('express');
var router = express.Router();
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.post('/', FrontendResource.render);
router.put('/api/node/:token', NodeResource.update);
router.delete('/api/node/:token', NodeResource.delete);
router.get('/api/node/:token', NodeResource.get);
router.put('/api/monitoring/confirm/:token', MonitoringResource.confirm); router.get('/api/version', VersionResource.get);
router.put('/api/monitoring/disable/:token', MonitoringResource.disable);
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('/api/monitoring/confirm/:token', MonitoringResource.confirm);
router.put('/internal/api/tasks/run/:id', TaskResource.run); router.put('/api/monitoring/disable/:token', MonitoringResource.disable);
router.put('/internal/api/tasks/enable/:id', TaskResource.enable);
router.put('/internal/api/tasks/disable/:id', TaskResource.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/tasks', TaskResource.getAll);
router.get('/internal/api/mails/:id', MailResource.get); router.put('/internal/api/tasks/run/:id', TaskResource.run);
router.delete('/internal/api/mails/:id', MailResource.delete); router.put('/internal/api/tasks/enable/:id', TaskResource.enable);
router.put('/internal/api/mails/reset/:id', MailResource.resetFailures); router.put('/internal/api/tasks/disable/:id', TaskResource.disable);
router.put('/internal/api/nodes/:token', NodeResource.update); router.get('/internal/api/monitoring', MonitoringResource.getAll);
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); 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);
}
}

View file

@ -1,238 +1,235 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('MailService', function ( const async = require('async')
Database, const deepExtend = require('deep-extend')
MailTemplateService, const moment = require('moment')
config,
_,
async,
deepExtend,
fs,
moment,
Logger,
Resources
) {
var MAIL_QUEUE_DB_BATCH_SIZE = 50;
var MAIL_QUEUE_MAX_PARALLEL_SENDING = 3;
var transporter = require('nodemailer').createTransport(deepExtend( const config = require('../config').config
{}, const Database = require('../db/database').db
config.server.email.smtp, const Logger = require('../logger')
{ const MailTemplateService = require('./mailTemplateService')
transport: 'smtp', const Resources = require('../utils/resources')
pool: true
}
));
MailTemplateService.configureTransporter(transporter); const MAIL_QUEUE_DB_BATCH_SIZE = 50;
const MAIL_QUEUE_MAX_PARALLEL_SENDING = 3;
function sendMail(options, callback) { const transporter = require('nodemailer').createTransport(deepExtend(
Logger {},
.tag('mail', 'queue') config.server.email.smtp,
.info( {
'Sending pending mail[%d] of type %s. ' + transport: 'smtp',
'Had %d failures before.', pool: true
options.id, options.email, options.failures }
); ));
MailTemplateService.render(options, function (err, renderedTemplate) { MailTemplateService.configureTransporter(transporter);
if (err) {
return callback(err);
}
var mailOptions = { function sendMail(options, callback) {
from: options.sender, Logger
to: options.recipient, .tag('mail', 'queue')
subject: renderedTemplate.subject, .info(
html: renderedTemplate.body 'Sending pending mail[%d] of type %s. ' +
}; 'Had %d failures before.',
options.id, options.email, options.failures
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) { MailTemplateService.render(options, function (err, renderedTemplate) {
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) {
if (err) { if (err) {
// we only log the error and increment the failure counter as we want to continue with pending mails return callback(err);
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); const mailOptions = {
}); from: options.sender,
} to: options.recipient,
subject: renderedTemplate.subject,
function doGetMail(id, callback) { html: renderedTemplate.body
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
);
});
}; };
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);
}
}

View file

@ -1,125 +1,122 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('MailTemplateService', function ( const async = require('async')
UrlBuilder, const deepExtend = require('deep-extend')
config, const fs = require('graceful-fs')
_, const moment = require('moment')
async,
deepExtend,
fs,
moment,
Logger
) {
var templateBasePath = __dirname + '/../mailTemplates';
var snippetsBasePath = templateBasePath + '/snippets';
var templateFunctions = {}; const config = require('../config').config
const Logger = require('../logger')
const UrlBuilder = require('../utils/urlBuilder')
function renderSnippet(name, data) { const templateBasePath = __dirname + '/../mailTemplates';
var snippetFile = snippetsBasePath + '/' + name + '.html'; const snippetsBasePath = templateBasePath + '/snippets';
return _.template(fs.readFileSync(snippetFile).toString())(deepExtend( const templateFunctions = {};
{},
// jshint -W040
this, // parent data
// jshint +W040
data,
templateFunctions
));
}
function snippet(name) { function renderSnippet(name, data) {
return function (data) { const snippetFile = snippetsBasePath + '/' + name + '.html';
return renderSnippet.bind(this)(name, data);
};
}
function renderLink(href, text) { return _.template(fs.readFileSync(snippetFile).toString())(deepExtend(
return _.template( {},
'<a href="<%- href %>#" style="color: #E5287A;"><%- text %></a>' // jshint -W040
)({ this, // parent data
href: href, // jshint +W040
text: text || href data,
}); templateFunctions
} ));
}
function renderHR() { function snippet(name) {
return '<hr style="border-top: 1px solid #333333; border-left: 0; border-right: 0; border-bottom: 0;" />'; return function (data) {
} return renderSnippet.bind(this)(name, data);
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 renderLink(href, text) {
return _.template(
'<a href="<%- href %>#" style="color: #E5287A;"><%- text %></a>'
)({
href: href,
text: text || href
});
}
function renderHR() {
return '<hr style="border-top: 1px solid #333333; border-left: 0; border-right: 0; border-bottom: 0;" />';
}
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);
}
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,346 +1,398 @@
'use strict'; 'use strict';
angular.module('ffffng') const _ = require('lodash')
.service('NodeService', function ( const async = require('async')
config, const crypto = require('crypto')
_, const fs = require('graceful-fs')
async, const glob = require('glob')
crypto,
fs,
glob,
Logger,
MailService,
Strings,
ErrorTypes,
UrlBuilder
) {
var MAX_PARALLEL_NODES_PARSING = 10;
var linePrefixes = { const config = require('../config').config
hostname: '# Knotenname: ', const ErrorTypes = require('../utils/errorTypes')
nickname: '# Ansprechpartner: ', const Logger = require('../logger')
email: '# Kontakt: ', const MailService = require('../services/mailService')
coords: '# Koordinaten: ', const Strings = require('../utils/strings')
mac: '# MAC: ', const UrlBuilder = require('../utils/urlBuilder')
token: '# Token: ',
monitoring: '# Monitoring: ',
monitoringToken: '# Monitoring-Token: '
};
var filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken']; const MAX_PARALLEL_NODES_PARSING = 10;
function generateToken() { const linePrefixes = {
return crypto.randomBytes(8).toString('hex'); hostname: '# Knotenname: ',
} nickname: '# Ansprechpartner: ',
email: '# Kontakt: ',
coords: '# Koordinaten: ',
mac: '# MAC: ',
token: '# Token: ',
monitoring: '# Monitoring: ',
monitoringToken: '# Monitoring-Token: '
};
function toNodeFilesPattern(filter) { const filenameParts = ['hostname', 'mac', 'key', 'token', 'monitoringToken'];
var pattern = _.join(
_.map(filenameParts, function (field) {
return filter.hasOwnProperty(field) ? filter[field] : '*';
}),
'@'
);
return config.server.peersPath + '/' + pattern.toLowerCase(); function generateToken() {
} return crypto.randomBytes(8).toString('hex');
}
function findNodeFiles(filter, callback) { function toNodeFilesPattern(filter) {
glob(toNodeFilesPattern(filter), callback); const pattern = _.join(
} _.map(filenameParts, function (field) {
return filter.hasOwnProperty(field) ? filter[field] : '*';
}),
'@'
);
function findNodeFilesSync(filter) { return config.server.peersPath + '/' + pattern.toLowerCase();
return glob.sync(toNodeFilesPattern(filter)); }
}
function findFilesInPeersPath(callback) { function findNodeFiles(filter, callback) {
glob(config.server.peersPath + '/*', function (err, files) { glob(toNodeFilesPattern(filter), callback);
if (err) { }
return callback(err);
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) { fs.lstat(file, function (err, stats) {
if (file[0] === '.') { if (err) {
return fileCallback(null, false); return fileCallback(err);
} }
fs.lstat(file, function (err, stats) { fileCallback(null, stats.isFile());
if (err) { });
return fileCallback(err); }, callback);
} });
}
fileCallback(null, stats.isFile()); function parseNodeFilename(filename) {
}); const parts = _.split(filename, '@', filenameParts.length);
}, callback); 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) { if (files.length > 1 || !token /* node is being created*/) {
var parts = _.split(filename, '@', filenameParts.length); return true;
var parsed = {};
_.each(_.zip(filenameParts, parts), function (part) {
parsed[part[0]] = part[1];
});
return parsed;
} }
function isDuplicate(filter, token) { return parseNodeFilename(files[0]).token !== token;
var files = findNodeFilesSync(filter); }
if (files.length === 0) {
return false;
}
if (files.length > 1 || !token /* node is being created*/) { function checkNoDuplicates(token, node, nodeSecrets) {
return true; if (isDuplicate({ hostname: node.hostname }, token)) {
} return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict};
return parseNodeFilename(files[0]).token !== token;
} }
function checkNoDuplicates(token, node, nodeSecrets) { if (node.key) {
if (isDuplicate({ hostname: node.hostname }, token)) { if (isDuplicate({ key: node.key }, token)) {
return {data: {msg: 'Already exists.', field: 'hostname'}, type: ErrorTypes.conflict}; 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) { if (isDuplicate({ mac: node.mac }, token)) {
return config.server.peersPath + '/' + return {data: {msg: 'Already exists.', field: 'mac'}, type: ErrorTypes.conflict};
(
(node.hostname || '') + '@' +
(node.mac || '') + '@' +
(node.key || '') + '@' +
(token || '') + '@' +
(nodeSecrets.monitoringToken || '')
).toLowerCase();
} }
function writeNodeFile(isUpdate, token, node, nodeSecrets, callback) { if (nodeSecrets.monitoringToken && isDuplicate({ monitoringToken: nodeSecrets.monitoringToken }, token)) {
var filename = toNodeFilename(token, node, nodeSecrets); return {data: {msg: 'Already exists.', field: 'monitoringToken'}, type: ErrorTypes.conflict};
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;
case 'monitoringToken': return null;
value = nodeSecrets.monitoringToken || ''; }
break;
default: function toNodeFilename(token, node, nodeSecrets) {
value = key === 'token' ? token : node[key]; return config.server.peersPath + '/' +
if (_.isUndefined(value)) { (
value = _.isUndefined(nodeSecrets[key]) ? '' : nodeSecrets[key]; (node.hostname || '') + '@' +
} (node.mac || '') + '@' +
break; (node.key || '') + '@' +
} (token || '') + '@' +
data += prefix + value + '\n'; (nodeSecrets.monitoringToken || '')
}); ).toLowerCase();
if (node.key) { }
data += 'key "' + node.key + '";\n';
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) { try {
var files = findNodeFilesSync({ token: token }); fs.writeFileSync(filename, data, 'utf8');
if (files.length !== 1) { }
return callback({data: 'Node not found.', type: ErrorTypes.notFound}); 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); return callback(null, token, node);
if (error) { }
return callback(error);
}
var file = files[0]; function deleteNodeFile(token, callback) {
try { findNodeFiles({ token: token }, function (err, files) {
fs.unlinkSync(file); if (err) {
} Logger.tag('node', 'delete').error('Could not find node file: ' + files, err);
catch (error) { return callback({data: 'Could not delete node.', type: ErrorTypes.internalError});
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});
} if (files.length !== 1) {
} else { return callback({data: 'Node not found.', type: ErrorTypes.notFound});
error = checkNoDuplicates(null, node, nodeSecrets);
if (error) {
return callback(error);
}
} }
try { try {
fs.writeFileSync(filename, data, 'utf8'); fs.unlinkSync(files[0]);
} }
catch (error) { catch (error) {
Logger.tag('node', 'save').error('Could not write node file: ' + filename, error); Logger.tag('node', 'delete').error('Could not delete node file: ' + files, error);
return callback({data: 'Could not write node data.', type: ErrorTypes.internalError}); return callback({data: 'Could not delete node.', type: ErrorTypes.internalError});
} }
return callback(null, token, node); return callback(null);
} });
}
function deleteNodeFile(token, callback) { function parseNodeFile(file, callback) {
findNodeFiles({ token: token }, function (err, files) { fs.readFile(file, function (err, contents) {
if (err) { if (err) {
Logger.tag('node', 'delete').error('Could not find node file: ' + files, err); return callback(err);
return callback({data: 'Could not delete node.', type: ErrorTypes.internalError}); }
}
if (files.length !== 1) { const lines = contents.toString();
return callback({data: 'Node not found.', type: ErrorTypes.notFound});
}
try { const node = {};
fs.unlinkSync(files[0]); const nodeSecrets = {};
}
catch (error) {
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); _.each(lines.split('\n'), function (line) {
}); const entries = {};
}
function parseNodeFile(file, callback) { for (const key in linePrefixes) {
fs.readFile(file, function (err, contents) { if (linePrefixes.hasOwnProperty(key)) {
if (err) { const prefix = linePrefixes[key];
return callback(err); if (line.substring(0, prefix.length) === prefix) {
} entries[key] = Strings.normalizeString(line.substr(prefix.length));
break;
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;
}
} }
} }
}
if (_.isEmpty(entries) && line.substring(0, 5) === 'key "') { if (_.isEmpty(entries) && line.substring(0, 5) === 'key "') {
entries.key = Strings.normalizeString(line.split('"')[1]); 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) { callback(null, node, nodeSecrets);
findNodeFiles(filter, function (err, files) { });
}
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) { if (err) {
return callback(err); return callback(err);
} }
if (files.length !== 1) { if (node.monitoring && !node.monitoringConfirmed) {
return callback(null); return sendMonitoringConfirmationMail(node, nodeSecrets, function (err) {
if (err) {
return callback(err);
}
return callback(null, token, node);
});
} }
var file = files[0]; return callback(null, token, node);
return parseNodeFile(file, callback);
}); });
} },
function getNodeDataByFilePattern(filter, callback) { updateNode: function (token, node, callback) {
findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) { this.getNodeDataByToken(token, function (err, currentNode, nodeSecrets) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
if (!node) { let monitoringConfirmed = false;
return callback({data: 'Node not found.', type: ErrorTypes.notFound}); let monitoringToken = '';
}
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;
if (node.monitoring) { 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) { if (err) {
return callback(err); return callback(err);
} }
@ -357,178 +409,124 @@ angular.module('ffffng')
return callback(null, token, node); return callback(null, token, node);
}); });
}, });
},
updateNode: function (token, node, callback) { internalUpdateNode: function (token, node, nodeSecrets, callback) {
this.getNodeDataByToken(token, function (err, currentNode, nodeSecrets) { writeNodeFile(true, token, node, nodeSecrets, callback);
if (err) { },
return callback(err);
}
var monitoringConfirmed = false; deleteNode: function (token, callback) {
var monitoringToken = ''; deleteNodeFile(token, callback);
},
if (node.monitoring) { getAllNodes: function (callback) {
if (!currentNode.monitoring) { findNodeFiles({}, function (err, files) {
// monitoring just has been enabled if (err) {
monitoringConfirmed = false; Logger.tag('nodes').error('Error getting all nodes:', err);
monitoringToken = generateToken(); return callback({data: 'Internal error.', type: ErrorTypes.internalError});
}
} else { async.mapLimit(
// monitoring is still enabled files,
MAX_PARALLEL_NODES_PARSING,
if (currentNode.email !== node.email) { parseNodeFile,
// new email so we need a new token and a reconfirmation function (err, nodes) {
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) {
if (err) { 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 callback(null, nodes);
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});
} }
);
});
},
async.mapLimit( getNodeDataByMac: function (mac, callback) {
files, return findNodeDataByFilePattern({ mac: mac }, callback);
MAX_PARALLEL_NODES_PARSING, },
parseNodeFile,
function (err, nodes) { 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) { if (err) {
Logger.tag('nodes').error('Error getting all nodes:', err); return fileCallback(err);
return callback({data: 'Internal error.', type: ErrorTypes.internalError});
} }
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) { fileCallback(null);
return findNodeDataByFilePattern({ mac: mac }, callback); });
}, }
getNodeDataByToken: function (token, callback) { fileCallback(null);
return getNodeDataByFilePattern({ token: token }, callback); });
}, },
callback
);
});
},
getNodeDataByMonitoringToken: function (monitoringToken, callback) { getNodeStatistics: function (callback) {
return getNodeDataByFilePattern({ monitoringToken: monitoringToken }, callback); this.getAllNodes(function (err, nodes) {
}, if (err) {
return callback(err);
}
fixNodeFilenames: function (callback) { const nodeStatistics = {
findFilesInPeersPath(function (err, files) { registered: _.size(nodes),
if (err) { withVPN: 0,
return callback(err); withCoords: 0,
monitoring: {
active: 0,
pending: 0
}
};
_.each(nodes, function (node) {
if (node.key) {
nodeStatistics.withVPN += 1;
} }
async.mapLimit( if (node.coords) {
files, nodeStatistics.withCoords += 1;
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);
} }
var nodeStatistics = { switch (node.monitoringState) {
registered: _.size(nodes), case 'active':
withVPN: 0, nodeStatistics.monitoring.active += 1;
withCoords: 0, break;
monitoring: { case 'pending':
active: 0, nodeStatistics.monitoring.pending += 1;
pending: 0 break;
} }
};
_.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);
}); });
}
}; callback(null, nodeStatistics);
}); });
}
}

View file

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

View file

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

View file

@ -1,224 +1,229 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Resources', function (_, Constraints, Validator, ErrorTypes, Logger) { const _ = require('lodash')
function respond(res, httpCode, data, type) {
switch (type) {
case 'html':
res.writeHead(httpCode, {'Content-Type': 'text/html'});
res.end(data);
break;
default: const Constraints = require('../../shared/validation/constraints')
res.writeHead(httpCode, {'Content-Type': 'application/json'}); const ErrorTypes = require('../utils/errorTypes')
res.end(JSON.stringify(data)); const Logger = require('../logger')
break; 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) { return {
var sortField = _.includes(allowedSortFields, restParams._sortField) ? restParams._sortField : undefined; query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'),
if (!sortField) { params: []
sortField = defaultSortField; };
} }
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 { return {
query: 'ORDER BY ' + sortField + ' ' + (restParams._sortDir === 'ASC' ? 'ASC' : 'DESC'), query: '1 = 1',
params: [] params: []
}; };
} }
function limitOffsetClause(restParams) { let query = _.join(
var page = restParams._page; _.map(filterFields, function (field) {
var perPage = restParams._perPage; return 'LOWER(' + field + ') LIKE ?';
}),
' OR '
);
return { query += ' ESCAPE \'\\\'';
query: 'LIMIT ? OFFSET ?',
params: [perPage, ((page - 1) * perPage)]
};
}
function escapeForLikePattern(str) { const search = '%' + (_.isString(restParams.q) ? escapeForLikePattern(_.toLower(restParams.q.trim())) : '') + '%';
return str const params = _.times(filterFields.length, _.constant(search));
.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;
}
return { return {
getData: function (req) { query: query,
return _.extend({}, req.body, req.params, req.query); params: params
}, };
}
getValidRestParams: function(type, subtype, req, callback) { function getConstrainedValues(data, constraints) {
var constraints = Constraints.rest[type]; const values = {};
if (!_.isPlainObject(constraints)) { _.each(_.keys(constraints), function (key) {
Logger.tag('validation', 'rest').error('Unknown REST resource type: {}', type); 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}); 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');
}
}

View file

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

View file

@ -1,39 +1,41 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('UrlBuilder', function (_, config) { const _ = require('lodash')
function formUrl(route, queryParams) {
var url = config.server.baseUrl; const config = require('../config').config
if (route || queryParams) {
url += '/#/'; function formUrl(route, queryParams) {
} let url = config.server.baseUrl;
if (route) { if (route || queryParams) {
url += route; url += '/#/';
}
if (queryParams) {
url += '?';
url += _.join(
_.map(
queryParams,
function (value, key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
),
'&'
);
}
return url;
} }
if (route) {
url += route;
}
if (queryParams) {
url += '?';
url += _.join(
_.map(
queryParams,
function (value, key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(value);
}
),
'&'
);
}
return url;
}
return { module.exports = {
editNodeUrl: function () { editNodeUrl () {
return formUrl('update'); return formUrl('update');
}, },
monitoringConfirmUrl: function (nodeSecrets) { monitoringConfirmUrl (nodeSecrets) {
return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken }); return formUrl('monitoring/confirm', { token: nodeSecrets.monitoringToken });
}, },
monitoringDisableUrl: function (nodeSecrets) { monitoringDisableUrl (nodeSecrets) {
return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken }); return formUrl('monitoring/disable', { token: nodeSecrets.monitoringToken });
} }
}; }
});

View file

@ -1,93 +1,96 @@
'use strict'; 'use strict';
angular.module('ffffng').factory('Validator', function (_, Strings, Logger) { const _ = require('lodash')
// TODO: sanitize input for further processing as specified by constraints (correct types, trimming, etc.)
function isValidBoolean(value) { const Strings = require('../utils/strings')
return _.isBoolean(value) || value === 'true' || value === 'false'; 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 (!_.isNumber(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);
return false; return false;
} }
function areValid(constraints, acceptUndefined, values) { if (_.isNaN(value) || !_.isFinite(value)) {
var fields = Object.keys(constraints); return false;
for (var i = 0; i < fields.length; i ++) {
var field = fields[i];
if (!isValid(constraints[field], acceptUndefined, values[field])) {
return false;
}
}
return true;
} }
return { if (_.isNumber(constraint.min) && value < constraint.min) {
forConstraint: function (constraint, acceptUndefined) { return false;
return _.partial(isValid, constraint, acceptUndefined); }
},
forConstraints: function (constraints, acceptUndefined) { if (_.isNumber(constraint.max) && value > constraint.max) {
return _.partial(areValid, constraints, acceptUndefined); 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);
}
}

View file

@ -1,113 +1,139 @@
'use strict'; 'use strict';
angular.module('ffffng').constant('Constraints', { (function () {
id:{ var constraints = {
type: 'string', id:{
regex: /^[1-9][0-9]*/,
optional: false
},
token:{
type: 'string',
regex: /^[0-9a-f]{16}$/i,
optional: false
},
node: {
hostname: {
type: 'string', type: 'string',
regex: /^[-a-z0-9_]{1,32}$/i, regex: /^[1-9][0-9]*/,
optional: false optional: false
}, },
key: { token:{
type: 'string', type: 'string',
regex: /^([a-f0-9]{64})$/i, regex: /^[0-9a-f]{16}$/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,
optional: false optional: false
}, },
nickname: { node: {
type: 'string', hostname: {
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', 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 optional: true
}, },
q: { email: {
type: 'string', 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 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
}
})()