From 3832f27451d0fde8f2428463204e3ec45c6b2e5e Mon Sep 17 00:00:00 2001 From: Rodrigo Rodriguez Date: Mon, 24 Sep 2018 11:04:36 -0300 Subject: [PATCH] * FIX: Admin now is internationalized. * FIX: Webchat now receives a private token. * FIX: OAuth2 now has got revised and included state to avoid CSRF attacks. * FIX: Now server will only start with a secure administration password. --- README.md | 8 +- VERSION.md | 7 + deploy/admin.gbapp/dialogs/AdminDialog.ts | 162 +++----- deploy/admin.gbapp/models/AdminModel.ts | 3 + deploy/admin.gbapp/services/GBAdminService.ts | 85 ++++- deploy/admin.gbapp/strings.ts | 21 ++ deploy/core.gbapp/models/GBModel.ts | 19 +- .../services/GBConversationalService.ts | 33 +- deploy/core.gbapp/services/GBCoreService.ts | 11 +- deploy/core.gbapp/services/GBDeployer.ts | 256 +++++++------ deploy/core.gbapp/services/GBMinService.ts | 357 +++++++++++------- deploy/default.gbui/src/GBUIApp.js | 2 +- .../default.gbui/src/players/GBLoginPlayer.js | 2 +- .../src/players/GBMarkdownPlayer.js | 8 +- deploy/kb.gbapp/dialogs/AskDialog.ts | 2 + deploy/kb.gbapp/services/KBService.ts | 8 +- deploy/security.gblib/index.ts | 12 +- package.json | 6 +- src/app.ts | 137 ++++--- 19 files changed, 678 insertions(+), 461 deletions(-) create mode 100644 deploy/admin.gbapp/strings.ts diff --git a/README.md b/README.md index 7dbb7f7d..75125fd5 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,13 @@ here is a list of admin commands related to deploying .gb* files. | deployPackage | Deploy a KB package. Usage **deployPackage** [package-name]. Then, you need to run rebuildIndex. | | undeployPackage | Undeploy a KB. Usage **undeployPackage** [package-name]. | | redeployPackage | Undeploy and then deploys the KB. Usage **redeployPackage** [package-name]. Then, you need to run rebuildIndex. | -| rebuildIndex | Rebuild Azure Search indexes, must be run after **deployPackage** or **redeployPackage**. | +| setupSecurity | Setup connection to user directories. | + +Discontinued commands: + +| Command | Description |Reason | +|-----------------| -----------------------------------------------------------------------------------------------------------------|------| +| rebuildIndex | Rebuild Azure Search indexes, must be run after **deployPackage** or **redeployPackage**. | Now it is called automatically | ### Credits & Inspiration diff --git a/VERSION.md b/VERSION.md index 2635b66b..af72d230 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,5 +1,12 @@ # Release History +## Version 0.1.3 + +* FIX: Admin now is internationalized. +* FIX: Webchat now receives a private token. +* FIX: OAuth2 now has got revised and included state to avoid CSRF attacks. +* FIX: Now server will only start with a secure administration password. + ## Version 0.1.2 * NEW: kb.gbapp now has a complete browser of excel articles. diff --git a/deploy/admin.gbapp/dialogs/AdminDialog.ts b/deploy/admin.gbapp/dialogs/AdminDialog.ts index 00573f5c..7ec9db15 100644 --- a/deploy/admin.gbapp/dialogs/AdminDialog.ts +++ b/deploy/admin.gbapp/dialogs/AdminDialog.ts @@ -42,59 +42,32 @@ import { GBConfigService } from "../../core.gbapp/services/GBConfigService"; import { KBService } from "./../../kb.gbapp/services/KBService"; import { BotAdapter } from "botbuilder"; import { GBAdminService } from "../services/GBAdminService"; +import { Messages } from "../strings"; /** * Dialogs for administration tasks. */ export class AdminDialog extends IGBDialog { - static async undeployPackageCommand(text: any, min: GBMinInstance, dc) { + + static async undeployPackageCommand(text: any, min: GBMinInstance) { let packageName = text.split(" ")[1]; let importer = new GBImporter(min.core); let deployer = new GBDeployer(min.core, importer); - dc.context.sendActivity(`Undeploying package ${packageName}...`); await deployer.undeployPackageFromLocalPath( min.instance, UrlJoin("deploy", packageName) ); - dc.context.sendActivity(`Package ${packageName} undeployed...`); } - static async deployPackageCommand( - text: string, - dc, - deployer: GBDeployer, - min: GBMinInstance + static async deployPackageCommand(text: string, + deployer: GBDeployer ) { let packageName = text.split(" ")[1]; - await dc.context.sendActivity( - `Deploying package ${packageName}... (It may take a few seconds)` - ); let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH"); await deployer.deployPackageFromLocalPath( UrlJoin(additionalPath, packageName) ); - await dc.context.sendActivity( - `Package ${packageName} deployed... Please run rebuildIndex command.` - ); } - - static async rebuildIndexCommand(min: GBMinInstance, dc) { - let search = new AzureSearch( - min.instance.searchKey, - min.instance.searchHost, - min.instance.searchIndex, - min.instance.searchIndexer - ); - dc.context.sendActivity("Rebuilding index..."); - await search.deleteIndex(); - let kbService = new KBService(min.core.sequelize); - await search.createIndex( - kbService.getSearchSchema(min.instance.searchIndex), - "gb" - ); - await dc.context.sendActivity("Index rebuilt."); - } - /** * Setup dialogs flows and define services call. * @@ -107,101 +80,80 @@ export class AdminDialog extends IGBDialog { let importer = new GBImporter(min.core); let deployer = new GBDeployer(min.core, importer); - min.dialogs.add("/adminRat", [ - async dc => { - await AdminDialog.refreshAdminToken(min, dc); - // await dc.context.sendActivity( - // `Deploying package ... (It may take a few seconds)` - // ); - // await AdminDialog.deployPackageCommand( - // "deployPackage ProjectOnline.gbkb", - // dc, - // deployer, - // min - // ); - await dc.endAll(); - } - ]); - - min.dialogs.add("/adminUpdateToken", [ - async (dc, args, next) => { - await dc.endAll(); - let service = new GBAdminService(); - await service.saveValue("authenticatorToken", args.token) - await dc.context.sendActivities([ - { type: 'typing' }, - { type: 'message', text: "Token has been updated." }, - { type: 'message', text: "Please, log out now from the administration work account on next screen." }, - { type: 'delay', value: 4000 }, - ]) - } - ]); - min.dialogs.add("/admin", [ - async (dc, args) => { - const prompt = "Please, authenticate:"; + async dc => { + const locale = dc.context.activity.locale; + const prompt = Messages[locale].authenticate; await dc.prompt("textPrompt", prompt); }, - async (dc, value) => { - let text = value; - const user = min.userState.get(dc.context); - - if (!user.authenticated || text === GBConfigService.get("ADMIN_PASS")) { - user.authenticated = true; + async (dc, password) => { + const locale = dc.context.activity.locale; + if ( + password === GBConfigService.get("ADMIN_PASS") && + GBAdminService.StrongRegex.test(password) + ) { + await dc.context.sendActivity( - "Welcome to Pragmatismo.io GeneralBots Administration." + Messages[locale].welcome ); - await dc.prompt("textPrompt", "Which task do you wanna run now?"); + await dc.prompt("textPrompt", Messages[locale].which_task); } else { + await dc.prompt("textPrompt", Messages[locale].wrong_password); await dc.endAll(); } }, async (dc, value) => { + const locale = dc.context.activity.locale; var text = value; const user = min.userState.get(dc.context); - + let cmdName = text.split(" ")[0]; + dc.context.sendActivity(Messages[locale].working(cmdName)) if (text === "quit") { user.authenticated = false; await dc.replace("/"); - } else if (text === "sync") { - await min.core.syncDatabaseStructure(); - await dc.context.sendActivity("Sync started..."); + } else if (cmdName === "deployPackage") { + await AdminDialog.deployPackageCommand(text, deployer); await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "rebuildIndex") { - await AdminDialog.rebuildIndexCommand(min, dc); + } else if (cmdName === "redeployPackage") { + await AdminDialog.undeployPackageCommand(text, min); + await AdminDialog.deployPackageCommand(text, deployer); + await dc.context.sendActivity(); await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "deployPackage") { - await AdminDialog.deployPackageCommand(text, dc, deployer, min); + } else if (cmdName === "undeployPackage") { + await AdminDialog.undeployPackageCommand(text, min); await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "redeployPackage") { - await AdminDialog.undeployPackageCommand(text, min, dc); - await AdminDialog.deployPackageCommand(text, dc, deployer, min); - await dc.context.sendActivity("Redeploy done."); - await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "undeployPackage") { - await AdminDialog.undeployPackageCommand(text, min, dc); - await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "applyPackage") { - await dc.context.sendActivity("Applying in progress..."); - await min.core.loadInstance(text.split(" ")[1]); - await dc.context.sendActivity("Applying done..."); - await dc.replace("/admin", { firstRun: false }); - } else if (text.split(" ")[0] === "rat") { - await AdminDialog.refreshAdminToken(min, dc); + } else if (cmdName === "setupSecurity") { + await AdminDialog.setupSecurity(min, dc); + } + else{ + await dc.context.sendActivity(Messages[locale].unknown_command); + dc.endAll() + await dc.replace("/answer", { query: text }); } } ]); } - private static async refreshAdminToken(min: any, dc: any) { - let config = { - authenticatorTenant: min.instance.authenticatorTenant, - authenticatorClientID: min.instance.authenticatorClientID - }; - await min.conversationalService.sendEvent(dc, "play", { - playerType: "login", - data: config - }); - await dc.context.sendActivity("Update your Administrative token by Login..."); + private static async setupSecurity(min: any, dc: any) { + const locale = dc.context.activity.locale; + let state = `${min.instance.instanceId}${Math.floor( + Math.random() * 1000000000 + )}`; + await min.adminService.setValue( + min.instance.instanceId, + "AntiCSRFAttackState", + state + ); + let url = `https://login.microsoftonline.com/${ + min.instance.authenticatorTenant + }/oauth2/authorize?client_id=${ + min.instance.authenticatorClientId + }&response_type=code&redirect_uri=${min.instance.botServerUrl}/${ + min.instance.botId + }/token&state=${state}&response_mode=query`; + + await dc.context.sendActivity( + Messages[locale].consent(url) + ); } } diff --git a/deploy/admin.gbapp/models/AdminModel.ts b/deploy/admin.gbapp/models/AdminModel.ts index a91ac851..9c847417 100644 --- a/deploy/admin.gbapp/models/AdminModel.ts +++ b/deploy/admin.gbapp/models/AdminModel.ts @@ -45,6 +45,9 @@ import { export class GuaribasAdmin extends Model { + @Column + instanceId: number; + @Column key: string; diff --git a/deploy/admin.gbapp/services/GBAdminService.ts b/deploy/admin.gbapp/services/GBAdminService.ts index 99e7bcb5..ffedb147 100644 --- a/deploy/admin.gbapp/services/GBAdminService.ts +++ b/deploy/admin.gbapp/services/GBAdminService.ts @@ -30,28 +30,97 @@ | | \*****************************************************************************/ -"use strict" +"use strict"; import { GuaribasAdmin } from "../models/AdminModel"; +import { IGBCoreService } from "botlib"; +import { AuthenticationContext, TokenResponse } from "adal-node"; +const UrlJoin = require("url-join"); export class GBAdminService { + public static StrongRegex = new RegExp( + "^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})" + ); - async saveValue(key: string, value: string): Promise { - let options = { where: {} } - options.where = { key: key } + core: IGBCoreService; + + constructor(core: IGBCoreService) { + this.core = core; + } + + public async setValue( + instanceId: number, + key: string, + value: string + ): Promise { + let options = { where: {} }; + options.where = { key: key }; let admin = await GuaribasAdmin.findOne(options); if (admin == null) { admin = new GuaribasAdmin(); admin.key = key; } admin.value = value; - return admin.save() + admin.instanceId = instanceId; + return admin.save(); } - async getValue(key: string) { - let options = { where: {} } - options.where = { key: key } + public async getValue(instanceId: number, key: string) { + let options = { where: {} }; + options.where = { key: key, instanceId: instanceId }; let obj = await GuaribasAdmin.findOne(options); return Promise.resolve(obj.value); } + + public async acquireElevatedToken(instanceId): Promise { + return new Promise(async (resolve, reject) => { + let instance = await this.core.loadInstanceById(instanceId); + + let expiresOn = new Date(await this.getValue(instanceId, "expiresOn")); + if (expiresOn.getTime() > new Date().getTime()) { + let accessToken = await this.getValue(instanceId, "accessToken"); + resolve(accessToken); + } else { + let authorizationUrl = UrlJoin( + instance.authenticatorAuthorityHostUrl, + instance.authenticatorTenant, + "/oauth2/authorize" + ); + + var authenticationContext = new AuthenticationContext(authorizationUrl); + let refreshToken = await this.getValue(instanceId, "refreshToken"); + let resource = "https://graph.microsoft.com"; + + authenticationContext.acquireTokenWithRefreshToken( + refreshToken, + instance.authenticatorClientId, + instance.authenticatorClientSecret, + resource, + async (err, res) => { + if (err) { + reject(err); + } else { + let tokens = res as TokenResponse; + await this.setValue( + instanceId, + "accessToken", + tokens.accessToken + ); + await this.setValue( + instanceId, + "refreshToken", + tokens.refreshToken + ); + await this.setValue( + instanceId, + "expiresOn", + tokens.expiresOn.toString() + ); + resolve(tokens.accessToken); + } + } + ); + } + }); + } } diff --git a/deploy/admin.gbapp/strings.ts b/deploy/admin.gbapp/strings.ts new file mode 100644 index 00000000..621a74a6 --- /dev/null +++ b/deploy/admin.gbapp/strings.ts @@ -0,0 +1,21 @@ +export const Messages = { + "en-US": { + authenticate: "Please, authenticate:", + welcome: "Welcome to Pragmatismo.io GeneralBots Administration.", + which_task: "Which task do you wanna run now?", + working:(command)=> `I'm working on ${command}`, + unknown_command: text => + `Well, but ${text} is not a administrative General Bots command, I will try to search for it.`, + hi: text => `Hello, ${text}.`, + undeployPackage: text => `Undeploying package ${text}...`, + deployPackage: text => `Deploying package ${text}...`, + redeployPackage: text => `Redeploying package ${text}...`, + packageUndeployed: text => `Package ${text} undeployed...`, + consent: (url)=>`Please, consent access to this app at: [Microsoft Online](${url}).`, + wrong_password: "Sorry, wrong password. Please, try again." + }, + "pt-BR": { + show_video: "Vou te mostrar um vĂ­deo. Por favor, aguarde...", + hi: msg => `Oi, ${msg}.` + } +}; diff --git a/deploy/core.gbapp/models/GBModel.ts b/deploy/core.gbapp/models/GBModel.ts index 03a6d58f..2b45910a 100644 --- a/deploy/core.gbapp/models/GBModel.ts +++ b/deploy/core.gbapp/models/GBModel.ts @@ -65,9 +65,9 @@ export class GuaribasInstance extends Model @AutoIncrement @Column instanceId: number; - + @Column - applicationPrincipal: string; + botServerUrl:string; @Column whoAmIVideo: string; @@ -109,10 +109,21 @@ export class GuaribasInstance extends Model @Column authenticatorTenant: string; + @Column - authenticatorSignUpSignInPolicy: string; + authenticatorAuthorityHostUrl: string; + @Column - authenticatorClientID: string; + authenticatorClientId: string; + + @Column + authenticatorClientSecret: string; + + @Column + cloudSubscriptionId: string; + + @Column + cloudRegion: string; @Column whatsappBotKey: string; diff --git a/deploy/core.gbapp/services/GBConversationalService.ts b/deploy/core.gbapp/services/GBConversationalService.ts index 563655c2..2614ad2e 100644 --- a/deploy/core.gbapp/services/GBConversationalService.ts +++ b/deploy/core.gbapp/services/GBConversationalService.ts @@ -1,4 +1,4 @@ -import { IGBInstance } from 'botlib'; +import { IGBInstance } from "botlib"; /*****************************************************************************\ | ( )_ _ | | _ _ _ __ _ _ __ ___ ___ _ _ | ,_)(_) ___ ___ _ | @@ -44,7 +44,6 @@ import { Messages } from "../strings"; import { AzureText } from "pragmatismo-io-framework"; const Nexmo = require("nexmo"); - export interface LanguagePickerSettings { defaultLocale?: string; supportedLocales?: string[]; @@ -62,30 +61,40 @@ export class GBConversationalService implements IGBConversationalService { } async sendEvent(dc: any, name: string, value: any): Promise { - const msg = MessageFactory.text(""); - msg.value = value; - msg.type = "event"; - msg.name = name; - return dc.context.sendActivity(msg); + if (dc.context.activity.channelId === "webchat") { + const msg = MessageFactory.text(""); + msg.value = value; + msg.type = "event"; + msg.name = name; + return dc.context.sendActivity(msg); + } } - async sendSms(min: GBMinInstance, mobile: string, text: string): Promise { + async sendSms( + min: GBMinInstance, + mobile: string, + text: string + ): Promise { return new Promise((resolve, reject) => { const nexmo = new Nexmo({ apiKey: min.instance.smsKey, - apiSecret: min.instance.smsSecret, + apiSecret: min.instance.smsSecret }); nexmo.message.sendSms( min.instance.smsServiceNumber, mobile, - text, (err, data) => { - if (err) { reject(err) } else { resolve(data) } + text, + (err, data) => { + if (err) { + reject(err); + } else { + resolve(data); + } } ); }); } - async routeNLP(dc: any, min: GBMinInstance, text: string): Promise { // Invokes LUIS. diff --git a/deploy/core.gbapp/services/GBCoreService.ts b/deploy/core.gbapp/services/GBCoreService.ts index 34e53d83..cd50946a 100644 --- a/deploy/core.gbapp/services/GBCoreService.ts +++ b/deploy/core.gbapp/services/GBCoreService.ts @@ -78,7 +78,7 @@ export class GBCoreService implements IGBCoreService { */ constructor() { this.dialect = GBConfigService.get("DATABASE_DIALECT") - this.adminService = new GBAdminService(); + this.adminService = new GBAdminService(this) } /** @@ -269,6 +269,15 @@ export class GBCoreService implements IGBCoreService { return GuaribasInstance.findAll({}); } + + /** + * Loads just one Bot instance by its internal Id. + */ + async loadInstanceById(instanceId: string): Promise { + let options = { where: {instanceId: instanceId} } + return GuaribasInstance.findOne(options); + } + /** * Loads just one Bot instance. */ diff --git a/deploy/core.gbapp/services/GBDeployer.ts b/deploy/core.gbapp/services/GBDeployer.ts index 669eaaca..a59794e7 100644 --- a/deploy/core.gbapp/services/GBDeployer.ts +++ b/deploy/core.gbapp/services/GBDeployer.ts @@ -30,36 +30,37 @@ | | \*****************************************************************************/ -"use strict" +"use strict"; -const logger = require("../../../src/logger") -const Path = require("path") -const UrlJoin = require("url-join") -const Fs = require("fs") -const WaitUntil = require("wait-until") -const express = require("express") +const logger = require("../../../src/logger"); +const Path = require("path"); +const UrlJoin = require("url-join"); +const Fs = require("fs"); +const WaitUntil = require("wait-until"); +const express = require("express"); -import { KBService } from "./../../kb.gbapp/services/KBService" -import { GBImporter } from "./GBImporter" -import { IGBCoreService, IGBInstance } from "botlib" -import { GBConfigService } from "./GBConfigService" -import { GBError } from "botlib" -import { GuaribasPackage } from "../models/GBModel" -import { IGBPackage } from "botlib" +import { KBService } from "./../../kb.gbapp/services/KBService"; +import { GBImporter } from "./GBImporter"; +import { IGBCoreService, IGBInstance } from "botlib"; +import { GBConfigService } from "./GBConfigService"; +import { GBError } from "botlib"; +import { GuaribasPackage, GuaribasInstance } from "../models/GBModel"; +import { IGBPackage } from "botlib"; +import { AzureSearch } from "pragmatismo-io-framework"; /** Deployer service for bots, themes, ai and more. */ export class GBDeployer { - core: IGBCoreService + core: IGBCoreService; - importer: GBImporter + importer: GBImporter; - workDir: string = "./work" + workDir: string = "./work"; - static deployFolder = "deploy" + static deployFolder = "deploy"; constructor(core: IGBCoreService, importer: GBImporter) { - this.core = core - this.importer = importer + this.core = core; + this.importer = importer; } /** @@ -72,103 +73,102 @@ export class GBDeployer { server: any, appPackages: Array ) { - let _this = this + let _this = this; return new Promise((resolve, reject) => { try { - let totalPackages = 0 - let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH") - let paths = [GBDeployer.deployFolder] + let totalPackages = 0; + let additionalPath = GBConfigService.get("ADDITIONAL_DEPLOY_PATH"); + let paths = [GBDeployer.deployFolder]; if (additionalPath) { - paths = paths.concat(additionalPath.toLowerCase().split(";")) + paths = paths.concat(additionalPath.toLowerCase().split(";")); } - let botPackages = new Array() - let gbappPackages = new Array() - let generalPackages = new Array() + let botPackages = new Array(); + let gbappPackages = new Array(); + let generalPackages = new Array(); function doIt(path) { - const isDirectory = source => Fs.lstatSync(source).isDirectory() + const isDirectory = source => Fs.lstatSync(source).isDirectory(); const getDirectories = source => Fs.readdirSync(source) .map(name => Path.join(source, name)) - .filter(isDirectory) + .filter(isDirectory); - let dirs = getDirectories(path) + let dirs = getDirectories(path); dirs.forEach(element => { if (element.startsWith(".")) { - logger.info(`Ignoring ${element}...`) + logger.info(`Ignoring ${element}...`); } else { if (element.endsWith(".gbot")) { - botPackages.push(element) + botPackages.push(element); } else if (element.endsWith(".gbapp")) { - gbappPackages.push(element) + gbappPackages.push(element); } else { - generalPackages.push(element) + generalPackages.push(element); } } - }) + }); } logger.info( `Starting looking for packages (.gbot, .gbtheme, .gbkb, .gbapp)...` - ) + ); paths.forEach(e => { - logger.info(`Looking in: ${e}...`) - doIt(e) - }) + logger.info(`Looking in: ${e}...`); + doIt(e); + }); /** Deploys all .gbapp files first. */ - let appPackagesProcessed = 0 + let appPackagesProcessed = 0; gbappPackages.forEach(e => { - logger.info(`Deploying app: ${e}...`) - // Skips .gbapp inside deploy folder. if (!e.startsWith("deploy")) { + logger.info(`Deploying app: ${e}...`); import(e) .then(m => { - let p = new m.Package() - p.loadPackage(core, core.sequelize) - appPackages.push(p) - logger.info(`App (.gbapp) deployed: ${e}.`) - appPackagesProcessed++ + let p = new m.Package(); + p.loadPackage(core, core.sequelize); + appPackages.push(p); + logger.info(`App (.gbapp) deployed: ${e}.`); + appPackagesProcessed++; }) .catch(err => { - logger.error(`Error deploying App (.gbapp): ${e}: ${err}`) - appPackagesProcessed++ - }) + logger.error(`Error deploying App (.gbapp): ${e}: ${err}`); + appPackagesProcessed++; + }); } else { - appPackagesProcessed++ + appPackagesProcessed++; } - }) + }); WaitUntil() .interval(1000) .times(10) - .condition(function (cb) { - logger.info(`Waiting for app package deployment...`) - cb(appPackagesProcessed == gbappPackages.length) + .condition(function(cb) { + logger.info(`Waiting for app package deployment...`); + cb(appPackagesProcessed == gbappPackages.length); }) - .done(function (result) { + .done(async result => { logger.info(`App Package deployment done.`); - (async () => { - await core.syncDatabaseStructure() - })() + await core.syncDatabaseStructure(); /** Deploys all .gbot files first. */ botPackages.forEach(e => { - logger.info(`Deploying bot: ${e}...`) - _this.deployBot(e) - logger.info(`Bot: ${e} deployed...`) - }) + logger.info(`Deploying bot: ${e}...`); + _this.deployBot(e); + logger.info(`Bot: ${e} deployed...`); + }); /** Then all remaining generalPackages are loaded. */ + generalPackages = generalPackages.filter(p => !p.endsWith(".git")); + generalPackages.forEach(filename => { - let filenameOnly = Path.basename(filename) - logger.info(`Deploying package: ${filename}...`) + let filenameOnly = Path.basename(filename); + logger.info(`Deploying package: ${filename}...`); /** Handles apps for general bots - .gbapp must stay out of deploy folder. */ @@ -178,57 +178,54 @@ export class GBDeployer { ) { /** Themes for bots. */ } else if (Path.extname(filename) === ".gbtheme") { - server.use("/themes/" + filenameOnly, express.static(filename)) + server.use("/themes/" + filenameOnly, express.static(filename)); logger.info( `Theme (.gbtheme) assets accessible at: ${"/themes/" + - filenameOnly}.` - ) + filenameOnly}.` + ); /** Knowledge base for bots. */ } else if (Path.extname(filename) === ".gbkb") { server.use( "/kb/" + filenameOnly + "/subjects", express.static(UrlJoin(filename, "subjects")) - ) + ); logger.info( `KB (.gbkb) assets accessible at: ${"/kb/" + filenameOnly}.` - ) - } else if ( - Path.extname(filename) === ".gbui" || - filename.endsWith(".git") - ) { + ); + } else if (Path.extname(filename) === ".gbui") { // Already Handled } else { /** Unknown package format. */ - let err = new Error(`Package type not handled: ${filename}.`) - reject(err) + let err = new Error(`Package type not handled: ${filename}.`); + reject(err); } - totalPackages++ - }) + totalPackages++; + }); WaitUntil() .interval(100) .times(5) - .condition(function (cb) { - logger.info(`Waiting for package deployment...`) - cb(totalPackages == generalPackages.length) + .condition(function(cb) { + logger.info(`Waiting for package deployment...`); + cb(totalPackages == generalPackages.length); }) - .done(function (result) { + .done(function(result) { if (botPackages.length === 0) { logger.info( "The server is running with no bot instances, at least one .gbot file must be deployed." - ) + ); } else { - logger.info(`Package deployment done.`) + logger.info(`Package deployment done.`); } - resolve() - }) - }) + resolve(); + }); + }); } catch (err) { - logger.error(err) - reject(err) + logger.error(err); + reject(err); } - }) + }); } /** @@ -236,13 +233,13 @@ export class GBDeployer { */ async deployBot(localPath: string): Promise { - let packageType = Path.extname(localPath) - let packageName = Path.basename(localPath) + let packageType = Path.extname(localPath); + let packageName = Path.basename(localPath); let instance = await this.importer.importIfNotExistsBotPackage( packageName, localPath - ) - return instance + ); + return instance; } async deployPackageToStorage( @@ -252,7 +249,7 @@ export class GBDeployer { return GuaribasPackage.create({ packageName: packageName, instanceId: instanceId - }) + }); } deployTheme(localPath: string) { @@ -268,71 +265,86 @@ export class GBDeployer { } async deployPackageFromLocalPath(localPath: string) { - let packageType = Path.extname(localPath) + let packageType = Path.extname(localPath); switch (packageType) { case ".gbot": - return this.deployBot(localPath) + return this.deployBot(localPath); case ".gbtheme": - return this.deployTheme(localPath) + return this.deployTheme(localPath); // PACKAGE: Put in package logic. case ".gbkb": - let service = new KBService(this.core.sequelize) - return service.deployKb(this.core, this, localPath) + let service = new KBService(this.core.sequelize); + return service.deployKb(this.core, this, localPath); case ".gbui": - break + break; default: var err = GBError.create( `GuaribasBusinessError: Unknow package type: ${packageType}.` - ) - Promise.reject(err) - break + ); + Promise.reject(err); + break; } } async undeployPackageFromLocalPath(instance: IGBInstance, localPath: string) { - let packageType = Path.extname(localPath) - let packageName = Path.basename(localPath) + let packageType = Path.extname(localPath); + let packageName = Path.basename(localPath); - let p = await this.getPackageByName(instance.instanceId, packageName) + let p = await this.getPackageByName(instance.instanceId, packageName); switch (packageType) { case ".gbot": // TODO: this.undeployBot(packageName, localPath) - break + break; case ".gbtheme": // TODO: this.undeployTheme(packageName, localPath) - break + break; case ".gbkb": - let service = new KBService(this.core.sequelize) - return service.undeployKbFromStorage(instance, p.packageId) + let service = new KBService(this.core.sequelize); + return service.undeployKbFromStorage(instance, this, p.packageId); case ".gbui": - break + break; default: var err = GBError.create( `GuaribasBusinessError: Unknown package type: ${packageType}.` - ) - Promise.reject(err) - break + ); + Promise.reject(err); + break; } } + public async rebuildIndex(instance: GuaribasInstance) { + let search = new AzureSearch( + instance.searchKey, + instance.searchHost, + instance.searchIndex, + instance.searchIndexer + ); + await search.deleteIndex(); + let kbService = new KBService(this.core.sequelize); + await search.createIndex( + kbService.getSearchSchema(instance.searchIndex), + "gb" + ); + } + async getPackageByName( instanceId: number, packageName: string ): Promise { - var where = { packageName: packageName, instanceId: instanceId } + var where = { packageName: packageName, instanceId: instanceId }; return GuaribasPackage.findOne({ where: where - }) + }); } /** @@ -341,15 +353,15 @@ export class GBDeployer { * */ async scanBootPackage() { - const deployFolder = "deploy" - let bootPackage = GBConfigService.get("BOOT_PACKAGE") + const deployFolder = "deploy"; + let bootPackage = GBConfigService.get("BOOT_PACKAGE"); if (bootPackage === "none") { - return Promise.resolve(true) + return Promise.resolve(true); } else { return this.deployPackageFromLocalPath( UrlJoin(deployFolder, bootPackage) - ) + ); } } } diff --git a/deploy/core.gbapp/services/GBMinService.ts b/deploy/core.gbapp/services/GBMinService.ts index 6a6822a1..67189135 100644 --- a/deploy/core.gbapp/services/GBMinService.ts +++ b/deploy/core.gbapp/services/GBMinService.ts @@ -30,13 +30,15 @@ | | \*****************************************************************************/ -"use strict" +"use strict"; -const { TextPrompt } = require("botbuilder-dialogs") -const UrlJoin = require("url-join") -const express = require("express") -const logger = require("../../../src/logger") -const request = require('request-promise-native') +const { TextPrompt } = require("botbuilder-dialogs"); +const UrlJoin = require("url-join"); +const express = require("express"); +const logger = require("../../../src/logger"); +const request = require("request-promise-native"); +var crypto = require("crypto"); +var AuthenticationContext = require("adal-node").AuthenticationContext; import { BotFrameworkAdapter, @@ -44,28 +46,32 @@ import { ConversationState, MemoryStorage, UserState -} from "botbuilder" +} from "botbuilder"; -import { GBCoreService } from "./GBCoreService" -import { GBConversationalService } from "./GBConversationalService" -import { GBMinInstance, IGBPackage } from "botlib" -import { GBAnalyticsPackage } from "../../analytics.gblib" -import { GBCorePackage } from "../../core.gbapp" -import { GBKBPackage } from "../../kb.gbapp" -import { GBDeployer } from "./GBDeployer" -import { GBSecurityPackage } from "../../security.gblib" -import { GBAdminPackage } from "./../../admin.gbapp/index" -import { GBCustomerSatisfactionPackage } from "../../customer-satisfaction.gbapp" -import { GBWhatsappPackage } from "../../whatsapp.gblib" +import { GBMinInstance, IGBPackage } from "botlib"; +import { GBAnalyticsPackage } from "../../analytics.gblib"; +import { GBCorePackage } from "../../core.gbapp"; +import { GBKBPackage } from "../../kb.gbapp"; +import { GBDeployer } from "./GBDeployer"; +import { GBSecurityPackage } from "../../security.gblib"; +import { GBAdminPackage } from "./../../admin.gbapp/index"; +import { GBCustomerSatisfactionPackage } from "../../customer-satisfaction.gbapp"; +import { GBWhatsappPackage } from "../../whatsapp.gblib"; +import { + IGBAdminService, + IGBCoreService, + IGBConversationalService +} from "botlib"; /** Minimal service layer for a bot. */ export class GBMinService { - core: GBCoreService - conversationalService: GBConversationalService - deployer: GBDeployer + core: IGBCoreService; + conversationalService: IGBConversationalService; + adminService: IGBAdminService; + deployer: GBDeployer; - corePackage = "core.gbai" + corePackage = "core.gbai"; /** * Static initialization of minimal instance. @@ -73,13 +79,15 @@ export class GBMinService { * @param core Basic database services to identify instance, for example. */ constructor( - core: GBCoreService, - conversationalService: GBConversationalService, + core: IGBCoreService, + conversationalService: IGBConversationalService, + adminService: IGBAdminService, deployer: GBDeployer ) { - this.core = core - this.conversationalService = conversationalService - this.deployer = deployer + this.core = core; + this.conversationalService = conversationalService; + this.adminService = adminService; + this.deployer = deployer; } /** @@ -97,37 +105,34 @@ export class GBMinService { server: any, appPackages: Array ): Promise { - // Serves default UI on root address '/'. - let uiPackage = "default.gbui" + let uiPackage = "default.gbui"; server.use( "/", express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build")) - ) + ); // Loads all bot instances from storage and starting loading them. - let instances = await this.core.loadInstances() + let instances = await this.core.loadInstances(); Promise.all( instances.map(async instance => { - // Gets the authorization key for each instance from Bot Service. - let webchatToken = await this.getWebchatToken(instance) + let webchatToken = await this.getWebchatToken(instance); // Serves the bot information object via HTTP so clients can get // instance information stored on server. server.get("/instances/:botId", (req, res) => { (async () => { - // Returns the instance object to clients requesting bot info. - let botId = req.params.botId - let instance = await this.core.loadInstance(botId) + let botId = req.params.botId; + let instance = await this.core.loadInstance(botId); if (instance) { - let speechToken = await this.getSTSToken(instance) + let speechToken = await this.getSTSToken(instance); res.send( JSON.stringify({ @@ -138,30 +143,30 @@ export class GBMinService { speechToken: speechToken, conversationId: webchatToken.conversationId, authenticatorTenant: instance.authenticatorTenant, - authenticatorClientID: instance.authenticatorClientID + authenticatorClientId: instance.authenticatorClientId }) - ) + ); } else { - let error = `Instance not found: ${botId}.` - res.sendStatus(error) - logger.error(error) + let error = `Instance not found: ${botId}.`; + res.sendStatus(error); + logger.error(error); } - })() - }) + })(); + }); // Build bot adapter. var { min, adapter, conversationState } = await this.buildBotAdapter( instance - ) + ); // Call the loadBot context.activity for all packages. - this.invokeLoadBot(appPackages, min, server) + this.invokeLoadBot(appPackages, min, server); // Serves individual URL for each bot conversational interface... - let url = `/api/messages/${instance.botId}` + let url = `/api/messages/${instance.botId}`; server.post(url, async (req, res) => { return this.receiver( adapter, @@ -171,20 +176,107 @@ export class GBMinService { min, instance, appPackages - ) - }) + ); + }); logger.info( `GeneralBots(${instance.engineName}) listening on: ${url}.` - ) + ); // Serves individual URL for each bot user interface. - let uiUrl = `/${instance.botId}` + let uiUrl = `/${instance.botId}`; server.use( uiUrl, express.static(UrlJoin(GBDeployer.deployFolder, uiPackage, "build")) - ) - logger.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`) + ); + + logger.info(`Bot UI ${uiPackage} accessible at: ${uiUrl}.`); + let state = `${instance.instanceId}${Math.floor( + Math.random() * 1000000000 + )}`; + + // 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. + server.get(`/${min.instance.botId}/auth`, function(req, res) { + let authorizationUrl = UrlJoin( + min.instance.authenticatorAuthorityHostUrl, + min.instance.authenticatorTenant, + "/oauth2/authorize" + ); + authorizationUrl = `${authorizationUrl}?response_type=code&client_id=${ + min.instance.authenticatorClientId + }&redirect_uri=${min.instance.botServerUrl}/${ + min.instance.botId + }/token`; + + res.redirect(authorizationUrl); + }); + + // 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. + + server.get(`/${min.instance.botId}/token`, async (req, res) => { + let state = await min.adminService.getValue( + min.instance.instanceId, + "AntiCSRFAttackState" + ); + + if (req.query.state != state) { + let msg = + "WARNING: state field was not provided as anti-CSRF token"; + logger.error(msg); + throw new Error(msg); + } + + var authenticationContext = new AuthenticationContext( + UrlJoin( + min.instance.authenticatorAuthorityHostUrl, + min.instance.authenticatorTenant + ) + ); + + let resource = "https://graph.microsoft.com"; + + authenticationContext.acquireTokenWithAuthorizationCode( + req.query.code, + UrlJoin(instance.botServerUrl, min.instance.botId, "/token"), + resource, + instance.authenticatorClientId, + instance.authenticatorClientSecret, + async (err, token) => { + if (err) { + let msg = `Error acquiring token: ${err}`; + logger.error(msg); + res.send(msg); + } else { + await this.adminService.setValue( + instance.instanceId, + "refreshToken", + token.refreshToken + ); + await this.adminService.setValue( + instance.instanceId, + "accessToken", + token.accessToken + ); + await this.adminService.setValue( + instance.instanceId, + "expiresOn", + token.expiresOn.toString() + ); + await this.adminService.setValue( + instance.instanceId, + "AntiCSRFAttackState", + null + ); + + res.redirect(min.instance.botServerUrl); + } + } + ); + }); // Setups handlers. // send: function (context.activity, next) { @@ -201,32 +293,33 @@ export class GBMinService { // ) // next() }) - ) + ); } private async buildBotAdapter(instance: any) { let adapter = new BotFrameworkAdapter({ appId: instance.marketplaceId, appPassword: instance.marketplacePassword - }) + }); - const storage = new MemoryStorage() - const conversationState = new ConversationState(storage) - const userState = new UserState(storage) - adapter.use(new BotStateSet(conversationState, userState)) + const storage = new MemoryStorage(); + const conversationState = new ConversationState(storage); + const userState = new UserState(storage); + adapter.use(new BotStateSet(conversationState, userState)); // The minimal bot is built here. - let min = new GBMinInstance() - min.botId = instance.botId - min.bot = adapter - min.userState = userState - min.core = this.core - min.conversationalService = this.conversationalService - min.instance = await this.core.loadInstance(min.botId) - min.dialogs.add("textPrompt", new TextPrompt()) + let min = new GBMinInstance(); + min.botId = instance.botId; + min.bot = adapter; + min.userState = userState; + min.core = this.core; + min.conversationalService = this.conversationalService; + min.adminService = this.adminService; + min.instance = await this.core.loadInstance(min.botId); + min.dialogs.add("textPrompt", new TextPrompt()); - return { min, adapter, conversationState } + return { min, adapter, conversationState }; } private invokeLoadBot(appPackages: any[], min: any, server: any) { @@ -244,19 +337,18 @@ export class GBMinService { GBCustomerSatisfactionPackage, GBWhatsappPackage ].forEach(sysPackage => { - logger.info(`Loading sys package: ${sysPackage.name}...`) - let p = Object.create(sysPackage.prototype) as IGBPackage - p.loadBot(min) - e.sysPackages.push(p) + let p = Object.create(sysPackage.prototype) as IGBPackage; + p.loadBot(min); + e.sysPackages.push(p); if (sysPackage.name === "GBWhatsappPackage") { - let url = "/instances/:botId/whatsapp" + let url = "/instances/:botId/whatsapp"; server.post(url, (req, res) => { - p["channel"].received(req, res) - }) + p["channel"].received(req, res); + }); } - }, this) - e.loadBot(min) - }, this) + }, this); + e.loadBot(min); + }, this); } /** @@ -272,12 +364,11 @@ export class GBMinService { appPackages: any[] ) { return adapter.processActivity(req, res, async context => { - try { - const state = conversationState.get(context) - const dc = min.dialogs.createContext(context, state) - dc.context.activity.locale = "en-US" - const user = min.userState.get(dc.context) + const state = conversationState.get(context); + const dc = min.dialogs.createContext(context, state); + dc.context.activity.locale = "en-US"; + const user = min.userState.get(dc.context); if (!user.loaded) { await min.conversationalService.sendEvent(dc, "loadInstance", { @@ -285,99 +376,95 @@ export class GBMinService { botId: instance.botId, theme: instance.theme, secret: instance.webchatKey - }) - user.loaded = true - user.subjects = [] + }); + user.loaded = true; + user.subjects = []; } logger.info( - `[User]: ${context.activity.type}, ChannelID: ${ - context.activity.channelId - } Text: ${context.activity.text}.` - ) + `User>: ${context.activity.text} (${context.activity.type}, ${ + context.activity.name + }, ${context.activity.channelId}, {context.activity.value})` + ); if ( context.activity.type === "conversationUpdate" && context.activity.membersAdded.length > 0 ) { - - let member = context.activity.membersAdded[0] + let member = context.activity.membersAdded[0]; if (member.name === "GeneralBots") { - logger.info(`Bot added to conversation, starting chat...`) + logger.info(`Bot added to conversation, starting chat...`); appPackages.forEach(e => { - e.onNewSession(min, dc) - }) + e.onNewSession(min, dc); + }); // Processes the root dialog. - await dc.begin("/") - + await dc.begin("/"); } else { - logger.info(`Member added to conversation: ${member.name}`) + logger.info(`Member added to conversation: ${member.name}`); } // Processes messages. - } else if (context.activity.type === "message") { - // Checks for /admin request. if (context.activity.text === "admin") { - await dc.begin("/admin") + await dc.begin("/admin"); // Checks for /menu JSON signature. - - } else if (context.activity.text.startsWith("{\"title\"")) { - await dc.begin("/menu", { data: JSON.parse(context.activity.text) }) + } else if (context.activity.text.startsWith('{"title"')) { + await dc.begin("/menu", { + data: JSON.parse(context.activity.text) + }); // Otherwise, continue to the active dialog in the stack. - } else { - if (dc.activeDialog) { - await dc.continue() + await dc.continue(); } else { - await dc.begin("/answer", {query: context.activity.text}) + await dc.begin("/answer", { query: context.activity.text }); } } // Processes events. - } else if (context.activity.type === "event") { - // Empties dialog stack before going to the target. - await dc.endAll() + await dc.endAll(); if (context.activity.name === "whoAmI") { - await dc.begin("/whoAmI") + await dc.begin("/whoAmI"); } else if (context.activity.name === "showSubjects") { - await dc.begin("/menu") + await dc.begin("/menu"); } else if (context.activity.name === "giveFeedback") { await dc.begin("/feedback", { fromMenu: true - }) + }); } else if (context.activity.name === "showFAQ") { - await dc.begin("/faq") + await dc.begin("/faq"); } else if (context.activity.name === "answerEvent") { await dc.begin("/answerEvent", { questionId: (context.activity as any).data, fromFaq: true - }) - + }); } else if (context.activity.name === "quality") { - await dc.begin("/quality", { score: (context.activity as any).data }) + await dc.begin("/quality", { + score: (context.activity as any).data + }); } else if (context.activity.name === "updateToken") { - let token = (context.activity as any).data - await dc.begin("/adminUpdateToken", { token: token }) + let token = (context.activity as any).data; + await dc.begin("/adminUpdateToken", { token: token }); } else { - await dc.continue() + await dc.continue(); } } } catch (error) { - let msg = `Error in main activity: ${error.message} ${error.stack? error.stack:""}` - logger.error(msg) + let msg = `Error in main activity: ${error.message} ${ + error.stack ? error.stack : "" + }`; + logger.error(msg); } - }) + }); } /** @@ -393,15 +480,15 @@ export class GBMinService { headers: { Authorization: `Bearer ${instance.webchatKey}` } - } + }; try { - let json = await request(options) - return Promise.resolve(JSON.parse(json)) + let json = await request(options); + return Promise.resolve(JSON.parse(json)); } catch (error) { - let msg = `Error calling Direct Line client, verify Bot endpoint on the cloud. Error is: ${error}.` - logger.error(msg) - return Promise.reject(msg) + let msg = `Error calling Direct Line client, verify Bot endpoint on the cloud. Error is: ${error}.`; + logger.error(msg); + return Promise.reject(msg); } } @@ -420,14 +507,14 @@ export class GBMinService { headers: { "Ocp-Apim-Subscription-Key": instance.speechKey } - } + }; try { - return await request(options) + return await request(options); } catch (error) { - let msg = `Error calling Speech to Text client. Error is: ${error}.` - logger.error(msg) - return Promise.reject(msg) + let msg = `Error calling Speech to Text client. Error is: ${error}.`; + logger.error(msg); + return Promise.reject(msg); } } } diff --git a/deploy/default.gbui/src/GBUIApp.js b/deploy/default.gbui/src/GBUIApp.js index ce94b96d..45901fc4 100644 --- a/deploy/default.gbui/src/GBUIApp.js +++ b/deploy/default.gbui/src/GBUIApp.js @@ -149,7 +149,7 @@ class GBUIApp extends React.Component { let graphScopes = ["Directory.AccessAsUser.All"]; let userAgentApplication = new UserAgentApplication( - this.state.instanceClient.authenticatorClientID, + this.state.instanceClient.authenticatorClientId, authority, function(errorDesc, token, error, tokenType) { if (error) { diff --git a/deploy/default.gbui/src/players/GBLoginPlayer.js b/deploy/default.gbui/src/players/GBLoginPlayer.js index ab264703..b9ec66f2 100644 --- a/deploy/default.gbui/src/players/GBLoginPlayer.js +++ b/deploy/default.gbui/src/players/GBLoginPlayer.js @@ -58,7 +58,7 @@ class GBLoginPlayer extends React.Component { let graphScopes = ["Directory.AccessAsUser.All"]; let userAgentApplication = new UserAgentApplication( - this.state.login.authenticatorClientID, + this.state.login.authenticatorClientId, authority, function (errorDesc, token, error, tokenType) { if (error) { diff --git a/deploy/default.gbui/src/players/GBMarkdownPlayer.js b/deploy/default.gbui/src/players/GBMarkdownPlayer.js index 40d4d813..a4a60ff0 100644 --- a/deploy/default.gbui/src/players/GBMarkdownPlayer.js +++ b/deploy/default.gbui/src/players/GBMarkdownPlayer.js @@ -119,14 +119,14 @@ class GBMarkdownPlayer extends Component { } if (this.state.prevId) { - prev = this.sendAnswer(this.state.prevId)}> + prev = this.sendAnswer(this.state.prevId)}> Back } if (this.state.nextId) { - next = this.sendAnswer(this.state.nextId)}> + next = this.sendAnswer(this.state.nextId)}> Next } diff --git a/deploy/kb.gbapp/dialogs/AskDialog.ts b/deploy/kb.gbapp/dialogs/AskDialog.ts index 9777d845..d2493cea 100644 --- a/deploy/kb.gbapp/dialogs/AskDialog.ts +++ b/deploy/kb.gbapp/dialogs/AskDialog.ts @@ -74,6 +74,8 @@ export class AskDialog extends IGBDialog { dc, answer ); + + await dc.replace("/ask", { isReturning: true }); } }]) diff --git a/deploy/kb.gbapp/services/KBService.ts b/deploy/kb.gbapp/services/KBService.ts index aff32b26..684e990d 100644 --- a/deploy/kb.gbapp/services/KBService.ts +++ b/deploy/kb.gbapp/services/KBService.ts @@ -442,7 +442,7 @@ export class KBService { } async sendAnswer(conversationalService: IGBConversationalService, - dc: any, answer: GuaribasAnswer): Promise { + dc: any, answer: GuaribasAnswer) { if (answer.content.endsWith('.mp4')) { await conversationalService.sendEvent(dc, "play", { @@ -560,6 +560,7 @@ export class KBService { async undeployKbFromStorage( instance: IGBInstance, + deployer: GBDeployer, packageId: number ) { @@ -576,8 +577,7 @@ export class KBService { where: { instanceId: instance.instanceId, packageId: packageId } }) - return Promise.resolve() - + await deployer.rebuildIndex(instance) } /** @@ -599,6 +599,8 @@ export class KBService { instance.instanceId, packageName) await this.importKbPackage(localPath, p, instance) + + deployer.rebuildIndex(instance) logger.info(`[GBDeployer] Finished import of ${localPath}`) } } diff --git a/deploy/security.gblib/index.ts b/deploy/security.gblib/index.ts index eb3d8302..a803febb 100644 --- a/deploy/security.gblib/index.ts +++ b/deploy/security.gblib/index.ts @@ -48,20 +48,22 @@ export class GBSecurityPackage implements IGBPackage { GuaribasUser, GuaribasUserGroup ]) + + core } - + unloadPackage(core: IGBCoreService): void { - + } loadBot(min: GBMinInstance): void { - + } unloadBot(min: GBMinInstance): void { - + } onNewSession(min: GBMinInstance, dc: any): void { - + } } diff --git a/package.json b/package.json index 9e69283b..555622cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "botserver", - "version": "0.1.2", + "version": "0.1.3", "description": "General Bot Community Edition open-core server.", "main": "./src/app.ts", "homepage": "http://www.generalbot.com", @@ -30,6 +30,8 @@ "node": ">=8.9.4" }, "dependencies": { + "@microsoft/microsoft-graph-client": "^1.3.0", + "adal-node": "^0.1.28", "async": "^2.6.1", "async-promises": "^0.2.1", "body-parser": "^1.18.3", @@ -39,7 +41,7 @@ "botbuilder-choices": "^4.0.0-preview1.2", "botbuilder-dialogs": "^4.0.0-preview1.2", "botbuilder-prompts": "^4.0.0-preview1.2", - "botlib": "^0.1.0", + "botlib": "^0.1.1", "chokidar": "^2.0.4", "csv-parse": "^3.0.0", "dotenv-extended": "^2.3.0", diff --git a/src/app.ts b/src/app.ts index 7dd72ce8..0be98eda 100644 --- a/src/app.ts +++ b/src/app.ts @@ -31,100 +31,123 @@ | | \*****************************************************************************/ -"use strict" +"use strict"; -const UrlJoin = require("url-join") -const logger = require("./logger") -const express = require("express") -const bodyParser = require("body-parser") +const UrlJoin = require("url-join"); +const logger = require("./logger"); +const express = require("express"); +const bodyParser = require("body-parser"); +const MicrosoftGraph = require("@microsoft/microsoft-graph-client"); -import { Sequelize } from "sequelize-typescript" -import { GBConfigService } from "../deploy/core.gbapp/services/GBConfigService" -import { GBConversationalService } from "../deploy/core.gbapp/services/GBConversationalService" -import { GBMinService } from "../deploy/core.gbapp/services/GBMinService" -import { GBDeployer } from "../deploy/core.gbapp/services/GBDeployer" -import { GBWhatsappPackage } from './../deploy/whatsapp.gblib/index' -import { GBCoreService } from "../deploy/core.gbapp/services/GBCoreService" -import { GBImporter } from "../deploy/core.gbapp/services/GBImporter" -import { GBAnalyticsPackage } from "../deploy/analytics.gblib" -import { GBCorePackage } from "../deploy/core.gbapp" -import { GBKBPackage } from '../deploy/kb.gbapp' -import { GBSecurityPackage } from '../deploy/security.gblib' -import { GBAdminPackage } from '../deploy/admin.gbapp/index' -import { GBCustomerSatisfactionPackage } from "../deploy/customer-satisfaction.gbapp" -import { IGBPackage } from 'botlib' +import { Sequelize } from "sequelize-typescript"; +import { GBConfigService } from "../deploy/core.gbapp/services/GBConfigService"; +import { GBConversationalService } from "../deploy/core.gbapp/services/GBConversationalService"; +import { GBMinService } from "../deploy/core.gbapp/services/GBMinService"; +import { GBDeployer } from "../deploy/core.gbapp/services/GBDeployer"; +import { GBWhatsappPackage } from "./../deploy/whatsapp.gblib/index"; +import { GBCoreService } from "../deploy/core.gbapp/services/GBCoreService"; +import { GBImporter } from "../deploy/core.gbapp/services/GBImporter"; +import { GBAnalyticsPackage } from "../deploy/analytics.gblib"; +import { GBCorePackage } from "../deploy/core.gbapp"; +import { GBKBPackage } from "../deploy/kb.gbapp"; +import { GBSecurityPackage } from "../deploy/security.gblib"; +import { GBAdminPackage } from "../deploy/admin.gbapp/index"; +import { GBCustomerSatisfactionPackage } from "../deploy/customer-satisfaction.gbapp"; +import { IGBPackage } from "botlib"; +import { GBAdminService } from "../deploy/admin.gbapp/services/GBAdminService"; -let appPackages = new Array() +let appPackages = new Array(); /** * General Bots open-core entry point. */ export class GBServer { - /** Program entry-point. */ static run() { - // Creates a basic HTTP server that will serve several URL, one for each // bot instance. This allows the same server to attend multiple Bot on // the Marketplace until GB get serverless. - let port = process.env.port || process.env.PORT || 4242 - logger.info(`The Bot Server is in STARTING mode...`) - let server = express() + let port = process.env.port || process.env.PORT || 4242; + logger.info(`The Bot Server is in STARTING mode...`); + let server = express(); - server.use(bodyParser.json()) // to support JSON-encoded bodies - server.use(bodyParser.urlencoded({ // to support URL-encoded bodies - extended: true - })) + server.use(bodyParser.json()); // to support JSON-encoded bodies + server.use( + bodyParser.urlencoded({ + // to support URL-encoded bodies + extended: true + }) + ); server.listen(port, () => { - (async () => { try { - - logger.info(`Accepting connections on ${port}...`) + logger.info(`Accepting connections on ${port}...`); // Reads basic configuration, initialize minimal services. - GBConfigService.init() - let core = new GBCoreService() - await core.initDatabase() + GBConfigService.init(); + let core = new GBCoreService(); + await core.initDatabase(); // Boot a bot package if any. - logger.info(`Starting instances...`) - let deployer = new GBDeployer(core, new GBImporter(core)) + logger.info(`Starting instances...`); + let deployer = new GBDeployer(core, new GBImporter(core)); // Build a minimal bot instance for each .gbot deployment. - let conversationalService = new GBConversationalService(core) - let minService = new GBMinService(core, conversationalService, deployer); + let conversationalService = new GBConversationalService(core); + let adminService = new GBAdminService(core); + let password = GBConfigService.get("ADMIN_PASS"); + + if (!GBAdminService.StrongRegex.test(password)) { + throw new Error( + "STOP: Please, define a really strong password in ADMIN_PASS environment variable before running the server." + ); + } + + let minService = new GBMinService( + core, + conversationalService, + adminService, + deployer + ); // NOTE: the semicolon is necessary before this line. // Loads all system packages. - [GBAdminPackage, GBAnalyticsPackage, GBCorePackage, GBSecurityPackage, - GBKBPackage, GBCustomerSatisfactionPackage, GBWhatsappPackage].forEach(e => { - logger.info(`Loading sys package: ${e.name}...`) - let p = Object.create(e.prototype) as IGBPackage - p.loadPackage(core, core.sequelize) - }) + [ + GBAdminPackage, + GBAnalyticsPackage, + GBCorePackage, + GBSecurityPackage, + GBKBPackage, + GBCustomerSatisfactionPackage, + GBWhatsappPackage + ].forEach(e => { + logger.info(`Loading sys package: ${e.name}...`); + let p = Object.create(e.prototype) as IGBPackage; + p.loadPackage(core, core.sequelize); + }); - await deployer.deployPackages(core, server, appPackages) - logger.info(`The Bot Server is in RUNNING mode...`) - - let instance = await minService.buildMin(server, appPackages) - logger.info(`Instance loaded: ${instance.botId}...`) - return core + logger.info(`Deploying packages.`); + await deployer.deployPackages(core, server, appPackages); + logger.info(`Building minimal instances.`); + await minService.buildMin(server, appPackages); + + logger.info(`All instances are now loaded and available.`); + logger.info(`The Bot Server is in RUNNING mode...`); + return core; } catch (err) { - logger.info(err) + logger.info(`STOP: ${err} ${err.stack ? err.stack : ""}`); } - - })() - }) + })(); + }); } } // First line to run. -GBServer.run() \ No newline at end of file +GBServer.run();