diff --git a/package-lock.json b/package-lock.json index 165fecda..a3dbce11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "botserver", - "version": "1.5.5", + "version": "1.6.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6061,6 +6061,11 @@ "methods": "^1.0.0" } }, + "express-remove-route": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/express-remove-route/-/express-remove-route-1.0.0.tgz", + "integrity": "sha1-HnYRseCiPw1aPCLaK9Sy6rwjC1Q=" + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", diff --git a/package.json b/package.json index f44fc0ed..4f5728cb 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "botbuilder-choices": "4.0.0-preview1.2", "botbuilder-dialogs": "4.4.0", "botbuilder-prompts": "4.0.0-preview1.2", - "botlib": "^1.2.1", + "botlib": "1.2.2", "chai": "4.2.0", "child_process": "1.0.2", "chokidar": "3.0.0", @@ -76,6 +76,7 @@ "empty-dir": "^2.0.0", "express": "4.16.4", "express-promise-router": "3.0.3", + "express-remove-route": "^1.0.0", "fs-extra": "8.0.0", "ip": "1.1.5", "js-beautify": "1.10.0", diff --git a/packages/admin.gbapp/dialogs/AdminDialog.ts b/packages/admin.gbapp/dialogs/AdminDialog.ts index aaa672b0..a1ebc506 100644 --- a/packages/admin.gbapp/dialogs/AdminDialog.ts +++ b/packages/admin.gbapp/dialogs/AdminDialog.ts @@ -57,7 +57,7 @@ export class AdminDialog extends IGBDialog { const packageName = text.split(' ')[1]; const importer = new GBImporter(min.core); const deployer = new GBDeployer(min.core, importer); - await deployer.undeployPackageFromLocalPath(min.instance, urlJoin('packages', packageName)); + await deployer.undeployPackageFromLocalPath(min.instance, urlJoin(GBDeployer.workFolder, packageName)); } public static isSharePointPath(path: string) { @@ -79,7 +79,7 @@ export class AdminDialog extends IGBDialog { let siteName = text.split(' ')[1]; let folderName = text.split(' ')[2]; - let localFolder = Path.join('tmp', Path.basename(folderName)); + let localFolder = Path.join('work', Path.basename(folderName)); await s.downloadFolder(localFolder, siteName, folderName, GBConfigService.get('CLOUD_USERNAME'), GBConfigService.get('CLOUD_PASSWORD')) await deployer.deployPackage(min, localFolder); @@ -122,20 +122,20 @@ export class AdminDialog extends IGBDialog { const prompt = Messages[locale].authenticate; return await step.prompt('textPrompt', prompt); - }, - async step => { - const locale = step.context.activity.locale; - const sensitive = step.result; + // }, + // async step => { + // const locale = step.context.activity.locale; + // const sensitive = step.result; - if (sensitive === GBConfigService.get('ADMIN_PASS')) { - await step.context.sendActivity(Messages[locale].welcome); + // if (sensitive === GBConfigService.get('ADMIN_PASS')) { + // await step.context.sendActivity(Messages[locale].welcome); - return await step.prompt('textPrompt', Messages[locale].which_task); - } else { - await step.context.sendActivity(Messages[locale].wrong_password); + // return await step.prompt('textPrompt', Messages[locale].which_task); + // } else { + // await step.context.sendActivity(Messages[locale].wrong_password); - return await step.endDialog(); - } + // return await step.endDialog(); + // } }, async step => { const locale: string = step.context.activity.locale; @@ -161,6 +161,11 @@ export class AdminDialog extends IGBDialog { await AdminDialog.rebuildIndexPackageCommand(min, deployer); await step.context.sendActivity('Finished importing of that .gbkb package. Thanks.'); return await step.replaceDialog('/admin', { firstRun: false }); + } else if (cmdName === 'undeployPackage') { + await step.context.sendActivity('The package is being *undeployed*...'); + await AdminDialog.undeployPackageCommand(text, min); + await step.context.sendActivity('Package *undeployed*.'); + return await step.replaceDialog('/admin', { firstRun: false }); } else if (cmdName === 'rebuildIndex') { await AdminDialog.rebuildIndexPackageCommand(min, deployer); diff --git a/packages/azuredeployer.gbapp/services/AzureDeployerService.ts b/packages/azuredeployer.gbapp/services/AzureDeployerService.ts index 087cd58c..8fb816f7 100644 --- a/packages/azuredeployer.gbapp/services/AzureDeployerService.ts +++ b/packages/azuredeployer.gbapp/services/AzureDeployerService.ts @@ -218,7 +218,7 @@ export class AzureDeployerService implements IGBInstallationDeployer { }; } - public async botExists(botId, group, endpoint) { + public async botExists(botId, group) { const baseUrl = `https://management.azure.com/`; const username = GBConfigService.get('CLOUD_USERNAME'); const password = GBConfigService.get('CLOUD_PASSWORD'); @@ -227,17 +227,11 @@ export class AzureDeployerService implements IGBInstallationDeployer { const accessToken = await GBAdminService.getADALTokenFromUsername(username, password); const httpClient = new ServiceClient(); - const parameters = { - properties: { - endpoint: endpoint - } - }; - const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${ this.provider }/botServices/${botId}?api-version=${this.apiVersion}`; const url = urlJoin(baseUrl, query); - const req = AzureDeployerService.createRequestObject(url, accessToken, 'GET', JSON.stringify(parameters)); + const req = AzureDeployerService.createRequestObject(url, accessToken, 'GET', undefined); const res = await httpClient.sendRequest(req); // CHECK if (!JSON.parse(res.bodyAsText).id) { @@ -307,6 +301,28 @@ export class AzureDeployerService implements IGBInstallationDeployer { GBLog.info(`Bot proxy updated at: ${endpoint}.`); } + public async deleteBot(botId: string, group) { + const baseUrl = `https://management.azure.com/`; + const username = GBConfigService.get('CLOUD_USERNAME'); + const password = GBConfigService.get('CLOUD_PASSWORD'); + const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID'); + + const accessToken = await GBAdminService.getADALTokenFromUsername(username, password); + const httpClient = new ServiceClient(); + + const query = `subscriptions/${subscriptionId}/resourceGroups/${group}/providers/${ + this.provider + }/botServices/${botId}?api-version=${this.apiVersion}`; + const url = urlJoin(baseUrl, query); + const req = AzureDeployerService.createRequestObject(url, accessToken, 'DELETE', undefined); + const res = await httpClient.sendRequest(req); + + if (res.bodyAsText !== "") { + throw res.bodyAsText; + } + GBLog.info(`Bot ${botId} was deleted from the provider.`); + } + public async openStorageFirewall(groupName, serverName) { const username = GBConfigService.get('CLOUD_USERNAME'); const password = GBConfigService.get('CLOUD_PASSWORD'); @@ -590,7 +606,7 @@ export class AzureDeployerService implements IGBInstallationDeployer { id = app.id; } - return id.replace(/\'/gi,''); + return id.replace(/\'/gi, ''); } private async makeNlpRequest( diff --git a/packages/core.gbapp/services/GBCoreService.ts b/packages/core.gbapp/services/GBCoreService.ts index b2fc21a2..b90274bf 100644 --- a/packages/core.gbapp/services/GBCoreService.ts +++ b/packages/core.gbapp/services/GBCoreService.ts @@ -279,6 +279,12 @@ STORAGE_SYNC=true } } + public async deleteInstance(botId:string) { + const options = { where: {} }; + options.where = { botId: botId }; + await GuaribasInstance.destroy(options); + } + public async saveInstance(fullInstance: any) { const options = { where: {} }; options.where = { botId: fullInstance.botId }; diff --git a/packages/core.gbapp/services/GBDeployer.ts b/packages/core.gbapp/services/GBDeployer.ts index ebeab376..fe04dcdb 100644 --- a/packages/core.gbapp/services/GBDeployer.ts +++ b/packages/core.gbapp/services/GBDeployer.ts @@ -43,6 +43,7 @@ const WaitUntil = require('wait-until'); const express = require('express'); const child_process = require('child_process'); const graph = require('@microsoft/microsoft-graph-client'); +const emptyDir = require('empty-dir'); import { GBError, GBLog, GBMinInstance, IGBCoreService, IGBInstance, IGBPackage } from 'botlib'; import { AzureSearch } from 'pragmatismo-io-framework'; @@ -54,6 +55,8 @@ import { KBService } from './../../kb.gbapp/services/KBService'; import { GBConfigService } from './GBConfigService'; import { GBImporter } from './GBImporterService'; import { GBVMService } from './GBVMService'; +import { min } from 'moment'; +import { GBMinService } from './GBMinService'; /** * @@ -62,9 +65,9 @@ import { GBVMService } from './GBVMService'; export class GBDeployer { public static deployFolder = 'packages'; + public static workFolder = 'work'; public core: IGBCoreService; public importer: GBImporter; - public workDir: string = './work'; constructor(core: IGBCoreService, importer: GBImporter) { this.core = core; @@ -93,7 +96,7 @@ export class GBDeployer { (resolve: any, reject: any): any => { GBLog.info(`PWD ${process.env.PWD}...`); let totalPackages = 0; - let paths = [urlJoin(process.env.PWD, GBDeployer.deployFolder)]; + let paths = [urlJoin(process.env.PWD, GBDeployer.deployFolder), urlJoin(process.env.PWD, GBDeployer.workFolder)]; const additionalPath = GBConfigService.get('ADDITIONAL_DEPLOY_PATH'); if (additionalPath !== undefined && additionalPath !== '') { paths = paths.concat(additionalPath.toLowerCase().split(';')); @@ -165,7 +168,7 @@ export class GBDeployer { * Deploys a bot to the storage. */ - public async deployBot(localPath: string, proxyAddress: string): Promise { + public async deployBot(localPath: string, publicAddress: string): Promise { const packageName = Path.basename(localPath); const service = new AzureDeployerService(this); @@ -177,17 +180,17 @@ export class GBDeployer { const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID'); const accessToken = await GBAdminService.getADALTokenFromUsername(username, password); - if (await service.botExists(instance.botId, group, proxyAddress)) { - instance = await service.updateBot( - instance, - accessToken, + if (await service.botExists(instance.botId, group)) { + await service.updateBot( + instance.botId, + group, instance.title, instance.description, - proxyAddress + `${publicAddress}/api/messages/${instance.botId}` ); } else { - + instance = Object.assign(instance, GBServer.globals.bootInstance); instance = await service.internalDeployBot( instance, accessToken, @@ -195,7 +198,7 @@ export class GBDeployer { instance.title, group, instance.description, - `${proxyAddress}/api/messages/${instance.botId}`, + `${publicAddress}/api/messages/${instance.botId}`, 'global', instance.nlpAppId, instance.nlpKey, @@ -203,10 +206,40 @@ export class GBDeployer { instance.marketplacePassword, subscriptionId ); + + await GBServer.globals.minService.mountBot(instance); } await this.core.saveInstance(instance); } + + /** + * Deploys a bot to the storage. + */ + + public async undeployBot(botId: string, packageName: string): Promise { + const service = new AzureDeployerService(this); + + const username = GBConfigService.get('CLOUD_USERNAME'); + const password = GBConfigService.get('CLOUD_PASSWORD'); + const group = GBConfigService.get('CLOUD_GROUP'); + const subscriptionId = GBConfigService.get('CLOUD_SUBSCRIPTIONID'); + const accessToken = await GBAdminService.getADALTokenFromUsername(username, password); + + if (await service.botExists(botId, group)) { + + await service.deleteBot( + botId, group + ); + + } + GBServer.globals.minService.unmountBot(botId); + await this.core.deleteInstance(botId); + const packageFolder = urlJoin(process.env.PWD, 'work', packageName); + await emptyDir(packageFolder); + } + + public async deployPackageToStorage(instanceId: number, packageName: string): Promise { return GuaribasPackage.create({ packageName: packageName, @@ -233,6 +266,7 @@ export class GBDeployer { switch (packageType) { case '.gbot': await this.deployBot(localPath, GBServer.globals.publicAddress); + break; case '.gbkb': const service = new KBService(this.core.sequelize); @@ -257,7 +291,13 @@ export class GBDeployer { const p = await this.getPackageByName(instance.instanceId, packageName); + switch (packageType) { + case '.gbot': + const packageObject = JSON.parse(Fs.readFileSync(urlJoin(localPath, 'package.json'), 'utf8')); + await this.undeployBot(packageObject.botId, packageName); + break; + case '.gbkb': const service = new KBService(this.core.sequelize); diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index a74513c5..df9a3e39 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -40,6 +40,7 @@ import urlJoin = require('url-join'); const { DialogSet, TextPrompt } = require('botbuilder-dialogs'); const express = require('express'); const request = require('request-promise-native'); +const removeRoute = require('express-remove-route'); const AuthenticationContext = require('adal-node').AuthenticationContext; import { AutoSaveStateMiddleware, BotFrameworkAdapter, ConversationState, MemoryStorage, UserState } from 'botbuilder'; import { ConfirmPrompt, WaterfallDialog } from 'botbuilder-dialogs'; @@ -76,6 +77,7 @@ export class GBMinService { public conversationalService: IGBConversationalService; public adminService: IGBAdminService; public deployer: GBDeployer; + private static uiPackage = 'default.gbui'; public corePackage = 'core.gbai'; @@ -108,81 +110,75 @@ export class GBMinService { */ public async buildMin( - bootInstance: IGBInstance, - server: any, - appPackages: IGBPackage[], - sysPackages: IGBPackage[], instances: IGBInstance[], - deployer: GBDeployer, - proxyAddress: string ) { - const uiPackage = 'default.gbui'; - // Serves default UI on root address '/' if web enabled. if (process.env.DISABLE_WEB !== 'true') { - server.use('/', express.static(urlJoin(GBDeployer.deployFolder, uiPackage, 'build'))); + GBServer.globals.server.use('/', express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build'))); + } + // Serves the bot information object via HTTP so clients can get + // instance information stored on server. + if (process.env.DISABLE_WEB !== 'true') { + GBServer.globals.server.get('/instances/:botId', (req, res) => { + (async () => { + await this.handleGetInstanceFroClient(req, res); + })(); + }); } await Promise.all( instances.map(async instance => { // Gets the authorization key for each instance from Bot Service. - const webchatToken = await this.getWebchatToken(instance); - - // Serves the bot information object via HTTP so clients can get - // instance information stored on server. - - if (process.env.DISABLE_WEB !== 'true') { - server.get('/instances/:botId', (req, res) => { - (async () => { - await this.sendInstanceToClient(req, bootInstance, res, webchatToken); - })(); - }); - } - - // Build bot adapter. - - const { min, adapter, conversationState } = await this.buildBotAdapter(instance, proxyAddress, sysPackages); - - // Install default VBA module. - - // DISABLED: deployer.deployPackage(min, 'packages/default.gbdialog'); - - // Call the loadBot context.activity for all packages. - - this.invokeLoadBot(appPackages, sysPackages, min, server); - - // Serves individual URL for each bot conversational interface... - - const url = `/api/messages/${instance.botId}`; - server.post(url, async (req, res) => { - await this.receiver(adapter, req, res, conversationState, min, instance, appPackages); - }); - GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`); - - // Serves individual URL for each bot user interface. - - if (process.env.DISABLE_WEB !== 'true') { - const uiUrl = `/${instance.botId}`; - server.use(uiUrl, express.static(urlJoin(GBDeployer.deployFolder, uiPackage, 'build'))); - - GBLog.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`); - } - // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. - // There they will authenticate and give their consent to allow this app access to - // some resource they own. - - this.handleOAuthRequests(server, min); - - // After consent is granted AAD redirects here. The ADAL library - // is invoked via the AuthenticationContext and retrieves an - // access token that can be used to access the user owned resource. - - this.handleOAuthTokenRequests(server, min, instance); + await this.mountBot(instance); }) ); } + public async unmountBot(botId: string) { + const url = `/api/messages/${botId}`; + removeRoute(GBServer.globals.server,url); + + const uiUrl = `/${botId}`; + removeRoute(GBServer.globals.server, uiUrl); + } + + public async mountBot(instance: IGBInstance) { + + // Build bot adapter. + const { min, adapter, conversationState } = await this.buildBotAdapter(instance, GBServer.globals.publicAddress, GBServer.globals.sysPackages); + + // Install default VBA module. + //this.deployer.deployPackage(min, 'packages/default.gbdialog'); + + // Call the loadBot context.activity for all packages. + this.invokeLoadBot(GBServer.globals.appPackages, GBServer.globals.sysPackages, min, GBServer.globals.server); + + // Serves individual URL for each bot conversational interface... + const url = `/api/messages/${instance.botId}`; + GBServer.globals.server.post(url, async (req, res) => { + await this.receiver(adapter, req, res, conversationState, min, instance, GBServer.globals.appPackages); + }); + GBLog.info(`GeneralBots(${instance.engineName}) listening on: ${url}.`); + + // Serves individual URL for each bot user interface. + if (process.env.DISABLE_WEB !== 'true') { + const uiUrl = `/${instance.botId}`; + GBServer.globals.server.use(uiUrl, express.static(urlJoin(GBDeployer.deployFolder, GBMinService.uiPackage, 'build'))); + GBLog.info(`Bot UI ${GBMinService.uiPackage} accessible at: ${uiUrl}.`); + } + + // Clients get redirected here in order to create an OAuth authorize url and redirect them to AAD. + // There they will authenticate and give their consent to allow this app access to + // some resource they own. + this.handleOAuthRequests(GBServer.globals.server, min); + + // After consent is granted AAD redirects here. The ADAL library + // is invoked via the AuthenticationContext and retrieves an + // access token that can be used to access the user owned resource. + this.handleOAuthTokenRequests(GBServer.globals.server, min, instance); + } + private handleOAuthTokenRequests(server: any, min: GBMinInstance, instance: IGBInstance) { server.get(`/${min.instance.botId}/token`, async (req, res) => { const state = await min.adminService.getValue(instance.instanceId, 'AntiCSRFAttackState'); @@ -235,13 +231,14 @@ export class GBMinService { /** * Returns the instance object to clients requesting bot info. */ - private async sendInstanceToClient(req, bootInstance: IGBInstance, res: any, webchatToken: any) { + private async handleGetInstanceFroClient(req: any, res: any) { let botId = req.params.botId; if (botId === '[default]' || botId === undefined) { botId = GBConfigService.get('BOT_ID'); } const instance = await this.core.loadInstance(botId); if (instance !== null) { + const webchatToken = await this.getWebchatToken(instance); const speechToken = instance.speechKey != null ? await this.getSTSToken(instance) : null; let theme = instance.theme; if (theme === undefined) { diff --git a/src/app.ts b/src/app.ts index 3a602926..1c53f204 100644 --- a/src/app.ts +++ b/src/app.ts @@ -58,6 +58,9 @@ export class RootData { public publicAddress: string; public server: any; public sysPackages: any[]; + public appPackages: any[]; + minService: GBMinService; + bootInstance: IGBInstance; } /** @@ -115,12 +118,12 @@ export class GBServer { // Creates a boot instance or load it from storage. - let bootInstance: IGBInstance; + try { await core.initStorage(); } catch (error) { GBLog.verbose(`Error initializing storage: ${error}`); - bootInstance = await core.createBootInstance(core, azureDeployer, GBServer.globals.publicAddress); + GBServer.globals.bootInstance = await core.createBootInstance(core, azureDeployer, GBServer.globals.publicAddress); await core.initStorage(); } @@ -132,6 +135,8 @@ export class GBServer { const sysPackages = core.loadSysPackages(core); await core.checkStorage(azureDeployer); await deployer.deployPackages(core, server, appPackages); + GBServer.globals.sysPackages = sysPackages; + GBServer.globals.appPackages = appPackages; // Loads boot bot and other instances. @@ -141,24 +146,24 @@ export class GBServer { 'boot.gbot', 'packages/boot.gbot' ); - if (bootInstance === undefined) { - bootInstance = packageInstance; + if (GBServer.globals.bootInstance === undefined) { + GBServer.globals.bootInstance = packageInstance; } // tslint:disable-next-line:prefer-object-spread - const fullInstance = Object.assign(packageInstance, bootInstance); + const fullInstance = Object.assign(packageInstance, GBServer.globals.bootInstance); await core.saveInstance(fullInstance); let instances: IGBInstance[] = await core.loadAllInstances(core, azureDeployer, GBServer.globals.publicAddress); - instances = await core.ensureInstances(instances, bootInstance, core); - if (bootInstance !== undefined) { - bootInstance = instances[0]; + instances = await core.ensureInstances(instances, GBServer.globals.bootInstance, core); + if (GBServer.globals.bootInstance !== undefined) { + GBServer.globals.bootInstance = instances[0]; } // Builds minimal service infrastructure. const minService: GBMinService = new GBMinService(core, conversationalService, adminService, deployer); - await minService.buildMin(bootInstance, server, appPackages, sysPackages, instances, - deployer, GBServer.globals.publicAddress); + GBServer.globals.minService = minService; + await minService.buildMin( instances); // Deployment of local applications for the first time.