Teams Toolkit with Azure

Neil HaddleyMay 29, 2021

Get started with Microsoft Teams app development using Azure.

Visual Studio Extension

Teams Toolkit makes creating Teams Tabs/Azure applications easier.

Add Teams Toolkit Extension to Visual Studio Code

Add Teams Toolkit Extension to Visual Studio Code

Click the Teams Toolkit to open Wizard

Click the Teams Toolkit to open Wizard

Configure Quick Start items

Configure Quick Start items

Sign-in to M365 account

Sign-in to M365 account

Sign-in to Azure

Sign-in to Azure

Click Create New Project link

Click Create New Project link

Start from a sample

Start from a sample

Select the Todo List with backend on Azure sample

Select the Todo List with backend on Azure sample

Download the sample code from Github

Download the sample code from Github

Notice that the sample code is a work in progress

Notice that the sample code is a work in progress

Open Command Palette

Open Command Palette

Use Team: Provision in the Cloud command to configure Azure

Use Team: Provision in the Cloud command to configure Azure

Enter a hard to guess database administrator account name

Enter a hard to guess database administrator account name

Configures Azure Services

Configures Azure Services

Provisioning progress

Provisioning progress

Newly created resource group

Newly created resource group

Open SQL Database to copy "Server name"

Open SQL Database to copy "Server name"

Open Azure Data Studio and enter SQL Server connection details

Open Azure Data Studio and enter SQL Server connection details

Ensure that Azure firewall rule is added

Ensure that Azure firewall rule is added

Use a Query tab in Azure Data Studio to create the Todo list

Use a Query tab in Azure Data Studio to create the Todo list

Use Teams: Deploy to the Cloud command

Use Teams: Deploy to the Cloud command

Deploy the React code and the Azure Function (API)

Deploy the React code and the Azure Function (API)

Use Teams: Build Teams Package command

Use Teams: Build Teams Package command

Use Teams: Publish to Teams command

Use Teams: Publish to Teams command

Install to organization

Install to organization

Open Microsoft Teams admin center. Locate app "pending approval"

Open Microsoft Teams admin center. Locate app "pending approval"

Publish app

Publish app

Click the Publish button

Click the Publish button

App has been added to Microsoft Teams

App has been added to Microsoft Teams

Use Add to a team button

Use Add to a team button

Select a Team (click Set up a tab)

Select a Team (click Set up a tab)

View tab configuration page

View tab configuration page

Welcome page click Start to progress

Welcome page click Start to progress

Login is required

Login is required

Provide consent

Provide consent

If no tasks to display

If no tasks to display

Add task (press enter)

Add task (press enter)

using map to render all of the tasks

using map to render all of the tasks

A row has been added to the database

A row has been added to the database

The function app is used to implement a REST api

The function app is used to implement a REST api

Tab.js

Tab.js

An extract from Tab.js

TEXT
1{this.state.showLoginPage === true && <div className="auth">
2          <Profile userInfo={this.state.userInfo} />
3          <h2>Welcome to To Do List App!</h2>
4          <Button primary onClick={() => this.loginBtnClick()}>Start</Button>
5        </div>}

Profile.js

TEXT
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4import React from 'react';
5import './Profile.css';
6import defaultPhoto from '../images/default-photo.png'
7
8class Profile extends React.Component {
9  render() {
10    return (
11      <div className="profile">
12        <div className="photo">
13          <img src={defaultPhoto} alt="avatar"/>
14        </div>
15        <div className="info">
16          <div className="name">{this.props.userInfo.displayName}</div>
17          <div className="email">{this.props.userInfo.preferredUserName}</div>
18        </div>
19      </div>
20    );
21  }
22}
23
24export default Profile;

An extract from Tab.js

