├── Modern.Provisioning.Async.Function ├── Modern.Provisioning.Async.Function │ ├── host.json │ ├── local.settings.json │ ├── Modern.Provisioning.Async.Function.csproj.user │ ├── Modern.Provisioning.Async.Function.csproj │ ├── ModernProvisioning.cs │ └── Graph.cs └── Modern.Provisioning.Async.Function.sln ├── VeronicaBot ├── iisnode.yml ├── .gitignore ├── .vscode │ └── launch.json ├── package.json ├── readme.md ├── web.config └── app.js ├── images ├── Flow.PNG ├── Azure_Bot.png ├── AAD_Key_Secret.PNG ├── Azure_Bot_Build.PNG ├── Azure_Bot_Files.PNG ├── Preview_Teams.PNG ├── Azure_Bot_Config.PNG ├── AAD_App_Registration.PNG ├── AAD_Read_Write_Group.PNG ├── AAD_Read_Write_Items.PNG ├── Azure_Bot_DirectLine.PNG ├── Preview_SharePoint.PNG ├── Azure_Bot_AppSettings.PNG └── Modern-Provisioning-Architecture.png ├── react-provisioning-bot ├── .vscode │ ├── extensions.json │ ├── settings.json │ └── launch.json ├── config │ ├── copy-assets.json │ ├── write-manifests.json │ ├── deploy-azure-storage.json │ ├── config.json │ ├── package-solution.json │ ├── serve.json │ └── tslint.json ├── .yo-rc.json ├── src │ └── extensions │ │ └── veronicaBot │ │ ├── components │ │ ├── IGraphBotState.ts │ │ ├── IGraphBotProps.ts │ │ ├── IGraphBotSettings.ts │ │ ├── GraphBot.module.scss │ │ └── GraphBot.tsx │ │ ├── loc │ │ ├── en-us.js │ │ └── myStrings.d.ts │ │ ├── VeronicaBotApplicationCustomizer.manifest.json │ │ └── VeronicaBotApplicationCustomizer.ts ├── gulpfile.js ├── sharepoint │ └── assets │ │ └── elements.xml ├── .gitignore ├── .editorconfig ├── tsconfig.json ├── README.md └── package.json ├── Flow └── AutomatedProvisioningScenario_20180616133022.zip ├── .gitignore ├── ProvisioningArtifacts ├── set-tenant-properties.ps1 └── create-sharepoint-list.ps1 └── readme.md /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/host.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /VeronicaBot/iisnode.yml: -------------------------------------------------------------------------------- 1 | nodeProcessCommandLine: "D:\Program Files (x86)\nodejs\6.9.1\node.exe" -------------------------------------------------------------------------------- /images/Flow.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Flow.PNG -------------------------------------------------------------------------------- /VeronicaBot/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.js.map 3 | package-lock.json 4 | .vscode/ 5 | launch.json -------------------------------------------------------------------------------- /images/Azure_Bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot.png -------------------------------------------------------------------------------- /images/AAD_Key_Secret.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/AAD_Key_Secret.PNG -------------------------------------------------------------------------------- /images/Azure_Bot_Build.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot_Build.PNG -------------------------------------------------------------------------------- /images/Azure_Bot_Files.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot_Files.PNG -------------------------------------------------------------------------------- /images/Preview_Teams.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Preview_Teams.PNG -------------------------------------------------------------------------------- /images/Azure_Bot_Config.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot_Config.PNG -------------------------------------------------------------------------------- /images/AAD_App_Registration.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/AAD_App_Registration.PNG -------------------------------------------------------------------------------- /images/AAD_Read_Write_Group.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/AAD_Read_Write_Group.PNG -------------------------------------------------------------------------------- /images/AAD_Read_Write_Items.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/AAD_Read_Write_Items.PNG -------------------------------------------------------------------------------- /images/Azure_Bot_DirectLine.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot_DirectLine.PNG -------------------------------------------------------------------------------- /images/Preview_SharePoint.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Preview_SharePoint.PNG -------------------------------------------------------------------------------- /react-provisioning-bot/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "msjsdiag.debugger-for-chrome" 4 | ] 5 | } -------------------------------------------------------------------------------- /images/Azure_Bot_AppSettings.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Azure_Bot_AppSettings.PNG -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/local.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | } 5 | } -------------------------------------------------------------------------------- /images/Modern-Provisioning-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/images/Modern-Provisioning-Architecture.png -------------------------------------------------------------------------------- /Flow/AutomatedProvisioningScenario_20180616133022.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giuleon/O365ModernProvisioning/HEAD/Flow/AutomatedProvisioningScenario_20180616133022.zip -------------------------------------------------------------------------------- /react-provisioning-bot/config/copy-assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/copy-assets.schema.json", 3 | "deployCdnPath": "temp/deploy" 4 | } 5 | -------------------------------------------------------------------------------- /react-provisioning-bot/config/write-manifests.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/write-manifests.schema.json", 3 | "cdnBasePath": "" 4 | } -------------------------------------------------------------------------------- /react-provisioning-bot/.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "@microsoft/generator-sharepoint": { 3 | "version": "1.4.1", 4 | "libraryName": "react-provisioning-bot", 5 | "libraryId": "34eb4b8e-de24-4f07-99b7-b28c2f459b54", 6 | "environment": "spo" 7 | } 8 | } -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/components/IGraphBotState.ts: -------------------------------------------------------------------------------- 1 | import IGraphBotSettings from "./IGraphBotSettings"; 2 | 3 | interface IPageHeaderState { 4 | showPanel?: boolean; 5 | isBotInitializing?: boolean; 6 | } 7 | 8 | export default IPageHeaderState; 9 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/components/IGraphBotProps.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCustomizerContext } from "@microsoft/sp-application-base"; 2 | 3 | interface IGraphBotProps { 4 | context: ApplicationCustomizerContext; 5 | } 6 | 7 | export default IGraphBotProps; 8 | -------------------------------------------------------------------------------- /react-provisioning-bot/gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const gulp = require('gulp'); 4 | const build = require('@microsoft/sp-build-web'); 5 | build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`); 6 | 7 | build.initialize(gulp); 8 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/loc/en-us.js: -------------------------------------------------------------------------------- 1 | define([], function() { 2 | return { 3 | "Title": "VeronicaBotApplicationCustomizer", 4 | "GraphBotButtonLabel": "My intranet assistant", 5 | "GraphBotInitializationMessage": "Please wait during the conversation setup." 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /react-provisioning-bot/config/deploy-azure-storage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/deploy-azure-storage.schema.json", 3 | "workingDir": "./temp/deploy/", 4 | "account": "", 5 | "container": "react-provisioning-bot", 6 | "accessKey": "" 7 | } -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/components/IGraphBotSettings.ts: -------------------------------------------------------------------------------- 1 | interface IGraphBotSettings { 2 | /** 3 | * The bot application id 4 | */ 5 | BotId: string; 6 | 7 | /** 8 | * The secret key for the bot "Direct Line" channel 9 | */ 10 | DirectLineSecret: string; 11 | } 12 | 13 | export default IGraphBotSettings; 14 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/loc/myStrings.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IVeronicaBotApplicationCustomizerStrings { 2 | Title: string; 3 | GraphBotButtonLabel: string; 4 | GraphBotInitializationMessage: string; 5 | } 6 | 7 | declare module 'VeronicaBotApplicationCustomizerStrings' { 8 | const strings: IVeronicaBotApplicationCustomizerStrings; 9 | export = strings; 10 | } 11 | -------------------------------------------------------------------------------- /react-provisioning-bot/sharepoint/assets/elements.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/components/GraphBot.module.scss: -------------------------------------------------------------------------------- 1 | .overlayList { 2 | z-index: 1; 3 | display: flex; 4 | justify-content: center; 5 | align-items: center; 6 | } 7 | 8 | .banner { 9 | background-color: "[theme: themePrimary]"; 10 | text-align: left; 11 | 12 | &__chatButton, &__chatButton:hover { 13 | color: white; 14 | } 15 | 16 | &__chatButtonIcon, &__chatButtonIcon:hover { 17 | color: white 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /react-provisioning-bot/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | // Configure glob patterns for excluding files and folders in the file explorer. 4 | "files.exclude": { 5 | "**/.git": true, 6 | "**/.DS_Store": true, 7 | "**/bower_components": true, 8 | "**/coverage": true, 9 | "**/lib-amd": true, 10 | "src/**/*.scss.ts": true 11 | }, 12 | "typescript.tsdk": ".\\node_modules\\typescript\\lib" 13 | } -------------------------------------------------------------------------------- /react-provisioning-bot/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | dist 11 | lib 12 | solution 13 | temp 14 | *.sppkg 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Visual Studio files 23 | .ntvs_analysis.dat 24 | .vs 25 | bin 26 | obj 27 | 28 | # Resx Generated Code 29 | *.resx.ts 30 | 31 | # Styles Generated Code 32 | *.scss.ts 33 | -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function.csproj.user: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <_LastSelectedProfileId>C:\solutions\O365ModernProvisioning\Modern.Provisioning.Async.Function\Modern.Provisioning.Async.Function\Properties\PublishProfiles\FunctionAppModernProvisioningAsync - Web Deploy.pubxml 5 | 6 | -------------------------------------------------------------------------------- /VeronicaBot/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceFolder}\\app.js", 12 | "cwd": "${workspaceFolder}", 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Dependency directories 7 | node_modules 8 | 9 | # Build generated files 10 | dist 11 | lib 12 | solution 13 | temp 14 | *.sppkg 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # OSX 20 | .DS_Store 21 | 22 | # Visual Studio files 23 | .ntvs_analysis.dat 24 | .vs 25 | bin 26 | obj 27 | 28 | # Resx Generated Code 29 | *.resx.ts 30 | 31 | # Styles Generated Code 32 | *.scss.ts 33 | 34 | # Sensitive data 35 | publish.js 36 | PostDeployScripts 37 | PublishProfiles -------------------------------------------------------------------------------- /VeronicaBot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello-chatconnector", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "azure-publish": "node publish.js" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "botbuilder": "^3.13.1", 14 | "botbuilder-azure": "^3.0.4", 15 | "q": "^1.5.1", 16 | "restify": "^5.0.0" 17 | }, 18 | "devDependencies": { 19 | "request": "^2.81.0", 20 | "zip-folder": "^1.0.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ProvisioningArtifacts/set-tenant-properties.ps1: -------------------------------------------------------------------------------- 1 | # Set your own values here 2 | $SiteCollectionUrl = "" 3 | $BotId = "" 4 | $BotDirectLineSecret = "" 5 | 6 | Connect-PnPOnline -Url $SiteCollectionUrl -UseWebLogin 7 | 8 | # Set the environment settings in the tenant property bag 9 | Set-PnPStorageEntity -Key "PnPGraphBot_BotId" -Value $BotId -Comment $Comment -Description "Bot ID" 10 | Set-PnPStorageEntity -Key "PnPGraphBot_BotDirectLineSecret" -Value $BotDirectLineSecret -Comment $Comment -Description "Bot Direct Line Secret" 11 | -------------------------------------------------------------------------------- /react-provisioning-bot/.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | 10 | # change these settings to your own preference 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # we recommend you to keep these unchanged 15 | end_of_line = lf 16 | charset = utf-8 17 | trim_trailing_whitespace = true 18 | insert_final_newline = true 19 | 20 | [*.md] 21 | trim_trailing_whitespace = false 22 | 23 | [{package,bower}.json] 24 | indent_style = space 25 | indent_size = 2 -------------------------------------------------------------------------------- /react-provisioning-bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "commonjs", 6 | "jsx": "react", 7 | "declaration": true, 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "skipLibCheck": true, 11 | "typeRoots": [ 12 | "./node_modules/@types", 13 | "./node_modules/@microsoft" 14 | ], 15 | "types": [ 16 | "es6-promise", 17 | "webpack-env" 18 | ], 19 | "lib": [ 20 | "es5", 21 | "dom", 22 | "es2015.collection" 23 | ] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /react-provisioning-bot/README.md: -------------------------------------------------------------------------------- 1 | ## react-provisioning-bot 2 | 3 | This is where you include your WebPart documentation. 4 | 5 | ### Building the code 6 | 7 | ```bash 8 | git clone the repo 9 | npm i 10 | npm i -g gulp 11 | gulp 12 | ``` 13 | 14 | This package produces the following: 15 | 16 | * lib/* - intermediate-stage commonjs build artifacts 17 | * dist/* - the bundled script, along with other resources 18 | * deploy/* - all resources which should be uploaded to a CDN. 19 | 20 | ### Build options 21 | 22 | gulp clean - TODO 23 | gulp test - TODO 24 | gulp serve - TODO 25 | gulp bundle - TODO 26 | gulp package-solution - TODO 27 | -------------------------------------------------------------------------------- /react-provisioning-bot/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/config.2.0.schema.json", 3 | "version": "2.0", 4 | "bundles": { 5 | "veronica-bot-application-customizer": { 6 | "components": [ 7 | { 8 | "entrypoint": "./lib/extensions/veronicaBot/VeronicaBotApplicationCustomizer.js", 9 | "manifest": "./src/extensions/veronicaBot/VeronicaBotApplicationCustomizer.manifest.json" 10 | } 11 | ] 12 | } 13 | }, 14 | "externals": {}, 15 | "localizedResources": { 16 | "VeronicaBotApplicationCustomizerStrings": "lib/extensions/veronicaBot/loc/{locale}.js" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /VeronicaBot/readme.md: -------------------------------------------------------------------------------- 1 | ## Use Azure app service editor 2 | 3 | 1. make code change in the online editor 4 | 5 | Your code changes go live as the code changes are saved. 6 | 7 | ## Use Visual Studio Code 8 | 9 | ### Build and debug 10 | 1. download source code zip and extract source in local folder 11 | 2. open the source folder in Visual Studio Code 12 | 3. make code changes 13 | 4. download and run [botframework-emulator](https://emulator.botframework.com/) 14 | 5. connect the emulator to http://localhost:3987 15 | 16 | ### Publish back 17 | 18 | ``` 19 | npm run azure-publish 20 | ``` 21 | 22 | ## Use continuous integration 23 | 24 | If you have setup continuous integration, then your bot will automatically deployed when new changes are pushed to the source repository. 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/VeronicaBotApplicationCustomizer.manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx/client-side-extension-manifest.schema.json", 3 | 4 | "id": "34fc95d6-9beb-4099-b50b-4b498edd898c", 5 | "alias": "VeronicaBotApplicationCustomizer", 6 | "componentType": "Extension", 7 | "extensionType": "ApplicationCustomizer", 8 | 9 | // The "*" signifies that the version should be taken from the package.json 10 | "version": "*", 11 | "manifestVersion": 2, 12 | 13 | // If true, the component can only be installed on sites where Custom Script is allowed. 14 | // Components that allow authors to embed arbitrary script code should set this to true. 15 | // https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f 16 | "requiresCustomScript": false 17 | } 18 | -------------------------------------------------------------------------------- /react-provisioning-bot/config/package-solution.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/spfx-build/package-solution.schema.json", 3 | "solution": { 4 | "name": "react-provisioning-bot-client-side-solution", 5 | "id": "34eb4b8e-de24-4f07-99b7-b28c2f459b54", 6 | "version": "1.0.0.0", 7 | "includeClientSideAssets": true, 8 | "features": [ 9 | { 10 | "title": "Application Extension - Deployment of custom action.", 11 | "description": "Deploys a custom action with ClientSideComponentId association", 12 | "id": "650dd352-f37a-4ea4-8b6c-cd8ccaba94ce", 13 | "version": "1.0.0.0", 14 | "assets": { 15 | "elementManifests": [ 16 | "elements.xml" 17 | ] 18 | } 19 | } 20 | ] 21 | }, 22 | "paths": { 23 | "zippedPackage": "solution/react-provisioning-bot.sppkg" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /react-provisioning-bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-provisioning-bot", 3 | "version": "0.0.1", 4 | "private": true, 5 | "engines": { 6 | "node": ">=0.10.0" 7 | }, 8 | "scripts": { 9 | "build": "gulp bundle", 10 | "clean": "gulp clean", 11 | "test": "gulp test" 12 | }, 13 | "dependencies": { 14 | "@microsoft/decorators": "~1.4.1", 15 | "@microsoft/sp-application-base": "~1.4.1", 16 | "@microsoft/sp-core-library": "~1.4.1", 17 | "@microsoft/sp-dialog": "~1.4.1", 18 | "@types/webpack-env": ">=1.12.1 <1.14.0", 19 | "botframework-webchat": "^0.11.4", 20 | "sp-pnp-js": "^3.0.5" 21 | }, 22 | "devDependencies": { 23 | "@microsoft/sp-build-web": "~1.4.1", 24 | "@microsoft/sp-module-interfaces": "~1.4.1", 25 | "@microsoft/sp-webpart-workbench": "~1.4.1", 26 | "gulp": "~3.9.1", 27 | "@types/chai": ">=3.4.34 <3.6.0", 28 | "@types/mocha": ">=2.2.33 <2.6.0", 29 | "ajv": "~5.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /react-provisioning-bot/config/serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/core-build/serve.schema.json", 3 | "port": 4321, 4 | "https": true, 5 | "serveConfigurations": { 6 | "default": { 7 | "pageUrl": "https://gdeluca.sharepoint.com/Lists/SitesRequest/AllItems.aspx", 8 | "customActions": { 9 | "34fc95d6-9beb-4099-b50b-4b498edd898c": { 10 | "location": "ClientSideExtension.ApplicationCustomizer", 11 | "properties": { 12 | "testMessage": "Test message" 13 | } 14 | } 15 | } 16 | }, 17 | "veronicaBot": { 18 | "pageUrl": "https://gdeluca.sharepoint.com/Lists/SitesRequest/AllItems.aspx", 19 | "customActions": { 20 | "34fc95d6-9beb-4099-b50b-4b498edd898c": { 21 | "location": "ClientSideExtension.ApplicationCustomizer", 22 | "properties": { 23 | "testMessage": "Test message" 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net461 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | PreserveNewest 17 | 18 | 19 | PreserveNewest 20 | Never 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /ProvisioningArtifacts/create-sharepoint-list.ps1: -------------------------------------------------------------------------------- 1 | # Set your own values here 2 | $SiteCollectionUrl = "" 3 | 4 | Connect-PnPOnline -Url $SiteCollectionUrl -UseWebLogin 5 | 6 | New-PnPList -Title 'SitesRequest' -Template GenericList -Url Lists/SitesRequest 7 | 8 | Add-PnPField -List "SitesRequest" -DisplayName "Status" -InternalName "Status" -Type Choice -Group "spProvisioning" -AddToDefaultView -Choices "Requested","Approved","Ready" -Required 9 | Add-PnPField -List "SitesRequest" -DisplayName "Owner" -InternalName "Owner" -Type Text -Group "spProvisioning" -AddToDefaultView -Required 10 | Add-PnPField -List "SitesRequest" -DisplayName "Description" -InternalName "Description" -Type Text -Group "spProvisioning" -AddToDefaultView -Required 11 | Add-PnPField -List "SitesRequest" -DisplayName "SiteType" -InternalName "SiteType" -Type Choice -Group "spProvisioning" -AddToDefaultView -Choices "TeamSite","CommunicationSite","Teams" -Required 12 | Add-PnPField -List "SitesRequest" -DisplayName "Alias" -InternalName "Alias" -Type Text -Group "spProvisioning" -AddToDefaultView -Required 13 | -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Modern.Provisioning.Async.Function", "Modern.Provisioning.Async.Function\Modern.Provisioning.Async.Function.csproj", "{14781806-680E-49D0-963B-27E99A31BDA7}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {14781806-680E-49D0-963B-27E99A31BDA7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {14781806-680E-49D0-963B-27E99A31BDA7}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {14781806-680E-49D0-963B-27E99A31BDA7}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {14781806-680E-49D0-963B-27E99A31BDA7}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {D00A8161-8101-4B5F-9257-9629D7823BB5} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /react-provisioning-bot/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Install Chrome Debugger Extension for Visual Studio Code to debug your components with the 4 | * Chrome browser: https://aka.ms/spfx-debugger-extensions 5 | */ 6 | "version": "0.2.0", 7 | "configurations": [{ 8 | "name": "Local workbench", 9 | "type": "chrome", 10 | "request": "launch", 11 | "url": "https://localhost:4321/temp/workbench.html", 12 | "webRoot": "${workspaceRoot}", 13 | "sourceMaps": true, 14 | "sourceMapPathOverrides": { 15 | "webpack:///../../../src/*": "${webRoot}/src/*", 16 | "webpack:///../../../../src/*": "${webRoot}/src/*", 17 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 18 | }, 19 | "runtimeArgs": [ 20 | "--remote-debugging-port=9222" 21 | ] 22 | }, 23 | { 24 | "name": "Hosted workbench", 25 | "type": "chrome", 26 | "request": "launch", 27 | "url": "https://enter-your-SharePoint-site/_layouts/workbench.aspx", 28 | "webRoot": "${workspaceRoot}", 29 | "sourceMaps": true, 30 | "sourceMapPathOverrides": { 31 | "webpack:///../../../src/*": "${webRoot}/src/*", 32 | "webpack:///../../../../src/*": "${webRoot}/src/*", 33 | "webpack:///../../../../../src/*": "${webRoot}/src/*" 34 | }, 35 | "runtimeArgs": [ 36 | "--remote-debugging-port=9222", 37 | "-incognito" 38 | ] 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /react-provisioning-bot/config/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://dev.office.com/json-schemas/core-build/tslint.schema.json", 3 | // Display errors as warnings 4 | "displayAsWarning": true, 5 | // The TSLint task may have been configured with several custom lint rules 6 | // before this config file is read (for example lint rules from the tslint-microsoft-contrib 7 | // project). If true, this flag will deactivate any of these rules. 8 | "removeExistingRules": true, 9 | // When true, the TSLint task is configured with some default TSLint "rules.": 10 | "useDefaultConfigAsBase": false, 11 | // Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules 12 | // which are active, other than the list of rules below. 13 | "lintConfig": { 14 | // Opt-in to Lint rules which help to eliminate bugs in JavaScript 15 | "rules": { 16 | "class-name": false, 17 | "export-name": false, 18 | "forin": false, 19 | "label-position": false, 20 | "member-access": true, 21 | "no-arg": false, 22 | "no-console": false, 23 | "no-construct": false, 24 | "no-duplicate-case": true, 25 | "no-duplicate-variable": true, 26 | "no-eval": false, 27 | "no-function-expression": true, 28 | "no-internal-module": true, 29 | "no-shadowed-variable": true, 30 | "no-switch-case-fall-through": true, 31 | "no-unnecessary-semicolons": true, 32 | "no-unused-expression": true, 33 | "no-use-before-declare": true, 34 | "no-with-statement": true, 35 | "semicolon": true, 36 | "trailing-comma": false, 37 | "typedef": false, 38 | "typedef-whitespace": false, 39 | "use-named-parameter": true, 40 | "valid-typeof": true, 41 | "variable-name": false, 42 | "whitespace": false 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /VeronicaBot/web.config: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/VeronicaBotApplicationCustomizer.ts: -------------------------------------------------------------------------------- 1 | import { override } from '@microsoft/decorators'; 2 | import { Log } from '@microsoft/sp-core-library'; 3 | import { 4 | BaseApplicationCustomizer, PlaceholderContent, PlaceholderName 5 | } from '@microsoft/sp-application-base'; 6 | import { Dialog } from '@microsoft/sp-dialog'; 7 | import * as React from 'react'; 8 | import * as ReactDOM from 'react-dom'; 9 | import IGraphBotProps from './components/IGraphBotProps'; 10 | import GraphBot from './components/GraphBot'; 11 | import * as strings from 'VeronicaBotApplicationCustomizerStrings'; 12 | 13 | const LOG_SOURCE: string = 'VeronicaBotApplicationCustomizer'; 14 | 15 | /** 16 | * If your command set uses the ClientSideComponentProperties JSON input, 17 | * it will be deserialized into the BaseExtension.properties object. 18 | * You can define an interface to describe it. 19 | */ 20 | export interface IVeronicaBotApplicationCustomizerProperties { 21 | // This is an example; replace with your own property 22 | testMessage: string; 23 | } 24 | 25 | /** A Custom Action which can be run during execution of a Client Side Application */ 26 | export default class VeronicaBotApplicationCustomizer 27 | extends BaseApplicationCustomizer { 28 | 29 | private _topPlaceHolder: PlaceholderContent; 30 | 31 | @override 32 | public onInit(): Promise { 33 | Log.info(LOG_SOURCE, `Initialized ${strings.Title}`); 34 | 35 | let message: string = this.properties.testMessage; 36 | if (!message) { 37 | message = '(No properties were provided.)'; 38 | } 39 | 40 | //Dialog.alert(`Hello from ${strings.Title}:\n\n${message}`); 41 | this._renderPlaceHolders(); 42 | 43 | return Promise.resolve(); 44 | } 45 | 46 | private _renderPlaceHolders(): void { 47 | 48 | // Check if the header placeholder is already set and if the header placeholder is available 49 | if (!this._topPlaceHolder && this.context.placeholderProvider.placeholderNames.indexOf(PlaceholderName.Top) !== -1) { 50 | this._topPlaceHolder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, { 51 | onDispose: () => {} 52 | }); 53 | 54 | // The extension should not assume that the expected placeholder is available. 55 | if (!this._topPlaceHolder) { 56 | console.error('The expected placeholder was not found.'); 57 | return; 58 | } 59 | 60 | if (this._topPlaceHolder.domElement) { 61 | const element: React.ReactElement = React.createElement( 62 | GraphBot, 63 | { 64 | context: this.context 65 | } as IGraphBotProps 66 | ); 67 | 68 | ReactDOM.render(element, this._topPlaceHolder.domElement); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /react-provisioning-bot/src/extensions/veronicaBot/components/GraphBot.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import IGraphBotProps from "./IGraphBotProps"; 3 | import { ActionButton } from "office-ui-fabric-react/lib/Button"; 4 | import { Panel, PanelType } from "office-ui-fabric-react/lib/Panel"; 5 | import { Spinner, SpinnerSize } from "office-ui-fabric-react/lib/spinner"; 6 | import { Overlay } from "office-ui-fabric-react/lib/overlay"; 7 | import * as ReactDOM from 'react-dom'; 8 | import { Chat, DirectLine, DirectLineOptions, ConnectionStatus } from 'botframework-webchat'; 9 | import IGraphBotState from "./IGraphBotState"; 10 | require("botframework-webchat/botchat.css"); 11 | import pnp, { Logger, LogLevel } from "sp-pnp-js"; 12 | import { Text } from "@microsoft/sp-core-library"; 13 | import styles from "./GraphBot.module.scss"; 14 | import { SPHttpClient } from "@microsoft/sp-http"; 15 | import IGraphBotSettings from "./IGraphBotSettings"; 16 | import * as strings from "VeronicaBotApplicationCustomizerStrings"; 17 | 18 | class GraphBot extends React.Component { 19 | 20 | private _botConnection: DirectLine; 21 | private _botId: string; 22 | private _directLineSecret: string; 23 | 24 | // Local storage keys 25 | private readonly ENTITYKEY_BOTID = "PnPGraphBot_BotId"; 26 | private readonly ENTITYKEY_DIRECTLINESECRET = "PnPGraphBot_BotDirectLineSecret"; 27 | private readonly CONVERSATION_ID_KEY = "PnPGraphBot_ConversationId"; 28 | 29 | constructor(props: IGraphBotProps) { 30 | super(props); 31 | 32 | this._login = this._login.bind(this); 33 | 34 | this.state = { 35 | showPanel: false, 36 | isBotInitializing: false 37 | }; 38 | 39 | // Enable sp-pnp-js session storage wrapper 40 | pnp.storage.local.enabled = true; 41 | } 42 | 43 | public render() { 44 | 45 | // Be careful, the user Id is mandatory to be able to use the bot state service (i.e privateConversationData) 46 | return ( 47 |
48 | 49 | {strings.GraphBotButtonLabel} 50 | 51 | this.setState({ showPanel: false })} 56 | > 57 | {this.state.isBotInitializing ? 58 | 59 | 60 | 61 | : 62 | 83 | } 84 | 85 |
86 | ); 87 | } 88 | 89 | public async componentDidMount() { 90 | 91 | // Delete expired local storage items (conversation id, etc.) 92 | pnp.storage.local.deleteExpired(); 93 | 94 | // Read the bot settings from the tenant property bag or local storage if available 95 | const settings = await this._getGraphBotSettings(this.props); 96 | 97 | // Note: no need to store these informations in state because they are never updated after that 98 | this._botId = settings.BotId; 99 | this._directLineSecret = settings.DirectLineSecret; 100 | } 101 | 102 | /** 103 | * Login the current user 104 | */ 105 | private async _login() { 106 | 107 | this.setState({ 108 | isBotInitializing: true, 109 | showPanel: true, 110 | }); 111 | 112 | // Get the conversation id if there is one. Otherwise, a new one will be created 113 | const conversationId = pnp.storage.local.get(this.CONVERSATION_ID_KEY); 114 | 115 | // Initialize the bot connection direct line 116 | this._botConnection = new DirectLine({ 117 | secret: this._directLineSecret, 118 | webSocket: false, // Needed to be able to retrieve history 119 | conversationId: conversationId ? conversationId : null, 120 | }); 121 | 122 | this._botConnection.connectionStatus$ 123 | .subscribe((connectionStatus) => { 124 | switch (connectionStatus) { 125 | // Successfully connected to the converstaion. 126 | case ConnectionStatus.Online: 127 | if (!conversationId) { 128 | // Store the current conversation id in the browser session storage 129 | // with 15 minutes expiration 130 | pnp.storage.local.put( 131 | this.CONVERSATION_ID_KEY, this._botConnection["conversationId"], 132 | pnp.util.dateAdd(new Date(), "minute", 15) 133 | ); 134 | } 135 | break; 136 | case ConnectionStatus.Uninitialized: 137 | this.setState({ 138 | isBotInitializing: false, 139 | }); 140 | break; 141 | } 142 | }); 143 | 144 | } 145 | 146 | /** 147 | * Read the bot settings in the tenant property bag or local storage 148 | * @param props the component properties 149 | */ 150 | private async _getGraphBotSettings(props: IGraphBotProps): Promise { 151 | 152 | // Read these values from the local storage first 153 | let botId = pnp.storage.local.get(this.ENTITYKEY_BOTID); 154 | let directLineSecret = pnp.storage.local.get(this.ENTITYKEY_DIRECTLINESECRET); 155 | 156 | const expiration = pnp.util.dateAdd(new Date(), "day", 1); 157 | 158 | try { 159 | 160 | if (!botId) { 161 | botId = await this.getTenantPropertyValue(this.ENTITYKEY_BOTID); 162 | pnp.storage.local.put(this.ENTITYKEY_BOTID, botId, expiration); 163 | } 164 | 165 | if (!directLineSecret) { 166 | directLineSecret = await this.getTenantPropertyValue(this.ENTITYKEY_DIRECTLINESECRET); 167 | pnp.storage.local.put(this.ENTITYKEY_DIRECTLINESECRET, directLineSecret, expiration); 168 | } 169 | 170 | return { 171 | BotId: botId, 172 | DirectLineSecret: directLineSecret, 173 | } as IGraphBotSettings; 174 | 175 | } catch (error) { 176 | Logger.write(Text.format("[GraphBot_getGraphBotSettings]: Error: {0}", error)); 177 | } 178 | } 179 | 180 | /** 181 | * Get the value of a tenant property bag property 182 | * @param key the property bag key 183 | */ 184 | public async getTenantPropertyValue(key: string): Promise { 185 | // Get settings from tenant properties 186 | try { 187 | pnp.sp.web.getStorageEntity(key).then(r => { 188 | console.log(r); 189 | return r; 190 | }); 191 | } catch (error) { 192 | Logger.write(Text.format("[getTenantProperty]: Error: {0}", error)); 193 | } 194 | } 195 | } 196 | 197 | export default GraphBot; 198 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Office 365 Modern Provisioning with Bot, Flow, Azure Function # 2 | 3 | ## Summary ## 4 | 5 | This sample demonstrates how to integrate a typical enterprise scenario where the user can submit a creation's request 6 | for a new SharePoint team site, communication site or a Microsoft team through a node.js Bot (App Only) which is available on Teams, Skype, Direct line and so on. 7 | The request is stored in a SharePoint list accessible only by an admin which can approve it, triggering a Microsoft Flow 8 | that contains the logic necessary to send an email to the end user and the admin in order to notify that the process is started. 9 | After that, if a request has the status equal to "Requested" the latter is processed by calling an Azure c# function that 10 | creates a SharePoint team site, communication site or a Microsoft Team. 11 | Microsoft Flow receives a response from the Azure function with HTTP status 200, at the end the user receives an email that notifies the end of the process. 12 | Furthermore, there is also a SharePoint Framework Application Customizer which allows the user to interact with the Bot by leveraging the capabilities of the direct line from a SharePoint site. 13 | 14 | [Blog post here http://www.delucagiuliano.com/office-365-modern-provisioning-with-bot-flow-azure-function-and-sharepoint-framework](http://www.delucagiuliano.com/office-365-modern-provisioning-with-bot-flow-azure-function-and-sharepoint-framework) 15 | 16 | ### When to use this pattern? ### 17 | This sample is suitable when you want to implement a typical enterprise scenario in order to request and approving the creation of a new SharePoint site or Microsoft team. 18 | 19 |

20 | 21 | 22 |

23 | 24 | ### Solution Architecture ### 25 |

26 | 27 |

28 | 29 | ## Used SharePoint Framework Version 30 | ![drop](https://img.shields.io/badge/drop-1.4.1-green.svg) 31 | 32 | ## Applies to 33 | 34 | * [SharePoint Framework](https:/dev.office.com/sharepoint) 35 | * [Office 365 tenant](https://dev.office.com/sharepoint/docs/spfx/set-up-your-development-environment) 36 | 37 | ## Solution 38 | 39 | Solution|Author(s) 40 | --------|--------- 41 | O365-Modern-Provisioning | Giuliano De Luca (MVP Office Development) - Twitter @giuleon 42 | 43 | ## Version history 44 | 45 | Version|Date|Comments 46 | -------|----|-------- 47 | 1.0 | February 19, 2018 | Initial release 48 | 49 | ## Disclaimer 50 | **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** 51 | 52 | --- 53 | 54 | ## Minimal Path to Awesome 55 | 56 | - Clone this repository and follow the instructions below 57 | 58 | ## Prerequisites ## 59 | 60 | ### 1- Setup the Azure AD application ### 61 | 62 | The Bot makes use of Microsoft Graph API (App Only), you need to register a new app in the Azure Active Directory behind your Office 365 tenant using the Azure portal: 63 |

64 | 65 |

66 | 67 | - Go to https://portal.azure.com. Log in and register a new application assigning a key secret: 68 |

69 | 70 |

71 | 72 | - Add the **Application Permission** for Microsoft Graph **Read and Write All Groups** and **Read and write items in all site collections**. 73 |

74 | 75 | 76 |

77 | 78 | - Keep in mind that if you have to work with the user's context you will need to change the permission in **Delegated Permission** and of course you will need to change the Bot in order to handle the sign-in and redirect with the token. 79 | 80 | ### 2- Create the Node.js Bot in Azure ### 81 | 82 | The prerequisite is an Azure subscription in order to go forward, therefore create the Azure Node.js Bot: 83 |

84 | 85 |

86 | 87 | - Click on build in your Azure Bot page and after "Open online code editor" 88 |

89 | 90 |

91 | 92 | - Click on build in your Azure Bot page and after "Open online code editor" 93 |

94 | 95 |

96 | 97 | - Replace the content of the files **app.js** and **package.json** with the sample contained in **VeronicaBot** folder (app.js, package.json) 98 | 99 | - The last step regards the configuration, remember to set up properly the variables in the Application Settings: 100 |

101 | 102 | 103 |

104 | 105 | ### 3- Create the SharePoint list, tenant properties and the SPFx Application Customizer ### 106 | 107 | The Bot will cover multiple scenarios Teams, Direct Line, Skype, Cortana, Email, Slack.... 108 | However, if you plan to make use of Direct Line you can install the SPFx application customizer **react-provisioning-bot** as scope your tenant or specific site collection. 109 | 110 |

111 | 112 |

113 | 114 | The SPFx reads the following tenant properties bag: 115 | 116 | ```typescript 117 | private readonly ENTITYKEY_BOTID = "PnPGraphBot_BotId"; 118 | private readonly ENTITYKEY_DIRECTLINESECRET = "PnPGraphBot_BotDirectLineSecret"; 119 | private readonly CONVERSATION_ID_KEY = "PnPGraphBot_ConversationId"; 120 | ``` 121 | 122 | Therefore, you have to run the script **set-tenant-properties.ps1** in the folder **ProvisioningArtifacts** to save these properties. 123 | 124 | There is a SharePoint list which is required in order to store the users's requests, therefore run the Powershell script **create-sharepoint-list.ps1**, if you have not installed on your machine the PnP cmdlets please [install it](https://github.com/SharePoint/PnP-PowerShell). 125 | I suggest you install the list in the root site collection of the tenant, conceptually it make sense dedicates this site to the admins, but of course you are free to install it where you prefer. 126 | 127 | ### 4- Azure Function ### 128 | 129 | The engine of this solution is a c# Azure Function **Modern.Provisioning.Async.Function** which makes use of PnP to create a new SharePoint site (Team or Communication) or a new Microsoft Teams according to the user's request. 130 | Just to clarify, the Azure Function uses the admin credentials, the password is encrypted into a [Azure Key Vault](https://azure.microsoft.com/en-us/services/key-vault/). 131 | In order to consume properly the Azure Function please don't forget to configure the application settings: 132 | 133 | Key | Description 134 | ------------ | ------------- 135 | spAdminUser | tenant admin email 136 | KeyVaultSecret | The key secret value after having created the key vault 137 | TokenEndpoint | The token endpoint that you can retrieve from your Office 365 tenant Azure portal 138 | listName | for this sample is **SitesRequest** 139 | ClientId | Client Id App Only registered in order to consume Microsoft Graph and already used by the Bot 140 | ClientSecret | Client Secret App Only registered in order to consume Microsoft Graph and already used by the Bot 141 | 142 | - The SharePoint sites are created across PnP 143 | - The Microsoft Teams are created with Graph 144 | 145 | ### 5- Microsoft Flow ### 146 | Last but not least, there is a Microsoft Flow to implement, which basically performs the following steps: 147 | 148 | - send an email to the admin when a user's request has been saved in the list **SitesRequest** 149 | - When the admin approves the request the Azure Function is called to start the provisioning 150 | - When the process is concluded an email notify the user that the request has been solved 151 | 152 |

153 | 154 |

155 | -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/ModernProvisioning.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Microsoft.Azure.WebJobs; 8 | using Microsoft.Azure.WebJobs.Extensions.Http; 9 | using Microsoft.Azure.WebJobs.Host; 10 | using Microsoft.SharePoint.Client; 11 | using OfficeDevPnP.Core; 12 | using OfficeDevPnP.Core.Sites; 13 | using Microsoft.Azure.Services.AppAuthentication; 14 | using Microsoft.Azure.KeyVault; 15 | 16 | namespace Modern.Provisioning.Async.Function 17 | { 18 | public static class ModernProvisioning 19 | { 20 | private static string adminPassword; 21 | 22 | [FunctionName("ModernProvisioning")] 23 | public static async Task Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log) 24 | { 25 | log.Info("C# HTTP trigger function processed a request."); 26 | if (adminPassword == null) 27 | { 28 | // This is the part where I grab the secret. 29 | var azureServiceTokenProvider = new AzureServiceTokenProvider(); 30 | log.Info("Getting the secret."); 31 | var kvClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)); 32 | log.Info("KeyVaultSecret: " + Environment.GetEnvironmentVariable("KeyVaultSecret")); 33 | adminPassword = (await kvClient.GetSecretAsync(Environment.GetEnvironmentVariable("KeyVaultSecret"))).Value; 34 | } 35 | 36 | // parse query parameter 37 | string name = req.GetQueryNameValuePairs() 38 | .FirstOrDefault(q => string.Compare(q.Key, "name", true) == 0) 39 | .Value; 40 | 41 | // Get request body 42 | dynamic data = await req.Content.ReadAsAsync(); 43 | 44 | // Set name to query string or body data 45 | name = name ?? data?.name; 46 | string title = data?.title; 47 | 48 | string siteUrl = string.Empty; 49 | string adminUser = Environment.GetEnvironmentVariable("spAdminUser"); 50 | log.Info("adminUser: " + adminUser); 51 | string spSite = Environment.GetEnvironmentVariable("spSite"); 52 | log.Info("spSite: " + adminUser); 53 | System.Security.SecureString secureString = new System.Security.SecureString(); 54 | foreach (char ch in adminPassword) 55 | { 56 | secureString.AppendChar(ch); 57 | } 58 | string sitesRequest = Environment.GetEnvironmentVariable("listName"); 59 | log.Info("listName: " + sitesRequest); 60 | Dictionary siteInfo = new Dictionary(); 61 | OfficeDevPnP.Core.AuthenticationManager authManager = new OfficeDevPnP.Core.AuthenticationManager(); 62 | string camlQuery = 63 | "" + 64 | "" + 65 | "" + 66 | "" + 67 | "" + 68 | "Approved" + 69 | "" + 70 | "" + 71 | "" + 72 | "1" + 73 | ""; 74 | CamlQuery cq = new CamlQuery(); 75 | cq.ViewXml = camlQuery; 76 | using (var context = authManager.GetSharePointOnlineAuthenticatedContextTenant(spSite, adminUser, secureString)) 77 | { 78 | List list = context.Web.Lists.GetByTitle(sitesRequest); 79 | ListItemCollection lic = list.GetItems(cq); 80 | context.Load(lic); 81 | context.ExecuteQuery(); 82 | foreach (ListItem item in lic) 83 | { 84 | siteInfo.Add("Id", item["ID"].ToString()); 85 | siteInfo.Add("title", item["Title"].ToString()); 86 | siteInfo.Add("owner", item["Owner"].ToString()); 87 | siteInfo.Add("description", item["Description"] == null ? "" : item["Description"].ToString()); 88 | siteInfo.Add("type", item["SiteType"].ToString()); 89 | siteInfo.Add("alias", item["Alias"].ToString()); 90 | log.Info("Processing: " + item["Title"].ToString()); 91 | var siteType = siteInfo["type"]; 92 | switch (siteType.ToLower()) 93 | { 94 | case "communicationsite": 95 | var ctx = context.CreateSiteAsync(new CommunicationSiteCollectionCreationInformation 96 | { 97 | Title = siteInfo["title"].ToString(), 98 | Owner = siteInfo["owner"].ToString(), 99 | Lcid = 1033, 100 | Description = siteInfo["description"].ToString(), 101 | Url = spSite + "/sites/" + siteInfo["alias"].ToString(), 102 | }).GetAwaiter().GetResult(); 103 | // Add OWner 104 | User user = ctx.Web.EnsureUser(siteInfo["owner"].ToString()); 105 | ctx.Web.Context.Load(user); 106 | ctx.Web.Context.ExecuteQueryRetry(); 107 | ctx.Web.AssociatedOwnerGroup.Users.AddUser(user); 108 | ctx.Web.AssociatedOwnerGroup.Update(); 109 | ctx.Web.Context.ExecuteQueryRetry(); 110 | break; 111 | case "teamsite": 112 | var ctxTeamsite = context.CreateSiteAsync(new TeamSiteCollectionCreationInformation 113 | { 114 | DisplayName = siteInfo["title"].ToString(), 115 | Description = siteInfo["description"].ToString(), 116 | Alias = siteInfo["alias"].ToString(), 117 | IsPublic = false, 118 | }).GetAwaiter().GetResult(); 119 | siteUrl = ctxTeamsite.Url; 120 | // Add OWner 121 | User userTeamSite = ctxTeamsite.Web.EnsureUser(siteInfo["owner"].ToString()); 122 | ctxTeamsite.Web.Context.Load(userTeamSite); 123 | ctxTeamsite.Web.Context.ExecuteQueryRetry(); 124 | ctxTeamsite.Web.AssociatedOwnerGroup.Users.AddUser(userTeamSite); 125 | ctxTeamsite.Web.AssociatedOwnerGroup.Update(); 126 | ctxTeamsite.Web.Context.ExecuteQueryRetry(); 127 | break; 128 | case "teams": 129 | string token = Graph.getToken(); 130 | log.Info("Access Token: " + token); 131 | string userId = string.Empty; 132 | string groupId = string.Empty; 133 | if (string.IsNullOrEmpty(token) == false) 134 | { 135 | userId = Graph.getUser(token, siteInfo["owner"].ToString()); 136 | log.Info("userId: " + userId); 137 | } 138 | if (string.IsNullOrEmpty(userId) == false) 139 | { 140 | string dataPost = 141 | "{ 'displayName': '" + siteInfo["title"].ToString() + "', 'groupTypes': ['Unified'], 'mailEnabled': true, 'mailNickname': '" + siteInfo["alias"].ToString().Replace("\r\n", "").Replace(" ","") + "', 'securityEnabled': false, 'owners@odata.bind': ['https://graph.microsoft.com/v1.0/users/" + userId + "'], 'visibility': 'Private' }"; 142 | groupId = Graph.createUnifiedGroup(token, dataPost); 143 | log.Info("groupId: " + groupId); 144 | log.Info("Creating team......"); 145 | string teamData = "{ \"memberSettings\": { \"allowCreateUpdateChannels\": true }, \"messagingSettings\": { \"allowUserEditMessages\": true, \"allowUserDeleteMessages\": true }, \"funSettings\": { \"allowGiphy\": true, \"giphyContentRating\": \"strict\" } }"; 146 | string team = Graph.createTeamFromUnifiedGroup(token, teamData, groupId); 147 | log.Info("team: " + team); 148 | //Graph.addOwnerToUnifiedGroup(token, groupId, userId); 149 | //removeOwnerToUnifiedGroup(token, groupId, userId); 150 | } 151 | siteUrl = siteInfo["title"].ToString(); 152 | log.Info("Teams ready: " + siteUrl); 153 | break; 154 | } 155 | // When the site or Teams has been created the status of the list item will change in ready 156 | if (siteUrl != string.Empty) 157 | { 158 | item["Status"] = "Ready"; 159 | item.Update(); 160 | 161 | context.ExecuteQuery(); 162 | } 163 | } 164 | } 165 | 166 | return siteUrl == null 167 | ? req.CreateResponse(HttpStatusCode.InternalServerError, "Something went wrong!") 168 | : req.CreateResponse(HttpStatusCode.OK, siteUrl); 169 | 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Modern.Provisioning.Async.Function/Modern.Provisioning.Async.Function/Graph.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | 10 | namespace Modern.Provisioning.Async.Function 11 | { 12 | public class Graph 13 | { 14 | public static string getToken() 15 | { 16 | string endPoint = Environment.GetEnvironmentVariable("TokenEndpoint"); 17 | string data = "grant_type=client_credentials&client_id=" + Environment.GetEnvironmentVariable("ClientId") + 18 | "&client_secret=" + Environment.GetEnvironmentVariable("ClientSecret") + "&resource=https://graph.microsoft.com"; 19 | string contenType = "application/x-www-form-urlencoded"; 20 | string responseFromServer = requestGetToken(endPoint, data, contenType); 21 | AccessToken accessToken = JsonConvert.DeserializeObject(responseFromServer); 22 | 23 | return accessToken.access_token; 24 | } 25 | 26 | public static string getUser(string token, string userPrincipalName) 27 | { 28 | string endPoint = "https://graph.microsoft.com/v1.0/users/" + userPrincipalName + "?$select=id"; 29 | string responseFromServer = requestGet(endPoint, token); 30 | GraphUser graphUser = JsonConvert.DeserializeObject(responseFromServer); 31 | 32 | return graphUser.id; 33 | } 34 | 35 | public static string createUnifiedGroup(string token, string data) 36 | { 37 | string endPoint = "https://graph.microsoft.com/v1.0/groups/"; 38 | string contenType = "application/json"; 39 | string responseFromServer = requestPost(token, endPoint, data, contenType); 40 | GraphGroup group = JsonConvert.DeserializeObject(responseFromServer); 41 | 42 | return group.id; 43 | } 44 | 45 | public static string createTeamFromUnifiedGroup(string token, string data, string groupId) 46 | { 47 | string endPoint = "https://graph.microsoft.com/v1.0/groups/" + groupId + "/team"; 48 | string contenType = "application/json"; 49 | string responseFromServer = requestPut(token, endPoint, data, contenType); 50 | GraphGroup group = JsonConvert.DeserializeObject(responseFromServer); 51 | 52 | return group.id; 53 | } 54 | 55 | public static bool addOwnerToUnifiedGroup(string token, string groupId, string userId) 56 | { 57 | bool ownerAdded = false; 58 | 59 | string endPoint = "https://graph.microsoft.com/v1.0/groups/" + groupId + "/owners/$ref"; 60 | 61 | string data = "{ '@odata.id': 'https://graph.microsoft.com/v1.0/users/" + userId + "' }"; 62 | string contenType = "application/json"; 63 | string responseFromServer = requestPost(token, endPoint, data, contenType); 64 | ownerAdded = true; 65 | 66 | return ownerAdded; 67 | } 68 | 69 | public static bool removeOwnerToUnifiedGroup(string token, string groupId, string userId) 70 | { 71 | bool ownerRemoved = false; 72 | string endPoint = "https://graph.microsoft.com/v1.0/groups/" + groupId + "/owners/" + userId + "/$ref"; 73 | string data = ""; 74 | string contenType = "application/json"; 75 | string responseFromServer = requestDelete(token, endPoint, data, contenType); 76 | ownerRemoved = true; 77 | 78 | return ownerRemoved; 79 | } 80 | 81 | private static string requestGetToken(string endPoint, string postData, string contentType = null) 82 | { 83 | // Create a request using a URL that can receive a post. 84 | WebRequest request = WebRequest.Create(endPoint); 85 | // Set the Method property of the request to POST. 86 | request.Method = "POST"; 87 | // Create POST data and convert it to a byte array. 88 | byte[] byteArray = Encoding.UTF8.GetBytes(postData); 89 | if (string.IsNullOrEmpty(contentType) == false) 90 | { 91 | // Set the ContentType property of the WebRequest. 92 | request.ContentType = contentType; 93 | } 94 | // Set the ContentLength property of the WebRequest. 95 | request.ContentLength = byteArray.Length; 96 | // Get the request stream. 97 | Stream dataStream = request.GetRequestStream(); 98 | // Write the data to the request stream. 99 | dataStream.Write(byteArray, 0, byteArray.Length); 100 | // Close the Stream object. 101 | dataStream.Close(); 102 | // Get the response. 103 | WebResponse response = request.GetResponse(); 104 | // Display the status. 105 | Console.WriteLine(((HttpWebResponse)response).StatusDescription); 106 | // Get the stream containing content returned by the server. 107 | dataStream = response.GetResponseStream(); 108 | // Open the stream using a StreamReader for easy access. 109 | StreamReader reader = new StreamReader(dataStream); 110 | // Read the content. 111 | string responseFromServer = reader.ReadToEnd(); 112 | 113 | // Clean up the streams. 114 | reader.Close(); 115 | dataStream.Close(); 116 | response.Close(); 117 | return responseFromServer; 118 | } 119 | 120 | private static string requestGet(string endPoint, string token, string postData = null, string contentType = null) 121 | { 122 | // Create a request for the URL. 123 | WebRequest request = WebRequest.Create(endPoint); 124 | // If required by the server, set the credentials. 125 | //request.Credentials = CredentialCache.DefaultCredentials; 126 | request.Headers.Add("Authorization", "Bearer " + token); 127 | // Get the response. 128 | WebResponse response = request.GetResponse(); 129 | // Display the status. 130 | Console.WriteLine(((HttpWebResponse)response).StatusDescription); 131 | // Get the stream containing content returned by the server. 132 | Stream dataStream = response.GetResponseStream(); 133 | // Open the stream using a StreamReader for easy access. 134 | StreamReader reader = new StreamReader(dataStream); 135 | // Read the content. 136 | string responseFromServer = reader.ReadToEnd(); 137 | // Display the content. 138 | Console.WriteLine(responseFromServer); 139 | // Clean up the streams and the response. 140 | reader.Close(); 141 | response.Close(); 142 | 143 | return responseFromServer; 144 | } 145 | 146 | private static string requestPost(string token, string endPoint, string postData, string contentType = null) 147 | { 148 | // Create a request using a URL that can receive a post. 149 | WebRequest request = WebRequest.Create(endPoint); 150 | // Set the Method property of the request to POST. 151 | request.Method = "POST"; 152 | // Create POST data and convert it to a byte array. 153 | byte[] byteArray = Encoding.UTF8.GetBytes(postData); 154 | request.Headers.Add("Authorization", "Bearer " + token); 155 | if (string.IsNullOrEmpty(contentType) == false) 156 | { 157 | // Set the ContentType property of the WebRequest. 158 | request.ContentType = contentType; 159 | } 160 | // Set the ContentLength property of the WebRequest. 161 | request.ContentLength = byteArray.Length; 162 | // Get the request stream. 163 | Stream dataStream = request.GetRequestStream(); 164 | // Write the data to the request stream. 165 | dataStream.Write(byteArray, 0, byteArray.Length); 166 | // Close the Stream object. 167 | dataStream.Close(); 168 | // Get the response. 169 | WebResponse response = request.GetResponse(); 170 | // Display the status. 171 | Console.WriteLine(((HttpWebResponse)response).StatusDescription); 172 | // Get the stream containing content returned by the server. 173 | dataStream = response.GetResponseStream(); 174 | // Open the stream using a StreamReader for easy access. 175 | StreamReader reader = new StreamReader(dataStream); 176 | // Read the content. 177 | string responseFromServer = reader.ReadToEnd(); 178 | 179 | // Clean up the streams. 180 | reader.Close(); 181 | dataStream.Close(); 182 | response.Close(); 183 | return responseFromServer; 184 | } 185 | 186 | private static string requestPut(string token, string endPoint, string postData, string contentType = null) 187 | { 188 | // Create a request using a URL that can receive a post. 189 | WebRequest request = WebRequest.Create(endPoint); 190 | // Set the Method property of the request to POST. 191 | request.Method = "PUT"; 192 | // Create POST data and convert it to a byte array. 193 | byte[] byteArray = Encoding.UTF8.GetBytes(postData); 194 | request.Headers.Add("Authorization", "Bearer " + token); 195 | if (string.IsNullOrEmpty(contentType) == false) 196 | { 197 | // Set the ContentType property of the WebRequest. 198 | request.ContentType = contentType; 199 | } 200 | // Set the ContentLength property of the WebRequest. 201 | request.ContentLength = byteArray.Length; 202 | // Get the request stream. 203 | Stream dataStream = request.GetRequestStream(); 204 | // Write the data to the request stream. 205 | dataStream.Write(byteArray, 0, byteArray.Length); 206 | // Close the Stream object. 207 | dataStream.Close(); 208 | // Get the response. 209 | WebResponse response = request.GetResponse(); 210 | // Display the status. 211 | Console.WriteLine(((HttpWebResponse)response).StatusDescription); 212 | // Get the stream containing content returned by the server. 213 | dataStream = response.GetResponseStream(); 214 | // Open the stream using a StreamReader for easy access. 215 | StreamReader reader = new StreamReader(dataStream); 216 | // Read the content. 217 | string responseFromServer = reader.ReadToEnd(); 218 | 219 | // Clean up the streams. 220 | reader.Close(); 221 | dataStream.Close(); 222 | response.Close(); 223 | return responseFromServer; 224 | } 225 | 226 | private static string requestDelete(string token, string endPoint, string postData, string contentType = null) 227 | { 228 | // Create a request using a URL that can receive a post. 229 | WebRequest request = WebRequest.Create(endPoint); 230 | // Set the Method property of the request to POST. 231 | request.Method = "DELETE"; 232 | // Create POST data and convert it to a byte array. 233 | byte[] byteArray = Encoding.UTF8.GetBytes(postData); 234 | request.Headers.Add("Authorization", "Bearer " + token); 235 | if (string.IsNullOrEmpty(contentType) == false) 236 | { 237 | // Set the ContentType property of the WebRequest. 238 | request.ContentType = contentType; 239 | } 240 | // Set the ContentLength property of the WebRequest. 241 | request.ContentLength = byteArray.Length; 242 | // Get the request stream. 243 | Stream dataStream = request.GetRequestStream(); 244 | // Write the data to the request stream. 245 | dataStream.Write(byteArray, 0, byteArray.Length); 246 | // Close the Stream object. 247 | dataStream.Close(); 248 | // Get the response. 249 | WebResponse response = request.GetResponse(); 250 | // Display the status. 251 | Console.WriteLine(((HttpWebResponse)response).StatusDescription); 252 | // Get the stream containing content returned by the server. 253 | dataStream = response.GetResponseStream(); 254 | // Open the stream using a StreamReader for easy access. 255 | StreamReader reader = new StreamReader(dataStream); 256 | // Read the content. 257 | string responseFromServer = reader.ReadToEnd(); 258 | 259 | // Clean up the streams. 260 | reader.Close(); 261 | dataStream.Close(); 262 | response.Close(); 263 | return responseFromServer; 264 | } 265 | 266 | public class AccessToken 267 | { 268 | public String token_type { get; set; } 269 | public String resource { get; set; } 270 | public String access_token { get; set; } 271 | public String expires_in { get; set; } 272 | public String ext_expires_in { get; set; } 273 | public String expires_on { get; set; } 274 | public String not_before { get; set; } 275 | } 276 | 277 | public class GraphGroup 278 | { 279 | public String id { get; set; } 280 | } 281 | 282 | public class GraphUser 283 | { 284 | public String id { get; set; } 285 | } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /VeronicaBot/app.js: -------------------------------------------------------------------------------- 1 | /*----------------------------------------------------------------------------------------- 2 | Veronica Bot - makes use of Microsoft Graph in order to store the users's request in a list 3 | Author: Giuliano De Luca (MVP Office Development) - Twitter @giuleon 4 | Date: February 20, 2018 5 | -----------------------------------------------------------------------------------------*/ 6 | 7 | var restify = require('restify'); 8 | var builder = require('botbuilder'); 9 | var botbuilder_azure = require("botbuilder-azure"); 10 | var request = require("request"); 11 | var Q = require('q'); 12 | 13 | // Setup Restify Server 14 | var server = restify.createServer(); 15 | 16 | server.use(restify.plugins.bodyParser({ 17 | mapParams: true 18 | })); // To be able to get the authorization code (req.params.code) 19 | 20 | server.listen(process.env.port || process.env.PORT || 3978, function () { 21 | console.log('%s listening to %s', server.name, server.url); 22 | }); 23 | 24 | // Create chat connector for communicating with the Bot Framework Service 25 | var connector = new builder.ChatConnector({ 26 | appId: process.env.MicrosoftAppId, 27 | appPassword: process.env.MicrosoftAppPassword, 28 | }); 29 | 30 | // Config 31 | var config = { 32 | 'clientId': process.env.AAD_CLIENT_ID, // The client Id retrieved from the Azure AD App 33 | 'clientSecret': process.env.AAD_CLIENT_SECRET, // The client secret retrieved from the Azure AD App 34 | 'tenant': process.env.TENANT, // The tenant Id or domain name (e.g mydomain.onmicrosoft.com) 35 | 'tokenEndpoint': process.env.tokenEndpoint, // This URL will be used for the Azure AD Application to send the authorization code. 36 | 'resource': process.env.RESOURCE, // The resource endpoint we want to give access to (in this case, SharePoint Online) 37 | 'listId': process.env.List_Id, // The list Id where the Bot will save the user's submission 38 | } 39 | 40 | // Graph 41 | var graph = {}; 42 | 43 | // Listen for messages from users 44 | server.post('/api/messages', connector.listen()); 45 | 46 | /*---------------------------------------------------------------------------------------- 47 | * Bot Storage: This is a great spot to register the private state storage for your bot. 48 | * We provide adapters for Azure Table, CosmosDb, SQL Azure, or you can implement your own! 49 | * For samples and documentation, see: https://github.com/Microsoft/BotBuilder-Azure 50 | * ---------------------------------------------------------------------------------------- */ 51 | 52 | var tableName = 'botdata'; 53 | var azureTableClient = new botbuilder_azure.AzureTableClient(tableName, process.env['AzureWebJobsStorage']); 54 | var tableStorage = new botbuilder_azure.AzureBotStorage({ gzipData: false }, azureTableClient); 55 | 56 | // Create your bot with a function to receive messages from the user 57 | var bot = new builder.UniversalBot(connector); 58 | bot.set('storage', tableStorage); 59 | 60 | bot.dialog('/', [ 61 | function (session) { 62 | if (session.privateConversationData["welcome"]) { 63 | session.send("Hi I'm your SharePoint Bot to assist you to request a new SharePoint site or Teams, what do you want to request?"); 64 | } 65 | session.privateConversationData["welcome"] = 'true'; 66 | session.beginDialog('makeYourChoice'); 67 | }, 68 | function (session, results) { 69 | session.privateConversationData["SiteType"] = results.response; 70 | session.beginDialog('askForTitle'); 71 | }, 72 | function (session, results) { 73 | session.privateConversationData["Title"] = results.response; 74 | session.beginDialog('askForReason'); 75 | }, 76 | function (session, results) { 77 | session.privateConversationData["Description"] = results.response; 78 | session.beginDialog('askForOwner'); 79 | }, 80 | function (session, results) { 81 | session.privateConversationData["Owner"] = results.response; 82 | session.beginDialog('askForAlias'); 83 | }, 84 | function (session, results) { 85 | session.privateConversationData["Alias"] = results.response; 86 | session.beginDialog('askForConfirmation'); 87 | }, 88 | function (session, results) { 89 | if (results.response === "confirmed") { 90 | // Get an access token for the app. 91 | auth.getAccessToken().then(function (token) { 92 | // create a new list item 93 | var params = { 94 | "fields": { 95 | "Title": session.privateConversationData['Title'], 96 | "Status": "Requested", 97 | "Owner": session.privateConversationData['Owner'], 98 | "Description": session.privateConversationData['Description'], 99 | "SiteType": session.privateConversationData['SiteType'], 100 | "Alias": session.privateConversationData['Alias'] 101 | } 102 | }; 103 | 104 | graph.postListItem(token, params) 105 | .then(function (result) { 106 | console.log(result); 107 | session.send("Request submitted successfully"); 108 | session.beginDialog('askForAnotherRequest'); 109 | }, function (error) { 110 | console.error('>>> Error creating a list item: ' + error); 111 | session.beginDialog('askForAnotherRequest'); 112 | }); 113 | }, function (error) { 114 | console.error('>>> Error getting access token: ' + error); 115 | session.beginDialog('askForAnotherRequest'); 116 | }); 117 | } else { 118 | session.beginDialog('askForAnotherRequest'); 119 | } 120 | }, 121 | function (session, results) { 122 | if (results.response === 'yes') { 123 | session.beginDialog('/'); 124 | } else { 125 | session.endDialog(); 126 | } 127 | }, 128 | ]).triggerAction({ matches: /^(show|list|restart)/i }); 129 | 130 | // Add dialog to return list of choices available 131 | bot.dialog('makeYourChoice', [ 132 | function (session) { 133 | var msg = new builder.Message(session) 134 | .speak('what do you want to request?') 135 | .text('what do you want to request?'); 136 | msg.attachmentLayout(builder.AttachmentLayout.carousel) 137 | msg.attachments([ 138 | new builder.ThumbnailCard(session) 139 | .title("SharePoint Site") 140 | .subtitle("A SharePoint team site connects you and your team to the content, information, and apps you rely on every day.") 141 | .text("Create a SharePoint Onlineteam site to provide a location where you and your team can work on projects and share information.") 142 | .images([builder.CardImage.create(session, 'https://pbs.twimg.com/profile_images/1097195102303207424/pDetc4fK_200x200.png')]) 143 | .buttons([ 144 | builder.CardAction.imBack(session, "TeamSite", "Confirm") 145 | ]), 146 | new builder.ThumbnailCard(session) 147 | .title("SharePoint Site") 148 | .subtitle("SharePoint communication sites are a great way to share information with others in a visually compelling format.") 149 | .text("With a communication site, typically only a small set of members contribute content that is consumed by a much larger audience.") 150 | .images([builder.CardImage.create(session, 'https://pbs.twimg.com/profile_images/1097195102303207424/pDetc4fK_200x200.png')]) 151 | .buttons([ 152 | builder.CardAction.imBack(session, "CommunicationSite", "Confirm") 153 | ]), 154 | new builder.ThumbnailCard(session) 155 | .title("Microsoft Teams") 156 | .subtitle("Microsoft Teams is the hub for teamwork in Office 365 that integrates all the people, content, and tools your team needs to be more engaged and effective.") 157 | .text("Work closer with your team by using chat, meeting, conference call, documents.") 158 | .images([builder.CardImage.create(session, 'https://pbs.twimg.com/profile_images/1128330992328921090/nVNqd5QP_200x200.png')]) 159 | .buttons([ 160 | builder.CardAction.imBack(session, "Teams", "Confirm") 161 | ]) 162 | ]); 163 | builder.Prompts.text(session, msg); 164 | }, 165 | function name(session, results) { 166 | session.endDialogWithResult({ response: results.response }); 167 | } 168 | ]); 169 | 170 | // This dialog prompts the user for a title. 171 | bot.dialog('askForTitle', [ 172 | function (session, args) { 173 | var question = 'What is the title of your ' + session.privateConversationData["SiteType"] + '?'; 174 | var msg = new builder.Message(session) 175 | .speak(question) 176 | .text(question); 177 | builder.Prompts.text(session, msg); 178 | }, 179 | function (session, results) { 180 | session.endDialogWithResult({ response: results.response }); 181 | } 182 | ]); 183 | 184 | // This dialog prompts the reason why 185 | bot.dialog('askForReason', [ 186 | function (session, args) { 187 | var question = 'Describe the reason of your request:'; 188 | var msg = new builder.Message(session) 189 | .speak(question) 190 | .text(question); 191 | builder.Prompts.text(session, msg); 192 | }, 193 | function (session, results) { 194 | session.endDialogWithResult({ response: results.response }); 195 | } 196 | ]); 197 | 198 | // This dialog prompts the Owner 199 | bot.dialog('askForOwner', [ 200 | function (session, args) { 201 | var question = ''; 202 | var msg = ''; 203 | if (args && args.reprompt) { 204 | question = 'The user doesn\'t exists, please insert a valid email'; 205 | msg = new builder.Message(session) 206 | .speak(question) 207 | .text(question); 208 | builder.Prompts.text(session, msg); 209 | } else { 210 | question = 'Please insert the email of the owner:'; 211 | msg = new builder.Message(session) 212 | .speak(question) 213 | .text(question); 214 | builder.Prompts.text(session, msg); 215 | } 216 | }, 217 | function (session, results) { 218 | var userEmail = results.response; 219 | // Get an access token for the app. 220 | auth.getAccessToken().then(function (token) { 221 | graph.getUserByEmail(token, userEmail) 222 | .then(function (result) { 223 | console.log(result); 224 | session.endDialogWithResult({ response: results.response }); 225 | }, function (error) { 226 | console.error('>>> Error getting the user: ' + error); 227 | // Repeat the dialog 228 | session.replaceDialog('askForOwner', { reprompt: true }); 229 | }); 230 | }); 231 | } 232 | ]); 233 | 234 | // This dialog prompts the user for a phone number. 235 | // It will re-prompt the user if the input does not match a pattern for phone number. 236 | bot.dialog('askForAlias', [ 237 | function (session, args) { 238 | var question = ''; 239 | var msg = ''; 240 | if (args && args.reprompt) { 241 | question = 'The alias already exists please choose another one.'; 242 | msg = new builder.Message(session) 243 | .speak(question) 244 | .text(question); 245 | builder.Prompts.text(session, msg); 246 | } else { 247 | question = 'Please insert an alias for your ' + session.privateConversationData["SiteType"] + ' without blank spaces or special characters.'; 248 | msg = new builder.Message(session) 249 | .speak(question) 250 | .text(question); 251 | builder.Prompts.text(session, msg); 252 | } 253 | }, 254 | function (session, results) { 255 | var alias = results.response; 256 | // Get an access token for the app. 257 | auth.getAccessToken().then(function (token) { 258 | graph.getUnifiedGroupByAlias(token, alias) 259 | .then(function (result) { 260 | console.log(result); 261 | if (result.length === 0) { 262 | session.endDialogWithResult({ response: alias }); 263 | } else { 264 | // Repeat the dialog 265 | session.replaceDialog('askForAlias', { reprompt: true }); 266 | } 267 | }, function (error) { 268 | console.error('>>> Error getting the group: ' + error); 269 | session.endDialogWithResult({ response: alias }); 270 | }); 271 | }); 272 | } 273 | ]); 274 | 275 | // Add dialog to request a confirmation 276 | bot.dialog('askForConfirmation', [ 277 | function (session) { 278 | var msg = new builder.Message(session) 279 | .speak( 280 | "Here the summary of your request. " + 281 | "Title: " + session.privateConversationData['Title'] + " " + 282 | "Owner: " + session.privateConversationData['Owner'] + " " + 283 | "Description: " + session.privateConversationData['Description'] + " " + 284 | "SiteType: " + session.privateConversationData['SiteType'] + " " + 285 | "Alias: " + session.privateConversationData['Alias']); 286 | msg.attachmentLayout(builder.AttachmentLayout.list) 287 | msg.attachments([ 288 | new builder.ThumbnailCard(session) 289 | .title("Summary") 290 | .subtitle("Here the summary of your request") 291 | .text( 292 | "Title: " + session.privateConversationData['Title'] + "\n\n" + 293 | "Owner: " + session.privateConversationData['Owner'] + "\n\n" + 294 | "Description: " + session.privateConversationData['Description'] + "\n\n" + 295 | "SiteType: " + session.privateConversationData['SiteType'] + "\n\n" + 296 | "Alias: " + session.privateConversationData['Alias']) 297 | // .images([builder.CardImage.create(session, '/images/sp.png')]) 298 | .buttons([ 299 | builder.CardAction.imBack(session, "canceled", "Cancel"), 300 | builder.CardAction.imBack(session, "confirmed", "Confirm"), 301 | ]) 302 | ]); 303 | builder.Prompts.text(session, msg); 304 | }, 305 | function name(session, results) { 306 | session.endDialogWithResult({ response: results.response }); 307 | } 308 | ]); 309 | 310 | // Add dialog to request a confirmation 311 | bot.dialog('askForAnotherRequest', [ 312 | function (session) { 313 | var msg = new builder.Message(session) 314 | .speak("Do you want to submit another request?"); 315 | msg.attachmentLayout(builder.AttachmentLayout.list) 316 | msg.attachments([ 317 | new builder.ThumbnailCard(session) 318 | .title("Request") 319 | .subtitle("Do you want to submit another request?") 320 | .text("Please confirm or not.") 321 | // .images([builder.CardImage.create(session, '/images/sp.png')]) 322 | .buttons([ 323 | builder.CardAction.imBack(session, "no", "No"), 324 | builder.CardAction.imBack(session, "yes", "Yes"), 325 | ]) 326 | ]); 327 | builder.Prompts.text(session, msg); 328 | }, 329 | function name(session, results) { 330 | session.endDialogWithResult({ response: results.response }); 331 | } 332 | ]); 333 | 334 | /*---------------------------------------------------------------------------------------- 335 | * GRAPH API 336 | ------------------------------------------------------------------------------------------ */ 337 | 338 | /** 339 | * Get all users 340 | * @param {*} token to append in the header in order to make the request 341 | */ 342 | graph.getUsers = function (token) { 343 | var deferred = Q.defer(); 344 | 345 | // Make a request to get all users in the tenant. Use $select to only get 346 | // necessary values to make the app more performant. 347 | request.get('https://graph.microsoft.com/v1.0/users?$select=id,displayName', { 348 | auth: { 349 | bearer: token 350 | } 351 | }, function (err, response, body) { 352 | var parsedBody = JSON.parse(body); 353 | 354 | if (err) { 355 | deferred.reject(err); 356 | } else if (parsedBody.error) { 357 | deferred.reject(parsedBody.error.message); 358 | } else { 359 | // The value of the body will be an array of all users. 360 | deferred.resolve(parsedBody.value); 361 | } 362 | }); 363 | 364 | return deferred.promise; 365 | }; 366 | 367 | /** 368 | * Get user by email 369 | * @param {*} token to append in the header in order to make the request 370 | * @param {*} email the email of the user 371 | */ 372 | graph.getUserByEmail = function (token, email) { 373 | var deferred = Q.defer(); 374 | 375 | // Make a request to get all users in the tenant. Use $select to only get 376 | // necessary values to make the app more performant. 377 | request.get('https://graph.microsoft.com/v1.0/users/' + email, { 378 | auth: { 379 | bearer: token 380 | } 381 | }, function (err, response, body) { 382 | var parsedBody = JSON.parse(body); 383 | 384 | if (err) { 385 | deferred.reject(err); 386 | } else if (parsedBody.error) { 387 | deferred.reject(parsedBody.error.message); 388 | } else { 389 | deferred.resolve(parsedBody.mail); 390 | } 391 | }); 392 | 393 | return deferred.promise; 394 | }; 395 | 396 | /** 397 | * Get unified group 398 | */ 399 | graph.getUnifiedGroupByAlias = function (token, mailNickname) { 400 | var deferred = Q.defer(); 401 | 402 | // Make a request to get all users in the tenant. Use $select to only get 403 | // necessary values to make the app more performant. 404 | request.get('https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+\'Unified\') and mailNickname eq \'' + mailNickname + '\'', { 405 | auth: { 406 | bearer: token 407 | } 408 | }, function (err, response, body) { 409 | var parsedBody = JSON.parse(body); 410 | 411 | if (err) { 412 | deferred.reject(err); 413 | } else if (parsedBody.error) { 414 | deferred.reject(parsedBody.error.message); 415 | } else { 416 | deferred.resolve(parsedBody.value); 417 | } 418 | }); 419 | 420 | return deferred.promise; 421 | }; 422 | 423 | /** 424 | * Create Group in the SP list 425 | * @param {*} token to append in the header in order to make the request 426 | * @param {*} params the json in order to create a new o365 group 427 | */ 428 | graph.createGroup = (token, params) => { 429 | var deferred = Q.defer(); 430 | var endpointUrl = "https://graph.microsoft.com/v1.0/groups"; 431 | 432 | request.post({ 433 | url: endpointUrl, 434 | headers: { 435 | "Authorization": "Bearer " + token, 436 | "Content-Type": "application/json" 437 | }, 438 | body: JSON.stringify(params) 439 | }, function (err, response, body) { 440 | var parsedBody = JSON.parse(body); 441 | 442 | if (err) { 443 | deferred.reject(err); 444 | } else if (parsedBody.error) { 445 | deferred.reject(parsedBody.error.message); 446 | } else { 447 | // The value of the body will be an array of all users. 448 | deferred.resolve(parsedBody.id); 449 | } 450 | }); 451 | 452 | return deferred.promise; 453 | } 454 | 455 | /** 456 | * 457 | * @param {*} token to append in the header in order to make the request 458 | * @param {*} params the json in order to create a new list item 459 | */ 460 | graph.postListItem = (token, params) => { 461 | var deferred = Q.defer(); 462 | var listId = config.listId; 463 | var endpointUrl = "https://graph.microsoft.com/v1.0/sites/root/lists/" + listId + "/items"; 464 | 465 | request.post({ 466 | url: endpointUrl, 467 | headers: { 468 | "Authorization": "Bearer " + token, 469 | "Content-Type": "application/json" 470 | }, 471 | body: JSON.stringify(params) 472 | }, function (err, response, body) { 473 | var parsedBody = JSON.parse(body); 474 | 475 | if (err) { 476 | deferred.reject(err); 477 | } else if (parsedBody.error) { 478 | deferred.reject(parsedBody.error.message); 479 | } else { 480 | // The value of the body will be an array of all users. 481 | deferred.resolve(parsedBody.value); 482 | } 483 | }); 484 | 485 | return deferred.promise; 486 | } 487 | 488 | 489 | /*---------------------------------------------------------------------------------------- 490 | * Authentication 491 | ------------------------------------------------------------------------------------------ */ 492 | // The auth module object. 493 | var auth = {}; 494 | 495 | /** 496 | * Get access token 497 | */ 498 | auth.getAccessToken = function () { 499 | var deferred = Q.defer(); 500 | 501 | // These are the parameters necessary for the OAuth 2.0 Client Credentials Grant Flow. 502 | // For more information, see Service to Service Calls Using Client Credentials (https://msdn.microsoft.com/library/azure/dn645543.aspx). 503 | var requestParams = { 504 | grant_type: 'client_credentials', 505 | client_id: config.clientId, 506 | client_secret: config.clientSecret, 507 | resource: config.resource 508 | }; 509 | 510 | /** 511 | * post: Make a request to the token issuing endpoint 512 | */ 513 | request.post({ url: config.tokenEndpoint, form: requestParams }, function (err, response, body) { 514 | var parsedBody = JSON.parse(body); 515 | 516 | if (err) { 517 | deferred.reject(err); 518 | } else if (parsedBody.error) { 519 | deferred.reject(parsedBody.error_description); 520 | } else { 521 | // If successful, return the access token. 522 | deferred.resolve(parsedBody.access_token); 523 | } 524 | }); 525 | 526 | return deferred.promise; 527 | }; 528 | --------------------------------------------------------------------------------