diff --git a/.env_example b/.env_example new file mode 100644 index 000000000..f0cfe5832 --- /dev/null +++ b/.env_example @@ -0,0 +1,7 @@ +PORT=1337 +NODE_ENV=production +KONGA_HOOK_TIMEOUT=120000 +DB_ADAPTER=postgres +DB_URI=postgresql://localhost:5432/konga +KONGA_LOG_LEVEL=warn +TOKEN_SECRET=some_secret_token diff --git a/.gitignore b/.gitignore index 922e80fcf..9422eeac7 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,6 @@ nbproject /config/mssqlconfig.js /www/ /kongadata/ +/certs/ +/.env +.env diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c83932c..6f73c9fb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to this project will be documented in this file. +## [0.12.1](https://github.com/pantsel/konga/releases/tag/0.12.1) - 28-07-2018 +* **[Deprecation]** Deprecated consumer imports. This feature was not adopted and provided unnecessary +complexity to maintenance as well as increased the overall project's size. +* **[Fix]** Fixed the trailing slash issue. Konga is now able to communicate with +Kong even if a trailing slash exists in the connection url. +* Cleaned up unused dependencies. +* When installing Konga from source, the `confing/local.js` file is deprecated +in favor of a `.env` file. Check the README.md for details. + ## [0.12.0](https://github.com/pantsel/konga/releases/tag/0.12.0) - 07-07-2018 * **[Fix]** Fix snapshots implementation. Use auto generated entity ids for proper relationships mapping. * **[Compatibility]** Implement new Kong plugins properly. diff --git a/README.md b/README.md index a663e6c02..86b78f1fe 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ _Konga is not an official app. No affiliation with [Kong](https://www.konghq.com - [**Used libraries**](#used-libraries) - [**Installation**](#installation) - [**Configuration**](#configuration) +- [**Environment variables**](#environment-variables) - [**Running Konga**](#running-konga) - [**Upgrading**](#upgrading) - [**FAQ**](#faq) @@ -51,37 +52,58 @@ It also works with Kong 0.13.* yet without the ability to manage services and ro ## Prerequisites - A running [Kong installation](https://getkong.org/) -- Nodejs +- Nodejs >= 8 (8.11.3 LTS is recommended) - Npm ## Used libraries * Sails.js, http://sailsjs.org/ * AngularJS, https://angularjs.org/ -* Bootstrap, http://getbootstrap.com/ ## Installation Install `npm` and `node.js`. Instructions can be found [here](http://sailsjs.org/#/getStarted?q=what-os-do-i-need). -Install `bower`, `gulp` and `sails` packages. +Install `bower`, ad `gulp` packages. ``` $ git clone https://github.com/pantsel/konga.git $ cd konga -$ npm install +$ npm i ``` ## Configuration You can configure your application to use your environment specified settings. -There is an example configuration file on following path. +There is an example configuration file on the root folder. ``` -config/local_example.js +.env_example ``` -Just copy this to `config/local.js` and make necessary changes to it. Note that this -`local.js` file is in .gitignore so it won't go to VCS at any point. +Just copy this to `.env` and make necessary changes to it. Note that this +`.env` file is in .gitignore so it won't go to VCS at any point. + +## Environment variables +These are the general environment variables Konga uses. + +| VAR | DESCRIPTION | VALUES | DEFAULT | +|--------------------|----------------------------------------------------------------------------------------------------------------------------|----------------------------------------|----------------------------------------------| +| PORT | The port that will be used by Konga's server | - | 1337 | +| NODE_ENV | The environment | `production`,`development` | `development` | +| SSL_KEY_PATH | If you want to use SSL, this will be the absolute path to the .key file. Both `SSL_KEY_PATH` & `SSL_CRT_PATH` must be set. | - | null | +| SSL_CRT_PATH | If you want to use SSL, this will be the absolute path to the .crt file. Both `SSL_KEY_PATH` & `SSL_CRT_PATH` must be set. | - | null | +| KONGA_HOOK_TIMEOUT | The time in ms that Konga will wait for startup tasks to finish before exiting the process. | - | 60000 | +| DB_ADAPTER | The database that Konga will use. If not set, the localDisk db will be used. | `mongo`,`mysql`,`postgres`,`sqlserver` | - | +| DB_URI | The full db connection string. Depends on `DB_ADAPTER`. If this is set, no other DB related var is needed. | - | - | +| DB_HOST | If `DB_URI` is not specified, this is the database host. Depends on `DB_ADAPTER`. | - | localhost | +| DB_PORT | If `DB_URI` is not specified, this is the database port. Depends on `DB_ADAPTER`. | - | DB default. | +| DB_USER | If `DB_URI` is not specified, this is the database user. Depends on `DB_ADAPTER`. | - | - | +| DB_PASSWORD | If `DB_URI` is not specified, this is the database user's password. Depends on `DB_ADAPTER`. | - | - | +| DB_DATABASE | If `DB_URI` is not specified, this is the name of Konga's db. Depends on `DB_ADAPTER`. | - | `konga_database` | +| DB_PG_SCHEMA | If using postgres as a database, this is the schema that will be used. | - | `public` | +| KONGA_LOG_LEVEL | The logging level | `silly`,`debug`,`info`,`warn`,`error` | `debug` on dev environment & `warn` on prod. | +| TOKEN_SECRET | The secret that will be used to sign JWT tokens issued by Konga | - | - | + ### Databases Integration @@ -94,45 +116,42 @@ The application also supports some of the most popular databases out of the box: 1. MySQL 2. MongoDB 3. PostgresSQL -4. SQL Server -In order to use them, in your `/config/local.js` replace -``` -models: { - connection: process.env.DB_ADAPTER || 'localDiskDb', -} -``` -with +In order to use them, set the appropriate env vars in your `.env` file. + + +## Running Konga + +### Development ``` -models: { - connection: process.env.DB_ADAPTER || 'the-name-of-adapter-you-wish-to-use', // 'mysql', 'mongo', 'sqlserver' or 'postgres' -} +$ npm start ``` +Konga GUI will be available at `http://localhost:1337` -See [Sails adapters](http://sailsjs.com/documentation/concepts/extending-sails/adapters/available-adapters) for further configuration +### Production ***************************************************************************************** -##### Note : -In case of `MySQL`, `PostgresSQL` or `SQL Server` adapters, -you will need to prepare the database as explained on the next topic. +In case of `MySQL` or `PostgresSQL` adapters, Konga will not perform db migrations when running in production mode. -***************************************************************************************** +You can manually perform the migrations by calling ```$ node ./bin/konga.js prepare``` +, passing the args needed for the database connectivity. -## Running Konga +For example: -### Development ``` -$ npm start +$ node ./bin/konga.js prepare --adapter postgres --uri postgresql://localhost:5432/konga ``` -Konga GUI will be available at `http://localhost:1337` +The process will exit after all migrations are completed. -### Production +***************************************************************************************** +Finally: ``` $ npm run production ``` Konga GUI will be available at `http://localhost:1337` + ### Production Docker Image The following instructions assume that you have a running Kong instance following the @@ -143,6 +162,7 @@ $ docker run -p 1337:1337 \ --network {{kong-network}} \ // optional --name konga \ -e "NODE_ENV=production" \ // or "development" | defaults to 'development' + -e "TOKEN_SECRET={{somerandomstring}}" \ pantsel/konga ``` @@ -151,20 +171,7 @@ $ docker run -p 1337:1337 \ 1. ##### Prepare the database > **Note**: You can skip this step if using the `mongo` adapter. -Konga will not perform db migrations when running in production mode. - -You can manually perform the migrations by calling ```$ node ./bin/konga.js prepare``` -, passing the args needed for the database connectivity. - -The available adapters are ```'postgres', or 'mysql'``` - -``` -$ node ./bin/konga.js prepare --adapter {adapter_name} --uri {full_connection_string} -``` -The process will exit after all migrations are completed. - -If you're deploying Konga via the docker image, you can prepare the database using -an ephemeral container running the prepare command. +You can prepare the database using an ephemeral container that runs the prepare command. **Args** @@ -173,10 +180,9 @@ argument | description | default -c | command | - -a | adapter (can be `postgres` or `mysql`) | - -u | full database connection url | - --p | port | `1339` ``` -$ docker run --rm pantsel/konga:next -c prepare -a {{adapter}} -u {{connection-uri}} -p {{port}} +$ docker run --rm pantsel/konga:next -c prepare -a {{adapter}} -u {{connection-uri}} ``` @@ -184,6 +190,7 @@ $ docker run --rm pantsel/konga:next -c prepare -a {{adapter}} -u {{connection-u ``` $ docker run -p 1337:1337 --network {{kong-network}} \ // optional + -e "TOKEN_SECRET={{somerandomstring}}" \ -e "DB_ADAPTER=the-name-of-the-adapter" \ // 'mongo','postgres','sqlserver' or 'mysql' -e "DB_HOST=your-db-hostname" \ -e "DB_PORT=your-db-port" \ // Defaults to the default db port @@ -199,6 +206,7 @@ $ docker run -p 1337:1337 // Alternatively you can use the full connection string to connect to a database $ docker run -p 1337:1337 --network {{kong-network}} \ // optional + -e "TOKEN_SECRET={{somerandomstring}}" \ -e "DB_ADAPTER=the-name-of-the-adapter" \ // 'mongo','postgres','sqlserver' or 'mysql' -e "DB_URI=full-conection-uri" \ -e "NODE_ENV=production" \ // or 'development' | defaults to 'development' @@ -217,7 +225,10 @@ login: admin | password: adminadminadmin *Demo user* login: demo | password: demodemodemo -This user data is populated to the database if there is not already any user data in it. [It is possible to alter the default user seed data.](DEFAULTUSERSEEDDATA.md) +This user data is populated to the database if there is not already any user data in it. [It is possible to alter the default user seed data.](./docs/DEFAULTUSERSEEDDATA.md) + +You may also configure Konga to authenticate via [LDAP](./docs/LDAP.md). + ## Upgrading In some cases a newer version of Konga may introduce new db tables, collections or changes in schemas. diff --git a/api/controllers/KongProxyController.js b/api/controllers/KongProxyController.js index d83e0f10e..bc6e8985d 100644 --- a/api/controllers/KongProxyController.js +++ b/api/controllers/KongProxyController.js @@ -6,6 +6,7 @@ var unirest = require("unirest"); var KongService = require("../services/KongService"); var ProxyHooks = require("../services/KongProxyHooks"); var _ = require("lodash"); +var Utils = require('../services/Utils'); function getEntityFromRequest(req) { @@ -44,6 +45,7 @@ var self = module.exports = { }); } + sails.log("Kong admin url =>", req.connection.kong_admin_url); var request = unirest[req.method.toLowerCase()](req.connection.kong_admin_url + req.url) diff --git a/api/controllers/RemoteStorageController.js b/api/controllers/RemoteStorageController.js deleted file mode 100644 index 09aa737e7..000000000 --- a/api/controllers/RemoteStorageController.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -'use strict' - -var unirest = require('unirest'); -var RemoteStorageService = require('../services/remote/RemoteStorageService') -var adapters = require('../services/remote/adapters') - -/** - * RemoteStorageController - */ -var RemoteStorageController = { - - loadAdapters : function(req,res) { - return adapters.getSchemas(req,res) - }, - - testConnection : function(req,res) { - - //return new RemoteStorageService(req.adapter).loadConsumers(req,res) - //var connection = mysql.createConnection({ - // host : req.query.host || '', - // user : req.query.user || 'root', - // password : req.query.password || '', - // database : req.query.database || '' - //}); - //connection.connect(function(err) { - // if (err) return res.negotiate(err); - // connection.query('SELECT 1 from ' + req.query.table, function(err, rows, fields) { - // if (err) return res.negotiate(err); - // return res.json(rows); - // }); - //}); - }, - - loadConsumers : function(req,res) { - return RemoteStorageService.loadConsumers(req,res) - }, -}; - -module.exports = RemoteStorageController; diff --git a/api/helpers/utils.js b/api/helpers/utils.js index 57443eebc..f3dd2e8d2 100644 --- a/api/helpers/utils.js +++ b/api/helpers/utils.js @@ -72,6 +72,10 @@ module.exports = { } return version; - } + }, + withoutTrailingSlash(str) { + if(!str) return str; + return str.replace(/\/$/, "") + }, } \ No newline at end of file diff --git a/api/policies/authenticated.js b/api/policies/authenticated.js index 54dc09421..2c21e0465 100644 --- a/api/policies/authenticated.js +++ b/api/policies/authenticated.js @@ -25,7 +25,7 @@ module.exports = function authenticated(request, response, next) { */ var verify = function verify(error, token) { if (!(_.isEmpty(error) && token !== -1)) { - return response.json(401, {message: 'Given authorization token is not valid'}); + return response.json(401, {message: 'Given authorization token is not valid', logout: true}); } else { // Store user id to request object request.token = token; @@ -42,6 +42,6 @@ module.exports = function authenticated(request, response, next) { try { sails.services.token.getToken(request, verify, true); } catch (error) { - return response.json(401, {message: error.message}); + return response.json(401, {message: error.message, logout: true}); } }; diff --git a/api/policies/dynamicNode.js b/api/policies/dynamicNode.js index 3be590ad2..d35a92580 100644 --- a/api/policies/dynamicNode.js +++ b/api/policies/dynamicNode.js @@ -11,42 +11,46 @@ var _ = require('lodash'); */ module.exports = function dynamicNode(request, response, next) { - if(request.headers['connection-id'] || request.query.connection_id) { - - sails.log.debug("Policy:dynamicNode", "`connection-id` is defined."); - - sails.models.kongnode.findOne(request.headers['connection-id'] || request.query.connection_id) - .exec(function(err,node) { - if (err) return next(err); - if (!node) return response.notFound({ - message: "connection not found" - }) - - request.connection = node; - - return next(); - }); - - }else{ - // Get the default node from user - sails.models.user.findOne({ - id:request.token - }).populate('node').exec(function(err,user) { - if(err) return next(err); - if(!user) return response.notFound({ - message : "user not found" - }) - - if(user.node) { - // sails.config.kong_admin_url = user.node.kong_admin_url - request.connection = user.node; - return next(); - }else{ - return response.badRequest({ - message : "No connection is selected. Please activate a connection in settings" - }); - } + if (request.headers['connection-id'] || request.query.connection_id) { + + sails.log.debug("Policy:dynamicNode", "`connection-id` is defined."); + + sails.models.kongnode.findOne(request.headers['connection-id'] || request.query.connection_id) + .exec(function (err, node) { + if (err) return next(err); + if (!node) return response.notFound({ + message: "connection not found" + }) + + // Remove trailing slash from kong_admin_url property + _.update(node, 'kong_admin_url', function(o) { return o.replace(/\/$/, ""); }); + + request.connection = node; + + return next(); + }); + + } else { + // Get the default node from user + sails.models.user.findOne({ + id: request.token + }).populate('node').exec(function (err, user) { + if (err) return next(err); + if (!user) return response.notFound({ + message: "user not found" + }) + + if (user.node) { + // Remove trailing slash from kong_admin_url property + _.update(user.node, 'kong_admin_url', function(o) { return o.replace(/\/$/, ""); }); + request.connection = user.node; + return next(); + } else { + return response.badRequest({ + message: "No connection is selected. Please activate a connection in settings" }); - } + } + }); + } }; diff --git a/api/services/KongService.js b/api/services/KongService.js index e790f5924..393754def 100644 --- a/api/services/KongService.js +++ b/api/services/KongService.js @@ -3,6 +3,7 @@ var unirest = require("unirest") var ApiHealthCheckService = require('../services/ApiHealthCheckService') var JWT = require("./Token"); +var Utils = require('../helpers/utils'); var KongService = { @@ -33,7 +34,7 @@ var KongService = { create: function (req, res) { - unirest.post(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.post(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .send(req.body) .end(function (response) { @@ -44,7 +45,7 @@ var KongService = { createCb: function (req, res, cb) { - unirest.post(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.post(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .send(req.body) .end(function (response) { @@ -55,7 +56,7 @@ var KongService = { createFromEndpointCb: function (endpoint, data, req, cb) { - unirest.post(req.connection.kong_admin_url + endpoint) + unirest.post(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + endpoint) .headers(KongService.headers(req, true)) .send(data) .end(function (response) { @@ -65,8 +66,8 @@ var KongService = { }, deleteFromEndpointCb: function (endpoint, req, cb) { - sails.log('Deleting ' + req.connection.kong_admin_url + endpoint); - unirest.delete(req.connection.kong_admin_url + endpoint) + sails.log('Deleting ' + Utils.withoutTrailingSlash(req.connection.kong_admin_url) + endpoint); + unirest.delete(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + endpoint) .headers(KongService.headers(req, true)) .end(function (response) { if (response.error) return cb(response) @@ -76,7 +77,7 @@ var KongService = { retrieve: function (req, res) { - unirest.get(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.get(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .end(function (response) { if (response.error) return res.kongError(response); @@ -87,7 +88,7 @@ var KongService = { fetch: (endpoint,req) => { return new Promise((resolve, reject) => { - unirest.get(req.connection.kong_admin_url + endpoint) + unirest.get(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + endpoint) .header('Content-Type', 'application/json') .end(function (response) { if (response.error) return reject(response) @@ -99,7 +100,7 @@ var KongService = { nodeStatus: function (node, cb) { - unirest.get(node.kong_admin_url + "/status") + unirest.get(Utils.withoutTrailingSlash(node.kong_admin_url) + "/status") .headers(KongService.headers(node, true)) .end(function (response) { if (response.error) return cb(response); @@ -109,7 +110,7 @@ var KongService = { nodeInfo: function (node, cb) { - unirest.get(node.kong_admin_url) + unirest.get(Utils.withoutTrailingSlash(node.kong_admin_url)) .headers(KongService.headers(node, true)) .end(function (response) { if (response.error) return cb(response); @@ -133,7 +134,7 @@ var KongService = { } }); }; - getData([], (req.kong_admin_url || req.connection.kong_admin_url) + endpoint); + getData([], (Utils.withoutTrailingSlash(req.kong_admin_url) || Utils.withoutTrailingSlash(req.connection.kong_admin_url)) + endpoint); }, @@ -153,11 +154,11 @@ var KongService = { } }); }; - getData([], (req.kong_admin_url || req.connection.kong_admin_url) + req.url.replace('/kong', '')); + getData([], (Utils.withoutTrailingSlash(req.kong_admin_url) || Utils.withoutTrailingSlash(req.connection.kong_admin_url)) + req.url.replace('/kong', '')); }, update: function (req, res) { - unirest.patch(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.patch(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .send(req.body) .end(function (response) { @@ -176,7 +177,7 @@ var KongService = { }, updateCb: function (req, res, cb) { - unirest.patch(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.patch(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .send(req.body) .end(function (response) { @@ -196,7 +197,7 @@ var KongService = { }, updateOrCreate: function (req, res) { - unirest.put(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.put(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .send(req.body) .end(function (response) { @@ -206,7 +207,7 @@ var KongService = { }, delete: function (req, res) { - unirest.delete(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.delete(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .end(function (response) { if (response.error) return res.kongError(response); @@ -227,7 +228,7 @@ var KongService = { }, deleteCb: function (req, res, cb) { - unirest.delete(req.connection.kong_admin_url + req.url.replace('/kong', '')) + unirest.delete(Utils.withoutTrailingSlash(req.connection.kong_admin_url) + req.url.replace('/kong', '')) .header('Content-Type', 'application/json') .end(function (response) { if (response.error) return cb(response); diff --git a/api/services/Passport.js b/api/services/Passport.js index b0ea0a959..aed143258 100644 --- a/api/services/Passport.js +++ b/api/services/Passport.js @@ -25,6 +25,8 @@ */ // Module dependencies +var LdapStrategy = require('passport-ldapauth'); +var ldapConf = require("../../config/ldap"); var passport = require('passport'); var path = require('path'); var url = require('url'); @@ -198,10 +200,14 @@ passport.endpoint = function endpoint(request, response) { passport.callback = function callback(request, response, next) { sails.log.verbose(__filename + ':' + __line + ' [Service.Passport.callback() called]'); - var provider = request.param('provider', 'local'); + var provider = request.param('provider', process.env.KONGA_AUTH_PROVIDER || 'local'); var action = request.param('action'); - if (provider === 'local' && action !== undefined) { + if (provider === 'ldap') { + passport.use(new LdapStrategy(ldapConf)); + this.authenticate('ldapauth', + this.protocols.ldap.getResolver(next))(request, response, response.next); + } else if (provider === 'local' && action !== undefined) { if (action === 'connect' && request.user) { this.protocols.local.connect(request, response, next); } else { diff --git a/api/services/SnapshotsService.js b/api/services/SnapshotsService.js index 117ab5e0c..8c0a697b0 100644 --- a/api/services/SnapshotsService.js +++ b/api/services/SnapshotsService.js @@ -215,7 +215,7 @@ module.exports = { sails.models.snapshot.create({ name: name || "snap@" + Date.now(), kong_node_name: node.name, - kong_node_url: node.kong_admin_url, + kong_node_url: Utils.withoutTrailingSlash(node.kong_admin_url), kong_version: status.version, data: result }).exec(function (err, created) { diff --git a/api/services/Utils.js b/api/services/Utils.js index b566f127a..ad9dfc3b3 100644 --- a/api/services/Utils.js +++ b/api/services/Utils.js @@ -1,14 +1,20 @@ module.exports = { - Object : { - clean : function clean(obj) { - for(var key in obj) { - if(JSON.stringify(obj[key])=="{}" || !obj[key]) { - delete obj[key]; - } else if (typeof obj[key] == "object") { - obj[key] = this.clean(obj[key]); - } - } - return obj; + Object: { + clean: function clean(obj) { + for (var key in obj) { + if (JSON.stringify(obj[key]) == "{}" || !obj[key]) { + delete obj[key]; + } else if (typeof obj[key] == "object") { + obj[key] = this.clean(obj[key]); } + } + return obj; } + }, + + isRuntimeVersionSupported() { + const minRequiredNodeVersion = '8.0.0'; + const semver = require('semver'); + return semver.gte(process.versions.node, minRequiredNodeVersion); + } } diff --git a/api/services/protocols/index.js b/api/services/protocols/index.js index 4f807f05f..e2f2c05c4 100644 --- a/api/services/protocols/index.js +++ b/api/services/protocols/index.js @@ -16,6 +16,7 @@ module.exports = { local: require('./local'), oauth: require('./oauth'), + ldap: require('./ldap'), oauth2: require('./oauth2'), openid: require('./openid') }; diff --git a/api/services/protocols/ldap.js b/api/services/protocols/ldap.js new file mode 100644 index 000000000..2bf114da5 --- /dev/null +++ b/api/services/protocols/ldap.js @@ -0,0 +1,93 @@ +var _ = require('lodash'); +var adminGroup = new RegExp(process.env.KONGA_ADMIN_GROUP_REG || "^(admin|konga)$"); +var ldapAttrMap = { + username: process.env.KONGA_LDAP_ATTR_USERNAME || 'uid', + firstName: process.env.KONGA_LDAP_ATTR_FIRSTNAME || 'givenName', + lastName: process.env.KONGA_LDAP_ATTR_LASTNAME || 'sn', + email: process.env.KONGA_LDAP_ATTR_EMAIL || 'mail' +}; +var commonName = /^cn=([^,]+),.*/; + +var ldapToUser = function (ldapUser, user, next) { + var data = _.clone(user || {}); + data.active = true; + + // copy attributes from the ldap user to the konga user using the ldapAttrMap + for (var userAttr in ldapAttrMap) { + if (ldapAttrMap.hasOwnProperty(userAttr)) { + data[userAttr] = ldapUser[ldapAttrMap[userAttr]]; + } + } + + if (data && data.id) { + sails.models.user.update({id: data.id}, data).exec(function(err) { + if (err) { + console.error("Failed to update user from ldap", err); + next(err); + } else { + setAdminStatus(ldapUser, data, next); + } + }); + } else { + sails.models.user.create(data).exec(function (err, user) { + if (err) { + console.error("Failed to create user from ldap", err); + next(err); + } else { + setAdminStatus(ldapUser, user, next); + } + }); + } +} + +var group_test = function (group) { + return adminGroup.test(group.cn); +} + +var member_test = function (group) { + return adminGroup.test(commonName.replace(group, "$1")); +} + +var setAdminStatus = function (ldapUser, user, next) { + user.admin = + _.findIndex(ldapUser._groups, group_test) > -1 || + _.findIndex(ldapUser.memberOf, member_test) > -1; + next(null, user); +} + +/** + * Resolve LDAP user + * + * This function can be used to create a user in the local db to store + * users' roles locally + * + * @param {Request} request + * @param {Response} response + * @param {Function} next + */ +exports.getResolver = function getResolver(next) { + return function resolveUser(err, result, message) { + if (result === false || typeof result === 'undefined') { + console.error('failed to resolve user', err, message); + var error = message; + next(error); + } else { + var ldapUser = result; + sails.models.user + .findOne({ // UID is the default, but the LDAP provider could be ActiveDirectory + username: (ldapUser.uid || ldapUser.sAMAccountName) + }) + .populate('node') + .exec(function onExec(error, user) { + if (error) { + // Dunno, something bad happened + console.error('failed to look up existing user', error); + next(error); + } else { + // sync the ldap user to konga user + ldapToUser(ldapUser, user, next); + } + }) + } + }; +} diff --git a/api/services/remote/RemoteStorageService.js b/api/services/remote/RemoteStorageService.js deleted file mode 100644 index 4d87bf07f..000000000 --- a/api/services/remote/RemoteStorageService.js +++ /dev/null @@ -1,6 +0,0 @@ -var RemoteStorageService = { - loadConsumers : function(req,res) { - return require('./adapters')[req.body.adapter].methods.loadConsumers(req,res); - } -} -module.exports = RemoteStorageService diff --git a/api/services/remote/adapters/api.js b/api/services/remote/adapters/api.js deleted file mode 100644 index 9c64f82bf..000000000 --- a/api/services/remote/adapters/api.js +++ /dev/null @@ -1,73 +0,0 @@ -var unirest = require('unirest') - -module.exports = { - enabled : true, - schema : { - "name": "API", - "value": "api", - "description": "Import Consumers by issuing a GET request to an API endpoint", - "form_fields": { - "connection": { - "endpoint": { - "name": "endpoint", - "required" : true, - "type": "text", - "description": "The API endpoint. ex: http://myapi.io/users?limit=3000" - }, - "headers": { - "name": "headers", - "type": "text", - "description": "A comma separated list of headers to send with the request. ex: Authorization:Bearer some-token-key,Content-type:application/json" - } - }, - "consumer": { - "data_property" : { - "name": "Data property", - "type": "text", - "description": "If specified, the consumers will be extracted from this resulting object's property." - }, - "username": { - "name": "username field", - "type": "text", - "required": true, - "description": "The property that will be used as the consumer username." - }, - "custom_id": { - "name": "custom_id field", - "type": "text", - "required": true, - "description": "The property will be used as the consumer custom_id." - } - } - } - }, - methods : { - loadConsumers : function(req,res) { - - var headers = {} - if(req.param('headers')) { - var string = req.param('headers') - var split = string.split(",") - split.forEach(function(keyVal){ - var array = keyVal.split(":") - headers[array[0]] = array[1] - }) - - sails.log("Headers =>",headers) - } - - var request = unirest.get(req.param('endpoint')) - request.headers(headers) - request.end(function (response) { - if (response.error) return res.negotiate(response) - - sails.log("response.body",response.body) - - var jsonRes = response.body - - return res.json(req.param('data_property') ? jsonRes[req.param('data_property')] : jsonRes) - }) - } - } - -} diff --git a/api/services/remote/adapters/csv.js b/api/services/remote/adapters/csv.js deleted file mode 100644 index 78a132150..000000000 --- a/api/services/remote/adapters/csv.js +++ /dev/null @@ -1,125 +0,0 @@ -var csv = require('csv-parser') -var fs = require('fs') - -module.exports = { - enabled : true, - schema : { - "name": "CSV", - "value": "csv", - "hasFiles" : true, - "description": "Import Consumers from a .csv document", - "form_fields": { - "connection": { - "file" : { - "name" : "File", - "type" : "file", - "required": true, - "description" : "Select the .csv document containing the consumers" - }, - "raw" : { - name : "raw", - "type" : "boolean", - "default" : false, - description : "Whether or not to decode to utf-8 strings (optional)." - }, - "separator" : { - name : "separator", - "type" : "text", - description : "Specify optional cell separator. Defaults to ','" - }, - "quote" : { - name : "quote", - "type" : "text", - description : "Specify optional quote character. Defaults to '\"'" - }, - "escape" : { - name : "escape", - "type" : "text", - description : "Specify optional escape character. Defaults to quote value" - }, - "newline" : { - name : "newline", - "type" : "text", - description : "Specify a newline character. Defaults to '\\n'" - }, - "strict" : { - name : "strict", - "type" : "boolean", - "default" : true, - description : "Require column length match headers length (optional)." - }, - - "headers" : { - name : "headers", - "type" : "text", - "required": true, - "description" : "Specify the headers of each .csv row as a comma separated string." + - " ex: 'id,name,email,updated_at,created_at...'" - } - }, - "consumer": { - "username": { - "name": "username column", - "type": "text", - "required": true, - "description": "The header of the cell that will be used as the consumer username." - }, - "custom_id": { - "name": "custom_id field", - "type": "text", - "required": true, - "description": "The header of the cell that will be used as the consumer custom_id." - } - } - } - }, - methods : { - loadConsumers : function(req,res) { - - req.file('file').upload(function (err, uploadFiles) { - - if(err) return res.negotiate(err); - - if(!uploadFiles.length) return res.badRequest("No files uploaded") - - var result = []; - - fs.createReadStream(uploadFiles[0].fd) - .pipe(csv({ - raw: req.body.raw || false, // do not decode to utf-8 strings - separator: req.body.separator || ',', // specify optional cell separator - quote: req.body.quote || '"', // specify optional quote character - escape: req.body.escape || '"', // specify optional escape character (defaults to quote value) - newline: req.body.newline || '\n', // specify a newline character - strict: req.body.strict || true, // require column length match headers length - headers: req.body.headers.split(",") // Specifing the headers - })) - .on('data', function (data) { - var consumer = {} - Object.keys(data).forEach(function(key){ - - if(key === req.body.username) { - consumer.username = data[key] - } - - if(key === req.body.custom_id) { - consumer.custom_id = data[key] - } - }) - - if(Object.keys(data).length === 2) - result.push(consumer); - }) - .on('end', function () { - if(!result.length) - return res.notFound("No consumers found"); - return res.json(result) - }) - - }); - - - } - } - -} diff --git a/api/services/remote/adapters/google-spreadsheet.js b/api/services/remote/adapters/google-spreadsheet.js deleted file mode 100644 index af8b72a1c..000000000 --- a/api/services/remote/adapters/google-spreadsheet.js +++ /dev/null @@ -1,143 +0,0 @@ -var fs = require('fs'); -var google = require('googleapis'); -var path = require('path') -var mkdirp = require('mkdirp'); - -module.exports = { - enabled : true, - schema : { - "name": "Google Spreadsheet", - "value": "google-spreadsheet", - "hasFiles" : true, - "description": "Import Consumers from Google spreadsheets. ", - "info" : "Konga uses the JWT method to authenticate with Google spreadsheets API." + - "Make sure you have created Service account JSON credentials for Konga " + - "and that your spreadsheet is shared with that service account's email.", - "form_fields": { - "connection": { - "file" : { - "name" : "JSON credentials file", - "type" : "file", - "description" : "The Service account JSON credentials file." + - "This file only needs to be uploaded once as long as the service account remains as is." - }, - "spreadsheetId": { - "name": "spreadsheetId", - "type": "text", - "required" : true, - "description": "The long id in the sheets URL" - }, - "range": { - "name": "range", - "type": "text", - "required" : true, - "description": "The range to read, in A1 notation. ex. A1:B15" - }, - "ignore_first_row": { - "name": "Ignore first row", - "type": "boolean", - "description": "Whether or not to ignore the first row of the spreadsheet." + - " This can be useful in case it contains the column titles." - }, - }, - "consumer": { - "username": { - "name": "username column", - "type": "number", - "required": true, - "description": "The index of the spreadsheet column that will be used as the consumer username. The first column is 0." - }, - "custom_id": { - "name": "custom_id column", - "type": "number", - "required": true, - "description": "The index of the spreadsheet column that will be used as the consumer custom_id. The first column is 0." - } - } - } - }, - methods : { - loadConsumers : function(req,res) { - - req.file('file').upload(function (err, uploadFiles) { - if (err) return res.negotiate(err); - - var CREDENTIALS_PATH = path.join(__dirname,"..","credentials/google-spreadsheets"); - - mkdirp(CREDENTIALS_PATH, function (err) { - if (err) return res.negotiate(err) - - if(!uploadFiles.length && !fs.existsSync(CREDENTIALS_PATH + "/credentials.json")) { - if (err) return res.notFound("Credentials JSON file not found. You will need to upload one."); - } - - if(uploadFiles[0]) { - fs.renameSync(uploadFiles[0].fd, CREDENTIALS_PATH + "/credentials.json"); - } - - var SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']; - - fs.readFile(CREDENTIALS_PATH + "/credentials.json", function(err, content) { - if (err) if (err) return res.negotiate(err); - - var key = JSON.parse(content); - - - var jwtClient = new google.auth.JWT( - key.client_email, - null, - key.private_key, - SCOPES, - null - ); - - jwtClient.authorize(function (err, tokens) { - if (err) return res.negotiate(err) - listConsumers(jwtClient) - }); - - function listConsumers(auth) { - var sheets = google.sheets('v4'); - sheets.spreadsheets.values.get({ - auth: auth, - spreadsheetId: req.body.spreadsheetId, - range : req.body.range, - }, function(err, response) { - if (err) return res.negotiate(err) - - //var rows = response.values; - if (response.length == 0 || response.values.length == 0) { - return res.notFound('No data found.') - } else { - //if(response.values.length > 200) return res.badRequest("Too many results! Maximum number of allowed results is 200") - var consumers = [] - response.values.forEach(function(value,index){ - consumers.push({ - username : value[req.body.username], - custom_id : value[req.body.custom_id], - }) - }) - - // Remove first item if specified - if(req.body.ignore_first_row === 'true') { - consumers.shift() - } - - return res.json(consumers) - } - }); - } - }) - }); - - }); - - - - - - - } - } - -} diff --git a/api/services/remote/adapters/index.js b/api/services/remote/adapters/index.js deleted file mode 100644 index 7acb34067..000000000 --- a/api/services/remote/adapters/index.js +++ /dev/null @@ -1,27 +0,0 @@ -var fs = require('fs'); - -// Methods -module.exports = { - getSchemas : function(req,res) { - fs.readdir(__dirname, function(err, files) { - if(err) return res.negotiate(err) - var schemas = {} - files.forEach(function(file) { - if(file.indexOf('index') < 0) { - var fileData = require('./' + file) - if(fileData.enabled) - schemas[file.replace(".js","")] = fileData.schema - } - }); - res.json(schemas) - }) - } -} - -// Export the actual adapters -var files = fs.readdirSync(__dirname) -files.forEach(function(file) { - if(file.indexOf('index') < 0 && file.indexOf('credentials') < 0) { - module.exports[file.replace(".js","")] = require('./' + file) - } -}) diff --git a/api/services/remote/adapters/kongajson.js b/api/services/remote/adapters/kongajson.js deleted file mode 100644 index 0793bf05e..000000000 --- a/api/services/remote/adapters/kongajson.js +++ /dev/null @@ -1,56 +0,0 @@ -var unirest = require('unirest') - -module.exports = { - enabled : false, - schema : { - "name": "Konga JSON export", - "value": "kongajson", - "description": "Import previously exported Consumers", - "hasFiles" : true, - "form_fields": { - "connection": { - "file": { - "name": "File", - "required" : true, - "type": "file", - "description": "The JSON file containing the exported consumers" - }, - "keepids" : { - name : "Keep Ids", - "type" : "boolean", - "default" : false, - description : "Keep exported consumers Ids" - }, - } - } - }, - methods : { - loadConsumers : function(req,res) { - - - req.file('file').upload(function (err, uploadFiles) { - - if(err) return res.negotiate(err); - - - if(!uploadFiles.length) return res.badRequest("No files uploaded") - - - var jsonData = require(uploadFiles[0].fd) - - var keepIds = req.body.keepids || false - - if(!keepIds) { - jsonData.forEach(function(item){ - delete item.id - }) - } - - - return res.json(jsonData.data) - - }); - } - } - -} diff --git a/api/services/remote/adapters/mongodb.js b/api/services/remote/adapters/mongodb.js deleted file mode 100644 index ca717e549..000000000 --- a/api/services/remote/adapters/mongodb.js +++ /dev/null @@ -1,96 +0,0 @@ -var MongoClient = require('mongodb').MongoClient; - -module.exports = { - enabled : true, - schema : { - "name": "MongoDB", - "value": "mongodb", - "description": "Import Consumers from a MongoDB collection", - "form_fields": { - "connection": { - "host": { - "name": "host", - "type": "text", - "description": "The database host. Defaults to localhost" - }, - "port": { - "name": "port", - "type": "number", - "description": "The database port. Defaults to 27017" - }, - "user": { - "name": "user", - "type": "text", - "description": "The database user." - }, - "password": { - "name": "password", - "type": "text", - "description": "The database user\"s password." - }, - "database": { - "name": "database", - "type": "text", - "required": true, - "description": "The database name." - } - }, - "consumer": { - "collection": { - "name": "collection", - "type": "text", - "required": true, - "description": "The mongodb collection containing the consumers that will be imported to Kong." - }, - "username": { - "name": "username field", - "type": "text", - "required": true, - "description": "The collection property that will be used as the consumer username." - }, - "custom_id": { - "name": "custom_id field", - "type": "text", - "required": true, - "description": "The collection property that will be used as the consumer custom_id." - } - } - } - }, - methods : { - loadConsumers : function(req,res) { - - var host = req.body.host || 'localhost' - var port = req.body.port || 27017 - var user = req.body.user - var password = req.body.password - var database = req.body.database || 'sails' - - // Build connection string - var m = 'mongodb://' - var up = user ? user + ':' + password + '@' : '' - var c = host + ':' + port + '/' + database; - var url = m + up + c - - MongoClient.connect(url, function(err, db) { - if (err) return res.negotiate(err); - var collection = db.collection(req.body.collection || ''); - collection - .aggregate([ - { "$group": { - "_id": "$_id", - "custom_id": { "$first": "$" + req.body.custom_id }, - "username": { "$first": "$" + req.body.username }, - }} - - ]).toArray(function(err, docs) { - db.close(); // close the connection - if (err) return res.negotiate(err); - if(!docs.length) return res.notFound() - return res.json(docs) - }); - }); - } - } - -} diff --git a/api/services/remote/adapters/mysql.js b/api/services/remote/adapters/mysql.js deleted file mode 100644 index 4bd8513b2..000000000 --- a/api/services/remote/adapters/mysql.js +++ /dev/null @@ -1,75 +0,0 @@ -var mysql = require('mysql') - -module.exports = { - enabled : true, - schema : { - "name": "MySQL", - "value": "mysql", - "description": "Import Consumers from a MySQL database table", - "form_fields": { - "connection": { - "host": { - "name": "host", - "type": "text", - "description": "The database host. Defaults to localhost" - }, - "user": { - "name": "user", - "type": "text", - "description": "The database user. Defaults to root" - }, - "password": { - "name": "password", - "type": "text", - "description": "The database user\"s password." - }, - "database": { - "name": "database", - "type": "text", - "required": true, - "description": "The database to connect to." - } - }, - "consumer": { - "table": { - "name": "table", - "type": "text", - "required": true, - "description": "The table containing the consumers that will be imported to Kong." - }, - "username": { - "name": "username field", - "type": "text", - "required": true, - "description": "The table field that will be used as the consumers username." - }, - "custom_id": { - "name": "custom_id field", - "type": "text", - "required": true, - "description": "The table field that will be used as the consumers custom_id." - } - } - } - }, - methods : { - loadConsumers : function(req,res) { - - var connection = mysql.createConnection({ - host : req.body.host || 'localhost', - user : req.body.user || 'root', - password : req.body.password || '', - database : req.body.database || '' - }); - - connection.connect(function(err) { - if (err) return res.negotiate(err); - connection.query('SELECT ' + req.body.username + ' as username,' + req.body.custom_id + ' as custom_id FROM ' + req.body.table, function(err, rows, fields) { - if (err) return res.negotiate(err); - return res.json(rows); - }); - }); - } - } - -} diff --git a/api/services/remote/credentials/google-spreadsheets/credentials.json b/api/services/remote/credentials/google-spreadsheets/credentials.json deleted file mode 100644 index 2af097dbf..000000000 --- a/api/services/remote/credentials/google-spreadsheets/credentials.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "type": "service_account", - "project_id": "konga-148120", - "private_key_id": "84413cd1c5f890600ed6d0c77beb0040f2880471", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDB6r16wV0AGbG3\nHVY3hXc/g6r01UrYHQScNTAAA021zMKYJ8TLe3dJXm978avDcKkFeYL58dWVmTT9\nlICm8JsXGrLlx55K2MHs8NjrtEzI3c4xK2Y4pvFqDA+kKqQYgdxGlaaEyX8p0me3\nx2Zh8SgXRD+zUHTjZtL/oawU6FJkXY7D2ewmEhsYhvja+lqSv3GCOofhkfH/EtIX\nsdO9Jkg/UHHKjUW3UbS5U2HesiTGVtDMuP10TawT4d2CCGPdjRTUriTZU8jR1LcW\naHBA17QPMdWa9ObP3unUJebKv2p3shmNNfj46R6UcJSEKxhrviun3lo0aQYQLeAl\nVz4LTQIRAgMBAAECggEAHphOtON1LOLg6ycxxyjDm73GZ0KPwHEznQG3RQlAZYKc\n4SzFG+Wq+GRx6nhCxV8tC7QUOiMxs5ysg6W+dphXn9mSiDZqfxyb3CpTzzxvMPHt\n6kwSoLWWOUkV3qzrnwI+ItTRpPm1mn+b5Z8MRD+sN5+I/V2gU5CRkcuMPvA4r6Tc\nvLSv7x2Ilq8jWNiTNZS/ec39OcFObnL+gesgtw+PHlh+bITaXJKPqvEYM74TM5mh\nGth1EOm6YkOcoTvUSBYzmD1S+mrX5EcDiSxEL9je83banqbd631oAIjP8XmictjR\nYCznFvl7z9S4Bx0OvmvZeAR3eRZaoJgvbvHJ0THRAQKBgQDumQXw+WWWK8yEryCK\nfC1CxwKeKCig9VsrWeHD5llJK5kngEZbulJ7T/45MogMtfqbZ7DGzFVju5kSzs0v\n7NBRkJllgw0O2xh4hVjT+J2+QzA73IGDP1R3LnBfa5tUnzy8bNuKLh29qBffaTmx\nBOqFxDcb7akm9nfPC4rLz5XLCQKBgQDQD3Wvnge8YLPeCa+49crNnaubfp9I7F5M\nzAhyBocfz0s57LV2YbDIxhuK/c9n8STuRvyHoZAEtKHy4vQopxgtkRsa2Yx8yIOa\nKt0rSGX14zv6djgN18Mbc0f8kefgDwsx7GdTQ5mxs5ZojpYmDYiuzjbagwPqBSRK\nKk6Oe5nYyQKBgQC6d5jvFNnRnPU/FOanlBiDQajIFbZ65IWVwa7xPMq2pn4RIuzZ\nryna+U9DQDyXQnlSjpzXIMXzJZ+h1UECnV7I/+sCLIM+AviC6CPdtUUCdtvxTIlj\nG1FVu1NTH3PLlI8Q6zpAKX5QxFez4DaYI7FtTUWMbBZwPtqvUuPsGJWGkQKBgC7N\n+4SJQWZAPtZJSY2LRZchzBQL1RtbiQ8vqwqzqzwdrueV93BtI47W+iU3WawhVFSC\nBZchYjucwv3XzmNCy66cgQN2QyNjHC/XSq/M9prtBnYemBeNHSgUs/H+hLIj0Dnn\n24qgn3eZVdGpmd9vlfr1CuP3Ky8+/t3sTIXDCmX5AoGBAOirb1knm3jT7cbbtNfO\n5gEKw7GC9MPo/Sh9RDAhIEIhYkACgGSiWcQmOv17HJW55BCHlDVw4PDqv70XfnVT\nChbx7Rdcwy0Co+WK9wAmt8mQRM7njh4+wxVQWdDnfotAxrjFJtO7RPjpZ29oyvsK\n/iD5HA2q4XORpNgJ1nNZ/jGC\n-----END PRIVATE KEY-----\n", - "client_email": "pantsel@konga-148120.iam.gserviceaccount.com", - "client_id": "113335730171302572539", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://accounts.google.com/o/oauth2/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/pantsel%40konga-148120.iam.gserviceaccount.com" -} diff --git a/app.js b/app.js index fc7da5b17..281e483bd 100644 --- a/app.js +++ b/app.js @@ -19,48 +19,57 @@ * The same command-line arguments are supported, e.g.: * `node app.js --silent --port=80 --prod` */ +require('dotenv').config() // Ensure a "sails" can be located: -var sails; +let sails; try { - sails = require('sails'); + sails = require('sails'); } catch (e) { - console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); - console.error('To do that, run `npm install sails`'); - console.error(''); - console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); - console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); - console.error('but if it doesn\'t, the app will run with the global sails instead!'); - return; + console.error('To run an app using `node app.js`, you usually need to have a version of `sails` installed in the same directory as your app.'); + console.error('To do that, run `npm install sails`'); + console.error(''); + console.error('Alternatively, if you have sails installed globally (i.e. you did `npm install -g sails`), you can use `sails lift`.'); + console.error('When you run `sails lift`, your app will still use a local `./node_modules/sails` dependency if it exists,'); + console.error('but if it doesn\'t, the app will run with the global sails instead!'); + return; } +// Validate node version +const Utils = require('./api/services/Utils'); +if(!Utils.isRuntimeVersionSupported()) { + sails.log.error("Incompatible Node.js version. Please make sure that you have Node.js >= 8 installed.") + process.exit(1); +} + + // Try to get `rc` dependency -var rc; +let rc; try { - rc = require('rc'); + rc = require('rc'); } catch (e0) { - try { - rc = require('sails/node_modules/rc'); - } catch (e1) { - console.error('Could not find dependency: `rc`.'); - console.error('Your `.sailsrc` file(s) will be ignored.'); - console.error('To resolve this, run:'); - console.error('npm install rc --save'); - rc = function () { - return {}; - }; - } + try { + rc = require('sails/node_modules/rc'); + } catch (e1) { + console.error('Could not find dependency: `rc`.'); + console.error('Your `.sailsrc` file(s) will be ignored.'); + console.error('To resolve this, run:'); + console.error('npm install rc --save'); + rc = function () { + return {}; + }; + } } -require("./makedb")(function(err) { - if(err) { - process.exit(); - } +require("./makedb")(function (err) { + if (err) { + process.exit(); + } - // Start server - sails.lift(rc('sails')); + // Start server + sails.lift(rc('sails')); }); diff --git a/assets/js/app/certificates/add-certificates-modal.html b/assets/js/app/certificates/add-certificates-modal.html index 53d7ce879..8ee3a69b4 100644 --- a/assets/js/app/certificates/add-certificates-modal.html +++ b/assets/js/app/certificates/add-certificates-modal.html @@ -62,7 +62,7 @@