TEXT
1async componentDidMount() {
2    await this.initTeamsFx();
3    await this.initData();
4  }
5
6  async initTeamsFx() {
7    // Initialize configuration from environment variables and set the global instance
8    loadConfiguration({
9      authentication: {
10        initiateLoginEndpoint: process.env.REACT_APP_START_LOGIN_PAGE_URL,
11        simpleAuthEndpoint: process.env.REACT_APP_TEAMSFX_ENDPOINT,
12        clientId: process.env.REACT_APP_CLIENT_ID
13      },
14      resources: [
15        {
16          type: ResourceType.API,
17          name: "default",
18          properties: {
19            endpoint: process.env.REACT_APP_FUNC_ENDPOINT
20          }
21        }
22      ]
23    });
24    const credential = new TeamsUserCredential();
25    // Get the user info from access token
26    const userInfo = await credential.getUserInfo();
27
28    this.setState({
29      userInfo: userInfo
30    });
31
32    this.credential = credential;
33    this.scope = ["User.Read", "User.ReadBasic.All"];
34    this.channelOrChatId = await this.getChannelOrChatId();
35  }
36
37  async initData() {
38    if (!await this.checkIsConsentNeeded()) {
39      await this.getItems();
40    }
41  }
42
43  async loginBtnClick() {
44    try {
45      // Popup login page to get user's access token
46      await this.credential.login(this.scope);
47    } catch (err) {
48      alert("Login failed: " + err);
49      return;
50    }
51    await this.initData();
52  }
53
54  async checkIsConsentNeeded() {
55    try {
56      await this.credential.getToken(this.scope);
57    } catch (error) {
58      this.setState({
59        showLoginPage: true
60      });
61      return true;
62    }
63    this.setState({
64      showLoginPage: false
65    });
66    return false;
67  }

An extract from Tab.js

TEXT
1{this.state.initialized && !this.state.items.length && !this.state.isAddingItem && <div className="no-item">
2                <div>
3                  <img src={noItemimage} alt="no item" />
4                </div>
5                <div>
6                  <h2>No tasks</h2>
7                  <p>Add more tasks to make you day productive.</p>
8                </div>
9              </div>}

An extract from Tab.js

TEXT
1{this.state.isAddingItem && <div className="item add">
2                <div className="complete">
3                  <Checkbox
4                    disabled
5                    className="is-completed-input"
6                  />
7                </div>
8                <div className="description">
9                  <Input
10                    autoFocus
11                    type="text"
12                    value={this.state.newItemDescription}
13                    onChange={(e) => this.setState({ newItemDescription: e.target.value })}
14                    onKeyDown={(e) => {
15                      if (e.key === 'Enter') {
16                        this.onAddItem();
17                      }
18                    }}
19                    onBlur={() => {
20                      if (this.state.newItemDescription) {
21                        this.onAddItem();
22                      }
23                      this.setState({
24                        isAddingItem: false,
25                      });
26                    }}
27                    className="text"
28                  />
29                </div>
30              </div>}

An extract from Tab.js

TEXT
1const items = this.state.items?.map((item, index) =>
2      <div key={index} className="item">
3        <div className="complete">
4          <Checkbox
5            checked={this.state.items[index].isCompleted}
6            onChange={(e, { checked }) => this.onCompletionStatusChange(item.id, index, checked)}
7            className="is-completed-input"
8          />
9        </div>
10        <div className="description">
11          <Input
12            value={this.state.items[index].description}
13            onChange={(e) => this.handleInputChange(index, "description", e.target.value)}
14            onKeyDown={(e) => {
15              if (e.key === 'Enter') {
16                this.onUpdateItem(item.id, this.state.items[index].description);
17                e.target.blur();
18              }
19            }}
20            onBlur={() => this.onUpdateItem(item.id, this.state.items[index].description)}
21            className={"text" + (this.state.items[index].isCompleted ? " is-completed" : "")}
22          />
23        </div>
24        <Creator objectId={item.objectId} credential={this.credential} scope={this.scope} />
25        <div className="action">
26          <MenuButton
27            trigger={<Button content="..." />}
28            menu={[
29              {
30                content: 'Delete',
31                onClick: () => this.onDeleteItem(item.id)
32              }
33            ]}
34            on="click"
35          />
36        </div>
37      </div>
38    );

index.js

