diff --git a/lib/admin/apis.js b/lib/admin/apis.js index 8eb82be..816c4ac 100644 --- a/lib/admin/apis.js +++ b/lib/admin/apis.js @@ -385,7 +385,7 @@ module.exports.create = function (deps, conf) { part = part[req.params.group]; } if (part && req.params.domId) { - part = part.domains.find(req.params.domId); + part = part.domains.findId(req.params.domId); } if (part && req.params.mod) { part = part[req.params.mod]; @@ -394,7 +394,7 @@ module.exports.create = function (deps, conf) { part = part[req.params.modGrp]; } if (part && req.params.modId) { - part = part.find(req.params.modId); + part = part.findId(req.params.modId); } if (part) { @@ -404,8 +404,20 @@ module.exports.create = function (deps, conf) { } }; - config.restful.saveBaseConfig = function (req, res) { + config.save = function (changer) { + var errors = changer.validate(); + if (errors.length) { + throw Object.assign(new Error(), errors[0], {statusCode: 400}); + } + + return deps.storage.config.save(changer); + }; + config.restful.saveBaseConfig = function (req, res, next) { console.log('config POST body', JSON.stringify(req.body)); + if (req.params.group === 'domains') { + next(); + return; + } deps.PromiseA.resolve().then(function () { var update; @@ -417,12 +429,8 @@ module.exports.create = function (deps, conf) { } var changer = new (require('./config').ConfigChanger)(conf); - var errors = changer.update(update); - if (errors.length) { - throw Object.assign(new Error(), errors[0], {statusCode: 400}); - } - - return deps.storage.config.save(changer); + changer.update(update); + return config.save(changer); }).then(function (config) { if (req.params.group) { config = config[req.params.group]; @@ -434,33 +442,41 @@ module.exports.create = function (deps, conf) { res.end(JSON.stringify({error: {message: err.message, code: err.code}})); }); }; - config.restful.createModule = function (req, res) { - var group = req.params.group; + + config.extractModList = function (changer, params) { var err; + if (params.domId) { + var dom = changer.domains.find(function (dom) { + return dom.id === params.domId; + }); + + if (!dom) { + err = new Error("no domain with ID '"+params.domId+"'"); + } else if (!dom.modules[params.group]) { + err = new Error("domains don't contain '"+params.group+"' modules"); + } else { + return dom.modules[params.group]; + } + } else { + if (!changer[params.group] || !changer[params.group].modules) { + err = new Error("'"+params.group+"' is not a valid settings group or doesn't support modules"); + } else { + return changer[params.group].modules; + } + } + + err.statusCode = 404; + throw err; + }; + config.restful.createModule = function (req, res, next) { + if (req.params.group === 'domains') { + next(); + return; + } + deps.PromiseA.resolve().then(function () { var changer = new (require('./config').ConfigChanger)(conf); - var modList; - if (req.params.domId) { - var dom = changer.domains.find(req.params.domId); - if (!dom) { - err = new Error("no domain with ID '"+req.params.domId+"'"); - } else if (!dom.modules[group]) { - err = new Error("domains don't contain '"+group+"' modules"); - } else { - modList = dom.modules[group]; - } - } else { - if (!changer[group] || !changer[group].modules) { - err = new Error("'"+group+"' is not a valid settings group or doesn't support modules"); - } else { - modList = changer[group].modules; - } - } - - if (err) { - err.statusCode = 404; - throw err; - } + var modList = config.extractModList(changer, req.params); var update = req.body; if (!Array.isArray(update)) { @@ -468,26 +484,54 @@ module.exports.create = function (deps, conf) { } update.forEach(modList.add, modList); - var errors = changer.validate(); - if (errors.length) { - throw Object.assign(new Error(), errors[0], {statusCode: 400}); - } - - return deps.storage.config.save(changer); - }).then(function (config) { - var result; - if (!req.params.domId) { - result = config[group].modules; - } else { - result = config.domains.find(function (dom) { return dom.id === req.params.domId; }).modules[group]; - } - res.send(deps.recase.snakeCopy(result)); + return config.save(changer); + }).then(function (newConf) { + res.send(deps.recase.snakeCopy(config.extractModList(newConf, req.params))); }, function (err) { res.statusCode = err.statusCode || 500; err.message = err.message || err.toString(); res.end(JSON.stringify({error: {message: err.message, code: err.code}})); }); }; + config.restful.updateModule = function (req, res, next) { + if (req.params.group === 'domains') { + next(); + return; + } + + deps.PromiseA.resolve().then(function () { + var changer = new (require('./config').ConfigChanger)(conf); + var modList = config.extractModList(changer, req.params); + modList.update(req.params.modId, req.body); + return config.save(changer); + }).then(function (newConf) { + res.send(deps.recase.snakeCopy(config.extractModList(newConf, req.params))); + }, function (err) { + res.statusCode = err.statusCode || 500; + err.message = err.message || err.toString(); + res.end(JSON.stringify({error: {message: err.message, code: err.code}})); + }); + }; + config.restful.removeModule = function (req, res, next) { + if (req.params.group === 'domains') { + next(); + return; + } + + deps.PromiseA.resolve().then(function () { + var changer = new (require('./config').ConfigChanger)(conf); + var modList = config.extractModList(changer, req.params); + modList.remove(req.params.modId); + return config.save(changer); + }).then(function (newConf) { + res.send(deps.recase.snakeCopy(config.extractModList(newConf, req.params))); + }, function (err) { + res.statusCode = err.statusCode || 500; + err.message = err.message || err.toString(); + res.end(JSON.stringify({error: {message: err.message, code: err.code}})); + }); + }; + config.restful.createDomain = function (req, res) { deps.PromiseA.resolve().then(function () { var changer = new (require('./config').ConfigChanger)(conf); @@ -497,13 +541,37 @@ module.exports.create = function (deps, conf) { update = [ update ]; } update.forEach(changer.domains.add, changer.domains); - - var errors = changer.validate(); - if (errors.length) { - throw Object.assign(new Error(), errors[0], {statusCode: 400}); + return config.save(changer); + }).then(function (config) { + res.send(deps.recase.snakeCopy(config.domains)); + }, function (err) { + res.statusCode = err.statusCode || 500; + err.message = err.message || err.toString(); + res.end(JSON.stringify({error: {message: err.message, code: err.code}})); + }); + }; + config.restful.updateDomain = function (req, res) { + deps.PromiseA.resolve().then(function () { + if (req.body.modules) { + throw Object.assign(new Error('do not add modules with this route'), {statusCode: 400}); } - return deps.storage.config.save(changer); + var changer = new (require('./config').ConfigChanger)(conf); + changer.domains.update(req.params.domId, req.body); + return config.save(changer); + }).then(function (config) { + res.send(deps.recase.snakeCopy(config.domains)); + }, function (err) { + res.statusCode = err.statusCode || 500; + err.message = err.message || err.toString(); + res.end(JSON.stringify({error: {message: err.message, code: err.code}})); + }); + }; + config.restful.removeDomain = function (req, res) { + deps.PromiseA.resolve().then(function () { + var changer = new (require('./config').ConfigChanger)(conf); + changer.domains.remove(req.params.domId); + return config.save(changer); }).then(function (config) { res.send(deps.recase.snakeCopy(config.domains)); }, function (err) { @@ -521,18 +589,28 @@ module.exports.create = function (deps, conf) { app.use('/', isAuthorized, jsonParser); - app.use( '/config', makeCorsHandler()); + // Not all config routes support PUT or DELETE, but not worth making this more specific + app.use( '/config', makeCorsHandler(['GET', 'POST', 'PUT', 'DELETE'])); app.get( '/config', config.restful.readConfig); app.get( '/config/:group', config.restful.readConfig); app.get( '/config/:group/:mod(modules)/:modId?', config.restful.readConfig); app.get( '/config/domains/:domId/:mod(modules)?', config.restful.readConfig); app.get( '/config/domains/:domId/:mod(modules)/:modGrp/:modId?', config.restful.readConfig); - app.post( '/config', config.restful.saveBaseConfig); - app.post( '/config/:group(?!domains)', config.restful.saveBaseConfig); - app.post( '/config/:group(?!domains)/modules', config.restful.createModule); - app.post( '/config/domains', config.restful.createDomain); - app.post( '/config/domains/:domId/modules/:group',config.restful.createModule); + app.post( '/config', config.restful.saveBaseConfig); + app.post( '/config/:group', config.restful.saveBaseConfig); + + app.post( '/config/:group/modules', config.restful.createModule); + app.put( '/config/:group/modules/:modId', config.restful.updateModule); + app.delete('/config/:group/modules/:modId', config.restful.removeModule); + + app.post( '/config/domains/:domId/modules/:group', config.restful.createModule); + app.put( '/config/domains/:domId/modules/:group/:modId', config.restful.updateModule); + app.delete('/config/domains/:domId/modules/:group/:modId', config.restful.removeModule); + + app.post( '/config/domains', config.restful.createDomain); + app.put( '/config/domains/:domId', config.restful.updateDomain); + app.delete('/config/domains/:domId', config.restful.removeDomain); return app; }; diff --git a/lib/admin/config.js b/lib/admin/config.js index 0cf8646..26dcbb7 100644 --- a/lib/admin/config.js +++ b/lib/admin/config.js @@ -207,20 +207,54 @@ function validate(config) { } module.exports.validate = validate; - -class ModuleList extends Array { +class IdList extends Array { constructor(rawList) { super(); if (Array.isArray(rawList)) { Object.assign(this, JSON.parse(JSON.stringify(rawList))); } + this._itemName = 'item'; } - find(id) { - return Array.prototype.find.call(this, function (mod) { - return mod.id === id; + findId(id) { + return Array.prototype.find.call(this, function (dom) { + return dom.id === id; }); } + + add(item) { + item.id = require('crypto').randomBytes(4).toString('hex'); + this.push(item); + } + + update(id, update) { + var item = this.findId(id); + if (!item) { + var error = new Error("no "+this._itemName+" with ID '"+id+"'"); + error.statusCode = 404; + throw error; + } + Object.assign(this.findId(id), update); + } + + remove(id) { + var index = this.findIndex(function (dom) { + return dom.id === id; + }); + if (index < 0) { + var error = new Error("no "+this._itemName+" with ID '"+id+"'"); + error.statusCode = 404; + throw error; + } + this.splice(index, 1); + } +} +class ModuleList extends IdList { + constructor(rawList) { + super(rawList); + this._itemName = 'module'; + } + add(mod) { if (!mod.type) { throw new Error("module must have a 'type' defined"); @@ -233,12 +267,10 @@ class ModuleList extends Array { this.push(mod); } } -class DomainList extends Array { +class DomainList extends IdList { constructor(rawList) { - super(); - if (Array.isArray(rawList)) { - Object.assign(this, JSON.parse(JSON.stringify(rawList))); - } + super(rawList); + this._itemName = 'domain'; this.forEach(function (dom) { dom.modules = { http: new ModuleList((dom.modules || {}).http), @@ -247,11 +279,6 @@ class DomainList extends Array { }); } - find(id) { - return Array.prototype.find.call(this, function (dom) { - return dom.id === id; - }); - } add(dom) { if (!Array.isArray(dom.names) || !dom.names.length) { throw new Error("domains must have a non-empty array for 'names'");