From 50865fa9ec4114f2ec387250446b892be0547ba5 Mon Sep 17 00:00:00 2001 From: Kasra Bigdeli Date: Sun, 17 Dec 2017 14:04:44 -0800 Subject: [PATCH] github hooks phase 1 --- app-backend/app.js | 2 + app-backend/dist/js/dashboard.min.js | 243 +++++++++--------- app-backend/dist/templates/apps.html | 42 +++ app-backend/src/datastore/DataStoreImpl.js | 67 ++++- app-backend/src/routes/AppDefinitionRouter.js | 26 +- app-backend/src/user/Authenticator.js | 80 +++++- app-backend/src/user/ServiceManager.js | 6 +- app-frontend/src/js/captain/apiManager.js | 2 + .../js/controllers/app-definitions-ctrl.js | 3 + app-frontend/src/templates/apps.html | 42 +++ 10 files changed, 383 insertions(+), 130 deletions(-) diff --git a/app-backend/app.js b/app-backend/app.js index c01dece..069565e 100644 --- a/app-backend/app.js +++ b/app-backend/app.js @@ -173,6 +173,8 @@ app.use(function (err, req, res, next) { res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; + Logger.e(err); + // render the error page res.status(err.status || 500); res.render('error'); diff --git a/app-backend/dist/js/dashboard.min.js b/app-backend/dist/js/dashboard.min.js index b8c5f26..bfa8476 100644 --- a/app-backend/dist/js/dashboard.min.js +++ b/app-backend/dist/js/dashboard.min.js @@ -464,6 +464,7 @@ angular var volumes = appDefinition.volumes; var ports = appDefinition.ports; var nodeId = appDefinition.nodeId; + var appPushWebhook = appDefinition.appPushWebhook; $http .post(BASE_API + 'user/appDefinitions/update', { @@ -472,6 +473,7 @@ angular notExposeAsWebApp: notExposeAsWebApp, volumes: volumes, ports: ports, + appPushWebhook: appPushWebhook, nodeId: nodeId, envVars: envVars }, createConfig()) @@ -650,6 +652,124 @@ angular.module('RDash') } }); +/** + * File Upload Read Directive + */ + +angular + .module('RDash') + .directive("fileread", [function () { + return { + scope: { + fileread: "=" + }, + link: function (scope, element, attributes) { + element.bind("change", function (changeEvent) { + scope.$apply(function () { + scope.fileread = changeEvent.target.files[0]; + // or all selected files: + // scope.fileread = changeEvent.target.files; + }); + }); + } + } + }]); +/** + * Loading Directive + * @see http://tobiasahlin.com/spinkit/ + */ + +angular + .module('RDash') + .directive('rdLoading', rdLoading); + +function rdLoading() { + var directive = { + restrict: 'AE', + template: '
' + }; + return directive; +}; +/** + * Widget Body Directive + */ + +angular + .module('RDash') + .directive('rdWidgetBody', rdWidgetBody); + +function rdWidgetBody() { + var directive = { + requires: '^rdWidget', + scope: { + loading: '=?', + classes: '@?' + }, + transclude: true, + template: '
', + restrict: 'E' + }; + return directive; +}; + +/** + * Widget Footer Directive + */ + +angular + .module('RDash') + .directive('rdWidgetFooter', rdWidgetFooter); + +function rdWidgetFooter() { + var directive = { + requires: '^rdWidget', + transclude: true, + template: '', + restrict: 'E' + }; + return directive; +}; +/** + * Widget Header Directive + */ + +angular + .module('RDash') + .directive('rdWidgetHeader', rdWidgetTitle); + +function rdWidgetTitle() { + var directive = { + requires: '^rdWidget', + scope: { + title: '@', + icon: '@' + }, + transclude: true, + template: '
{{title}}
', + restrict: 'E' + }; + return directive; +}; +/** + * Widget Directive + */ + +angular + .module('RDash') + .directive('rdWidget', rdWidget); + +function rdWidget() { + var directive = { + transclude: true, + template: '
', + restrict: 'EA' + }; + return directive; + + function link(scope, element, attrs) { + /* */ + } +}; /** * Alerts Controller */ @@ -685,6 +805,7 @@ angular.module('RDash') function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, apiManager, captainToast, $uibModal, $state, $location) { + $scope.rootDomainWithProtocol = window.location.protocol; $scope.loadingState = {}; $scope.loadingState.enabled = true; $scope.search = {}; @@ -715,6 +836,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, } $scope.rootDomain = data.rootDomain; + $scope.rootDomainWithProtocol = window.location.protocol + '//' + data.rootDomain; }); @@ -936,6 +1058,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, ports: app.ports, nodeId: app.nodeId, notExposeAsWebApp: app.notExposeAsWebApp, + appPushWebhook: app.appPushWebhook, volumes: app.volumes } @@ -1526,122 +1649,4 @@ function SettingsCtrl($scope, $cookieStore, $rootScope, pageDefinitions, }()); -} -/** - * File Upload Read Directive - */ - -angular - .module('RDash') - .directive("fileread", [function () { - return { - scope: { - fileread: "=" - }, - link: function (scope, element, attributes) { - element.bind("change", function (changeEvent) { - scope.$apply(function () { - scope.fileread = changeEvent.target.files[0]; - // or all selected files: - // scope.fileread = changeEvent.target.files; - }); - }); - } - } - }]); -/** - * Loading Directive - * @see http://tobiasahlin.com/spinkit/ - */ - -angular - .module('RDash') - .directive('rdLoading', rdLoading); - -function rdLoading() { - var directive = { - restrict: 'AE', - template: '
' - }; - return directive; -}; -/** - * Widget Body Directive - */ - -angular - .module('RDash') - .directive('rdWidgetBody', rdWidgetBody); - -function rdWidgetBody() { - var directive = { - requires: '^rdWidget', - scope: { - loading: '=?', - classes: '@?' - }, - transclude: true, - template: '
', - restrict: 'E' - }; - return directive; -}; - -/** - * Widget Footer Directive - */ - -angular - .module('RDash') - .directive('rdWidgetFooter', rdWidgetFooter); - -function rdWidgetFooter() { - var directive = { - requires: '^rdWidget', - transclude: true, - template: '', - restrict: 'E' - }; - return directive; -}; -/** - * Widget Header Directive - */ - -angular - .module('RDash') - .directive('rdWidgetHeader', rdWidgetTitle); - -function rdWidgetTitle() { - var directive = { - requires: '^rdWidget', - scope: { - title: '@', - icon: '@' - }, - transclude: true, - template: '
{{title}}
', - restrict: 'E' - }; - return directive; -}; -/** - * Widget Directive - */ - -angular - .module('RDash') - .directive('rdWidget', rdWidget); - -function rdWidget() { - var directive = { - transclude: true, - template: '
', - restrict: 'EA' - }; - return directive; - - function link(scope, element, attrs) { - /* */ - } -}; \ No newline at end of file +} \ No newline at end of file diff --git a/app-backend/dist/templates/apps.html b/app-backend/dist/templates/apps.html index 8f24c20..015d4de 100644 --- a/app-backend/dist/templates/apps.html +++ b/app-backend/dist/templates/apps.html @@ -235,6 +235,48 @@
+ +
+

+ Deploy from Github/Bitbucket +

+

+ POST Webhook: +
+

{{app.appPushWebhook.pushWebhookToken?(rootDomainWithProtocol+'/api/v1/webhooks/triggerbuild?token='+app.appPushWebhook.pushWebhookToken):
+                      '** Add repo info and save for this webhook to appear **'}}
+

+
+
+
+
+
+ Username: + +
+
+
+
+ Password: + +
+
+
+
+
+
+ Repository: + +
+
+
+
+ Branch: + +
+
+
+
diff --git a/app-backend/src/datastore/DataStoreImpl.js b/app-backend/src/datastore/DataStoreImpl.js index 91fc0b9..dbc8185 100644 --- a/app-backend/src/datastore/DataStoreImpl.js +++ b/app-backend/src/datastore/DataStoreImpl.js @@ -2,6 +2,7 @@ * Created by kasra on 27/06/17. */ const Configstore = require('configstore'); +const uuid = require('uuid/v4'); const isValidPath = require('is-valid-path'); const fs = require('fs-extra'); const ApiStatusCodes = require('../api/ApiStatusCodes'); @@ -392,11 +393,39 @@ class DataStore { }); } - updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports) { + updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook, authenticator) { const self = this; - return this.getAppDefinition(appName) - .then(function (app) { + let app; + + return Promise.resolve() + .then(function () { + + return self.getAppDefinition(appName); + + }) + .then(function (appObj) { + + app = appObj; + + }) + .then(function () { + + if (appPushWebhook.repoInfo && appPushWebhook.repoInfo.repo && appPushWebhook.repoInfo.branch + && appPushWebhook.repoInfo.user && appPushWebhook.repoInfo.password) { + return authenticator + .getAppPushWebhookDatastore({ + repo: appPushWebhook.repoInfo.repo, + branch: appPushWebhook.repoInfo.branch, + user: appPushWebhook.repoInfo.user, + password: appPushWebhook.repoInfo.password + }) + } + + return null; + + }) + .then(function (appPushWebhookRepoInfo) { instanceCount = Number(instanceCount); @@ -404,6 +433,22 @@ class DataStore { app.notExposeAsWebApp = !!notExposeAsWebApp; app.nodeId = nodeId; + if (appPushWebhookRepoInfo) { + + app.appPushWebhook = app.appPushWebhook || {}; + + if (!app.appPushWebhook.tokenVersion) { + app.appPushWebhook.tokenVersion = uuid(); + } + + app.appPushWebhook.repoInfo = appPushWebhookRepoInfo; + } + else { + + app.appPushWebhook = {}; + + } + if (ports) { function isPortValid(portNumber) { @@ -471,6 +516,21 @@ class DataStore { } } + }) + .then(function () { + + if (app.appPushWebhook.repoInfo) { + return authenticator + .getAppPushWebhookToken(appName, app.appPushWebhook.tokenVersion) + } + + }) + .then(function (pushWebhookToken) { + + if (pushWebhookToken) { + app.appPushWebhook.pushWebhookToken = pushWebhookToken; + } + self.data.set(APP_DEFINITIONS + '.' + appName, app); }); @@ -687,6 +747,7 @@ class DataStore { envVars: [], volumes: [], ports: [], + appPushWebhook: {}, // tokenVersion, repoInfo, pushWebhookToken versions: [] }; diff --git a/app-backend/src/routes/AppDefinitionRouter.js b/app-backend/src/routes/AppDefinitionRouter.js index 5e7d300..eb4ec6c 100644 --- a/app-backend/src/routes/AppDefinitionRouter.js +++ b/app-backend/src/routes/AppDefinitionRouter.js @@ -4,6 +4,7 @@ const router = express.Router(); const BaseApi = require('../api/BaseApi'); const ApiStatusCodes = require('../api/ApiStatusCodes'); const Logger = require('../utils/Logger'); +const Authenticator = require('../user/Authenticator'); // Get a list of oneclickspps router.get('/oneclickapps', function (req, res, next) { @@ -36,19 +37,39 @@ router.get('/', function (req, res, next) { let dataStore = res.locals.user.dataStore; let serviceManager = res.locals.user.serviceManager; + let appsArray = []; dataStore.getAppDefinitions() .then(function (apps) { - let appsArray = []; + let promises = []; Object.keys(apps).forEach(function (key, index) { let app = apps[key]; app.appName = key; app.isAppBuilding = serviceManager.isAppBuilding(key); + app.appPushWebhook = app.appPushWebhook || {}; + + let repoInfoEncrypted = app.appPushWebhook ? app.appPushWebhook.repoInfo : null; + if (repoInfoEncrypted) { + promises.push( + Authenticator.get(dataStore.getNameSpace()) + .decodeAppPushWebhookDatastore(repoInfoEncrypted) + .then(function (decryptedData) { + app.appPushWebhook.repoInfo = decryptedData; + })); + } + else { + app.appPushWebhook.repoInfo = {}; + } appsArray.push(app); }); + return Promise.all(promises); + + }) + .then(function () { + let baseApi = new BaseApi(ApiStatusCodes.STATUS_OK, "App definitions are retrieved."); baseApi.data = appsArray; baseApi.rootDomain = dataStore.getRootDomain(); @@ -299,6 +320,7 @@ router.post('/update/', function (req, res, next) { let appName = req.body.appName; let nodeId = req.body.nodeId; let notExposeAsWebApp = req.body.notExposeAsWebApp; + let appPushWebhook = req.body.appPushWebhook || {}; let envVars = req.body.envVars || []; let volumes = req.body.volumes || []; let ports = req.body.ports || []; @@ -306,7 +328,7 @@ router.post('/update/', function (req, res, next) { Logger.d('Updating app started: ' + appName); - serviceManager.updateAppDefinition(appName, Number(instanceCount), envVars, volumes, nodeId, notExposeAsWebApp, ports) + serviceManager.updateAppDefinition(appName, Number(instanceCount), envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook) .then(function () { Logger.d('AppName is updated: ' + appName); diff --git a/app-backend/src/user/Authenticator.js b/app-backend/src/user/Authenticator.js index 3681a99..4d65a53 100644 --- a/app-backend/src/user/Authenticator.js +++ b/app-backend/src/user/Authenticator.js @@ -10,8 +10,9 @@ const DataStoreProvider = require('../datastore/DataStoreProvider'); const captainDefaultPassword = EnvVar.DEFAULT_PASSWORD || 'captain42'; -const COOKIE_AUTH_PREFIX = 'cookie-'; - +const COOKIE_AUTH_SUFFIX = 'cookie-'; +const WEBHOOK_APP_PUSH_SUFFIX = '-webhook-app-push'; +const WEBHOOK_APP_PUSH_DATASTORE_SUFFIX = "-webhook-app-datastore"; class Authenticator { @@ -72,7 +73,7 @@ class Authenticator { } getAuthTokenForCookies(password) { - return this.getAuthToken(password, COOKIE_AUTH_PREFIX); + return this.getAuthToken(password, COOKIE_AUTH_SUFFIX); } getAuthToken(password, keySuffix) { @@ -101,7 +102,7 @@ class Authenticator { } decodeAuthTokenFromCookies(token) { - return this.decodeAuthToken(token, COOKIE_AUTH_PREFIX); + return this.decodeAuthToken(token, COOKIE_AUTH_SUFFIX); } decodeAuthToken(token, keySuffix) { @@ -135,6 +136,77 @@ class Authenticator { }); } + + getAppPushWebhookDatastore(dataToSave) { + const self = this; + + return self.getGenericToken(dataToSave, WEBHOOK_APP_PUSH_DATASTORE_SUFFIX) + } + + decodeAppPushWebhookDatastore(token) { + const self = this; + + return self.decodeGenericToken(token, WEBHOOK_APP_PUSH_DATASTORE_SUFFIX) + } + + getAppPushWebhookToken(appName, tokenVersion) { + const self = this; + + if (!appName) { + throw ApiStatusCodes.createError(ApiStatusCodes.STATUS_ERROR_GENERIC, 'App name are required for webhook token..'); + } + + return self.getGenericToken({ + tokenVersion: tokenVersion, + appName: appName + }, WEBHOOK_APP_PUSH_SUFFIX) + } + + decodeAppPushWebhookToken(token) { + const self = this; + + return self.decodeGenericToken(token, WEBHOOK_APP_PUSH_SUFFIX) + } + + getGenericToken(obj, keySuffix) { + + const self = this; + obj.namespace = self.namespace; + + return Promise.resolve() + .then(function () { + return jwt.sign({ + data: obj + }, self.encryptionKey + (keySuffix ? keySuffix : '')); + }) + } + + decodeGenericToken(token, keySuffix) { + const self = this; + + return new Promise(function (resolve, reject) { + + jwt.verify(token, self.encryptionKey + (keySuffix ? keySuffix : ''), function (err, rawDecoded) { + if (err) { + Logger.d(err); + reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_AUTH_TOKEN_INVALID, 'Token corrupted')); + return; + } + + let decodedData = rawDecoded.data; + + if (decodedData.namespace !== self.namespace) { + reject(ApiStatusCodes.createError(ApiStatusCodes.STATUS_AUTH_TOKEN_INVALID, + 'token does not match the namespace')); + return; + } + + resolve(decodedData); + }); + + }); + } + } const authenticatorCache = {}; diff --git a/app-backend/src/user/ServiceManager.js b/app-backend/src/user/ServiceManager.js index fb68479..4f44dee 100644 --- a/app-backend/src/user/ServiceManager.js +++ b/app-backend/src/user/ServiceManager.js @@ -6,6 +6,7 @@ const path = require('path'); const CaptainManager = require('./CaptainManager'); const ApiStatusCodes = require('../api/ApiStatusCodes'); const TemplateHelper = require('./TemplateHelper'); +const Authenticator = require('./Authenticator'); const uuid = require('uuid/v4'); const SOURCE_FOLDER_NAME = 'src'; @@ -630,7 +631,7 @@ class ServiceManager { }); } - updateAppDefinition(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports) { + updateAppDefinition(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports, appPushWebhook) { const self = this; const dataStore = this.dataStore; @@ -721,7 +722,8 @@ class ServiceManager { }) .then(function () { - return dataStore.updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, notExposeAsWebApp, ports); + return dataStore.updateAppDefinitionInDb(appName, instanceCount, envVars, volumes, nodeId, + notExposeAsWebApp, ports, appPushWebhook, Authenticator.get(dataStore.getNameSpace())); }) .then(function () { diff --git a/app-frontend/src/js/captain/apiManager.js b/app-frontend/src/js/captain/apiManager.js index 6388868..73f05f1 100644 --- a/app-frontend/src/js/captain/apiManager.js +++ b/app-frontend/src/js/captain/apiManager.js @@ -382,6 +382,7 @@ var volumes = appDefinition.volumes; var ports = appDefinition.ports; var nodeId = appDefinition.nodeId; + var appPushWebhook = appDefinition.appPushWebhook; $http .post(BASE_API + 'user/appDefinitions/update', { @@ -390,6 +391,7 @@ notExposeAsWebApp: notExposeAsWebApp, volumes: volumes, ports: ports, + appPushWebhook: appPushWebhook, nodeId: nodeId, envVars: envVars }, createConfig()) diff --git a/app-frontend/src/js/controllers/app-definitions-ctrl.js b/app-frontend/src/js/controllers/app-definitions-ctrl.js index 4d40342..22c2269 100644 --- a/app-frontend/src/js/controllers/app-definitions-ctrl.js +++ b/app-frontend/src/js/controllers/app-definitions-ctrl.js @@ -6,6 +6,7 @@ angular.module('RDash') function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, apiManager, captainToast, $uibModal, $state, $location) { + $scope.rootDomainWithProtocol = window.location.protocol; $scope.loadingState = {}; $scope.loadingState.enabled = true; $scope.search = {}; @@ -36,6 +37,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, } $scope.rootDomain = data.rootDomain; + $scope.rootDomainWithProtocol = window.location.protocol + '//' + data.rootDomain; }); @@ -257,6 +259,7 @@ function AppDefinitionCtrl($scope, $cookieStore, $rootScope, pageDefinitions, ports: app.ports, nodeId: app.nodeId, notExposeAsWebApp: app.notExposeAsWebApp, + appPushWebhook: app.appPushWebhook, volumes: app.volumes } diff --git a/app-frontend/src/templates/apps.html b/app-frontend/src/templates/apps.html index 68aeee1..abcae73 100644 --- a/app-frontend/src/templates/apps.html +++ b/app-frontend/src/templates/apps.html @@ -236,6 +236,48 @@
+ +
+

+ Deploy from Github/Bitbucket +

+

+ POST Webhook: +
+

{{app.appPushWebhook.pushWebhookToken?(rootDomainWithProtocol+'/api/v1/webhooks/triggerbuild?token='+app.appPushWebhook.pushWebhookToken):
+                      '** Add repo info and save for this webhook to appear **'}}
+

+
+
+
+
+
+ Username: + +
+
+
+
+ Password: + +
+
+
+
+
+
+ Repository: + +
+
+
+
+ Branch: + +
+
+
+