TEXT
1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4const {
5    loadConfiguration,
6    OnBehalfOfUserCredential,
7    DefaultTediousConnectionConfiguration,
8} = require("@microsoft/teamsfx");
9const { Connection, Request } = require('tedious');
10
11/**
12 * This function handles requests sent from teamsfx client SDK.
13 * The HTTP request should contain an SSO token in the header and any content in the body.
14 * The SSO token should be queried from Teams client by teamsfx client SDK.
15 * Before trigger this function, teamsfx binding would process the SSO token and generate teamsfx configuration.
16 *
17 * This function initializes the teamsfx Server SDK with the configuration and calls these APIs:
18 * - getUserInfo() - Get the user's information from the received SSO token.
19 * - getMicrosoftGraphClientWithUserIdentity() - Get a graph client to access user's Microsoft 365 data.
20 *
21 * The response contains multiple message blocks constructed into a JSON object, including:
22 * - An echo of the request body.
23 * - The display name encoded in the SSO token.
24 * - Current user's Microsoft 365 profile if the user has consented.
25 *
26 * @param {Context} context - The Azure Functions context object.
27 * @param {HttpRequest} req - The HTTP request.
28 * @param {teamsfxConfig} config - The teamsfx configuration generated by teamsfx binding.
29 */
30module.exports = async function (context, req, config) {
31    let connection;
32    // Initialize configuration from environment variables and set the global instance
33    loadConfiguration()
34    try {
35        connection = await getSQLConnection();
36        const method = req.method.toLowerCase();
37        const accessToken = config.AccessToken;
38        const credential = new OnBehalfOfUserCredential(accessToken);
39        // Get the user info from access token
40        const currentUser = await credential.getUserInfo();
41        const objectId = currentUser.objectId;
42        var query;
43
44        switch (method) {
45            case "get":
46                query = `select id, description, isCompleted, objectId from dbo.Todo where channelOrChatId = '${req.query.channelOrChatId}'`;
47                break;
48            case "put":
49                if (req.body.description) {
50                    query = `update dbo.Todo set description = N'${req.body.description}' where id = ${req.body.id}`;
51                } else {
52                    query = `update dbo.Todo set isCompleted = ${req.body.isCompleted ? 1 : 0} where id = ${req.body.id}`;
53                }
54                break;
55            case "post":
56                query = `insert into dbo.Todo (description, objectId, isCompleted, channelOrChatId) values (N'${req.body.description}','${objectId}',${req.body.isCompleted ? 1 : 0},'${req.body.channelOrChatId}')`;
57                break;
58            case "delete":
59                query = "delete from dbo.Todo where " + (req.body ? `id = ${req.body.id}` : `objectId = '${objectId}'`);
60                break;
61        }
62        // Execute SQL through TeamsFx server SDK generated connection and return result
63        const result = await execQuery(query, connection);
64        return {
65            status: 200,
66            body: result
67        }
68    }
69    catch (err) {
70        return {
71            status: 500,
72            body: {
73                error: err.message
74            }
75        }
76    }
77    finally {
78        if (connection) {
79            connection.close();
80        }
81    }
82}
83
84async function getSQLConnection() {
85    const sqlConnectConfig = new DefaultTediousConnectionConfiguration();
86    const config = await sqlConnectConfig.getConfig();
87    const connection = new Connection(config);
88    return new Promise((resolve, reject) => {
89        connection.on('connect', err => {
90            if (err) {
91                reject(err);
92            }
93            resolve(connection);
94        })
95        connection.on('debug', function (err) {
96            console.log('debug:', err);
97        });
98    })
99}
100
101async function execQuery(query, connection) {
102    return new Promise((resolve, reject) => {
103        const res = [];
104        const request = new Request(query, (err) => {
105            if (err) {
106                reject(err);
107            }
108        });
109
110        request.on('row', columns => {
111            const row = {};
112            columns.forEach(column => {
113                row[column.metadata.colName] = column.value;
114            });
115            res.push(row)
116        });
117
118        request.on('requestCompleted', () => {
119            resolve(res)
120        });
121
122        request.on("error", err => {
123            reject(err);
124        });
125
126        connection.execSql(request);
127    })
128}

References