├── 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 | 
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 |
--------------------------------------------------------------------------------