Criar Office de complementos com o Microsoft Graph
Este tutorial ensina como criar um Office para Excel que usa a API do Microsoft Graph para recuperar informações de calendário para um usuário.
Dica
Se você preferir apenas baixar o tutorial concluído, poderá baixar ou clonar o GitHub repositório.
Pré-requisitos
Antes de iniciar essa demonstração, você deve terNode.js e o Yarn instalados em sua máquina de desenvolvimento. Se você não tiver Node.js ou Yarn, visite o link anterior para opções de download.
Observação
Windows usuários podem precisar instalar o Python e Ferramentas de Build do Visual Studio para dar suporte a módulos NPM que precisam ser compilados a partir de C/C++. O Node.js instalador no Windows oferece uma opção para instalar automaticamente essas ferramentas. Como alternativa, você pode seguir instruções em https://github.com/nodejs/node-gyp#on-windows.
Você também deve ter uma conta pessoal da Microsoft com uma caixa de correio em Outlook.com, ou uma conta de trabalho ou de estudante da Microsoft. Se você não tiver uma conta da Microsoft, há algumas opções para obter uma conta gratuita:
- Você pode se inscrever em uma nova conta pessoal da Microsoft.
- Você pode se inscrever no programa Microsoft 365 desenvolvedor para obter uma assinatura Microsoft 365 gratuita.
Observação
Este tutorial foi escrito com o Nó versão 14.15.0 e o Yarn versão 1.22.0. As etapas neste guia podem funcionar com outras versões, mas que não foram testadas.
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub.
Criar um Suplemento do Office
Neste exercício, você criará uma solução de Office de complemento usando o Express. A solução consistirá em duas partes.
- O complemento, implementado como arquivos HTML e JavaScript estáticos.
- Um Node.js/Express que atende ao add-in e implementa uma API da Web para recuperar dados do add-in.
Criar o servidor
Abra sua interface de linha de comando (CLI), navegue até um diretório onde você deseja criar seu projeto e execute o seguinte comando para gerar um arquivo package.json.
yarn init
Insira valores para os prompts conforme apropriado. Se você não tiver certeza, os valores padrão serão bons.
Execute os seguintes comandos para instalar dependências.
yarn add express@4.17.1 express-promise-router@4.1.0 dotenv@10.0.0 node-fetch@2.6.1 jsonwebtoken@8.5.1@ yarn add jwks-rsa@2.0.4 @azure/msal-node@1.3.0 @microsoft/microsoft-graph-client@3.0.0 yarn add date-fns@2.23.0 date-fns-tz@1.1.6 isomorphic-fetch@3.0.0 windows-iana@5.0.2 yarn add -D typescript@4.3.5 ts-node@10.2.0 nodemon@2.0.12 @types/node@16.4.13 @types/express@4.17.13 yarn add -D @types/node-fetch@2.5.12 @types/jsonwebtoken@8.5.4 @types/microsoft-graph@2.0.0 yarn add -D @types/office-js@1.0.195 @types/jquery@3.5.6 @types/isomorphic-fetch@0.0.35
Execute o seguinte comando para gerar um arquivo tsconfig.json.
tsc --init
Abra ./tsconfig.json em um editor de texto e faça as seguintes alterações.
- Altere o
target
valor paraes6
. - Descomungar o
outDir
valor e defini-lo como./dist
. - Descomungar o
rootDir
valor e defini-lo como./src
.
- Altere o
Abra ./package.json e adicione a seguinte propriedade ao JSON.
"scripts": { "start": "nodemon ./src/server.ts", "build": "tsc --project ./" },
Execute o seguinte comando para gerar e instalar certificados de desenvolvimento para o seu complemento.
npx office-addin-dev-certs install
Se solicitado a confirmar, confirme as ações. Depois que o comando é concluído, você verá uma saída semelhante à seguinte.
You now have trusted access to https://localhost. Certificate: <path>\localhost.crt Key: <path>\localhost.key
Crie um novo arquivo chamado .env na raiz do seu projeto e adicione o código a seguir.
AZURE_APP_ID='YOUR_APP_ID_HERE' AZURE_CLIENT_SECRET='YOUR_CLIENT_SECRET_HERE' TLS_CERT_PATH='PATH_TO_LOCALHOST.CRT' TLS_KEY_PATH='PATH_TO_LOCALHOST.KEY'
Substitua
PATH_TO_LOCALHOST.CRT
pelo caminho para localhost.crtPATH_TO_LOCALHOST.KEY
e pelo caminho para localhost.key saída pelo comando anterior.Crie um novo diretório na raiz do seu projeto denominado src.
Crie dois diretórios no diretório ./src : addin e api.
Crie um novo arquivo chamado auth.ts no diretório ./src/api e adicione o código a seguir.
import Router from 'express-promise-router'; const authRouter = Router(); // TODO: Implement this router export default authRouter;
Crie um novo arquivo chamado graph.ts no diretório ./src/api e adicione o código a seguir.
import Router from 'express-promise-router'; const graphRouter = Router(); // TODO: Implement this router export default graphRouter;
Crie um novo arquivo chamado server.ts no diretório ./src e adicione o código a seguir.
import express from 'express'; import https from 'https'; import fs from 'fs'; import dotenv from 'dotenv'; import path from 'path'; // Load .env file dotenv.config(); import authRouter from './api/auth'; import graphRouter from './api/graph'; const app = express(); const PORT = 3000; // Support JSON payloads app.use(express.json()); app.use(express.static(path.join(__dirname, 'addin'))); app.use(express.static(path.join(__dirname, 'dist/addin'))); app.use('/auth', authRouter); app.use('/graph', graphRouter); const serverOptions = { key: fs.readFileSync(process.env.TLS_KEY_PATH!), cert: fs.readFileSync(process.env.TLS_CERT_PATH!), }; https.createServer(serverOptions, app).listen(PORT, () => { console.log(`⚡️[server]: Server is running at https://localhost:${PORT}`); });
Criar o suplemento
Crie um novo arquivo chamado taskpane.html no diretório ./src/addin e adicione o código a seguir.
<html> <head> <link rel="stylesheet" href="https://static2.sharepointonline.com/files/fabric/office-ui-fabric-core/11.0.0/css/fabric.min.css"/> <link rel="stylesheet" href="taskpane.css"/> </head> <body class="ms-Fabric"> <div class="container"> <p class="ms-fontSize-32">Checking authentication...</p> </div> <div class="status"></div> <div class="overlay"> <p class="ms-fontSize-24 ms-fontColor-white">Working...</p> </div> <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.5.1.min.js"></script> <script src="https://appsforoffice.microsoft.com/lib/beta/hosted/office.js"></script> <script src="https://cdn.jsdelivr.net/npm/luxon@1.25.0/build/global/luxon.min.js"></script> <script src="taskpane.js"></script> </body> </html>
Crie um novo arquivo chamado taskpane.css no diretório ./src/addin e adicione o código a seguir.
.container { margin: 10px; } .status { margin: 10px; } .overlay { position: fixed; display: none; width: 100%; height: 100%; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 2; cursor: pointer; } .overlay p { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); } .status-card { padding: 1em; } .primary-button { padding: .5em; color: white; background-color: #0078d4; border: none; display: block; } .success-msg { background-color: #dff6dd; } .error-msg { background-color: #fde7e9; } .date-picker { display: block; padding: .5em; margin: 10px 0; } .form-input { display: block; padding: .5em; margin: 10px 0; width: 100%; }
Crie um novo arquivo chamado taskpane.js no diretório ./src/addin e adicione o código a seguir.
// TEMPORARY CODE TO VERIFY ADD-IN LOADS 'use strict'; Office.onReady(info => { if (info.host === Office.HostType.Excel) { $(function() { $('p').text('Hello World!!'); }); } });
Crie um novo diretório no diretório .src/addin denominado ativos.
Adicione três arquivos PNG neste diretório de acordo com a tabela a seguir.
Nome do arquivo Tamanho em pixels icon-80.png 80x80 icon-32.png 32x32 icon-16.png 16 x 16 Observação
Você pode usar qualquer imagem que quiser para esta etapa. Você também pode baixar as imagens usadas neste exemplo diretamente GitHub.
Crie um novo diretório na raiz do manifesto nomeado do projeto.
Crie um novo arquivo chamado manifest.xml na pasta ./manifest e adicione o código a seguir. Substitua
NEW_GUID_HERE
por um novo GUID, comob4fa03b8-1eb6-4e8b-a380-e0476be9e019
.<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:ov="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="TaskPaneApp"> <Id>NEW_GUID_HERE</Id> <Version>1.0.0.0</Version> <ProviderName>Contoso</ProviderName> <DefaultLocale>en-US</DefaultLocale> <DisplayName DefaultValue="Excel Graph Calendar"/> <Description DefaultValue="An add-in that shows how to call Microsoft Graph to access the user's calendar."/> <IconUrl DefaultValue="https://localhost:3000/assets/icon-32.png"/> <HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-80.png"/> <SupportUrl DefaultValue="https://www.contoso.com/help"/> <AppDomains> <AppDomain>https://localhost:3000</AppDomain> </AppDomains> <Hosts> <Host Name="Workbook"/> </Hosts> <DefaultSettings> <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/> </DefaultSettings> <Permissions>ReadWriteDocument</Permissions> <VersionOverrides xmlns="http://schemas.microsoft.com/office/taskpaneappversionoverrides" xsi:type="VersionOverridesV1_0"> <Hosts> <Host xsi:type="Workbook"> <DesktopFormFactor> <GetStarted> <Title resid="GetStarted.Title"/> <Description resid="GetStarted.Description"/> <LearnMoreUrl resid="GetStarted.LearnMoreUrl"/> </GetStarted> <ExtensionPoint xsi:type="PrimaryCommandSurface"> <OfficeTab id="TabHome"> <Group id="CommandsGroup"> <Label resid="CommandsGroup.Label"/> <Icon> <bt:Image size="16" resid="Icon.16x16"/> <bt:Image size="32" resid="Icon.32x32"/> <bt:Image size="80" resid="Icon.80x80"/> </Icon> <Control xsi:type="Button" id="TaskpaneButton"> <Label resid="TaskpaneButton.Label"/> <Supertip> <Title resid="TaskpaneButton.Label"/> <Description resid="TaskpaneButton.Tooltip"/> </Supertip> <Icon> <bt:Image size="16" resid="Icon.16x16"/> <bt:Image size="32" resid="Icon.32x32"/> <bt:Image size="80" resid="Icon.80x80"/> </Icon> <Action xsi:type="ShowTaskpane"> <TaskpaneId>ImportCalendar</TaskpaneId> <SourceLocation resid="Taskpane.Url"/> </Action> </Control> </Group> </OfficeTab> </ExtensionPoint> </DesktopFormFactor> </Host> </Hosts> <Resources> <bt:Images> <bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/> <bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/> <bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/> </bt:Images> <bt:Urls> <bt:Url id="GetStarted.LearnMoreUrl" DefaultValue="https://docs.microsoft.com/graph"/> <bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/> </bt:Urls> <bt:ShortStrings> <bt:String id="GetStarted.Title" DefaultValue="Get started with the Excel Graph Calendar add-in!"/> <bt:String id="CommandsGroup.Label" DefaultValue="Graph Calendar"/> <bt:String id="TaskpaneButton.Label" DefaultValue="Import Calendar"/> </bt:ShortStrings> <bt:LongStrings> <bt:String id="GetStarted.Description" DefaultValue="Add-in loaded succesfully. Go to the HOME tab and click the 'Import Calendar' button to get started."/> <bt:String id="TaskpaneButton.Tooltip" DefaultValue="Click to open the Import Calendar task pane"/> </bt:LongStrings> </Resources> <WebApplicationInfo> <Id>YOUR_APP_ID_HERE</Id> <Resource>api://localhost:3000/YOUR_APP_ID_HERE</Resource> <Scopes> <Scope>openid</Scope> <Scope>profile</Scope> <Scope>access_as_user</Scope> </Scopes> </WebApplicationInfo> </VersionOverrides> </OfficeApp>
Carregar lado a lado o complemento no Excel
Inicie o servidor executando o seguinte comando.
yarn start
Abra seu navegador e navegue até
https://localhost:3000/taskpane.html
. Você deve ver umaNot loaded
mensagem.No navegador, vá para Office.com e entre. Selecione Criar na barra de ferramentas à esquerda e selecione Planilha.
Selecione a guia Inserir e selecione Office-ins.
Selecione Upload Meu Complemento e, em seguida, selecione Procurar. Upload arquivo ./manifest/manifest.xml.
Selecione o botão Importar Calendário na guia Início para abrir o taskpane.
Depois que o taskpane abrir, você deverá ver uma
Hello World!
mensagem.
Registrar o aplicativo no portal
Neste exercício, você criará um novo registro de aplicativo Web do Azure AD usando o Azure Active Directory de administração.
Abra um navegador e navegue até o centro de administração do Azure Active Directory. Faça logon usando uma conta pessoal (também conhecida como Conta da Microsoft) ou Conta Corporativa ou de Estudante.
Selecione Azure Active Directory na navegação esquerda e selecione Registros de aplicativos em Gerenciar.
Selecione Novo registro. Na página Registrar um aplicativo, defina os valores da seguinte forma.
- Defina Nome para
Office Add-in Graph Tutorial
. - Defina Tipos de conta com suporte para Contas em qualquer diretório organizacional e contas pessoais da Microsoft.
- Em URI de Redirecionamento, defina o primeiro menu suspenso para
Single-page application (SPA)
e defina o valor comohttps://localhost:3000/consent.html
.
- Defina Nome para
Selecione Registrar. Na página Office Tutorial do Graph, copie o valor da ID do Aplicativo (cliente) e salve-a, você precisará dele na próxima etapa.
Selecione Autenticação em Gerenciar. Localize a seção Concessão Implícita e habilita tokens de Acesso e tokens de ID. Selecione Salvar.
Selecione Certificados e segredos sob Gerenciar. Selecione o botão Novo segredo do cliente. Insira um valor em Descrição e selecione uma das opções para Expira em e selecione Adicionar.
Copie o valor secreto do cliente antes de sair desta página. Você precisará dele na próxima etapa.
Importante
Este segredo do cliente nunca é mostrado novamente, portanto, copie-o agora.
Selecione permissões de API em Gerenciar, em seguida, selecione Adicionar uma permissão.
Selecione Microsoft Graph, em seguida, Permissões delegadas.
Selecione as seguintes permissões e selecione Adicionar permissões.
- offline_access - isso permitirá que o aplicativo atualize tokens de acesso quando eles expirarem.
- Calendars.ReadWrite - isso permitirá que o aplicativo leia e escreva no calendário do usuário.
- MailboxSettings.Read - isso permitirá que o aplicativo receba o fuso horário do usuário a partir de suas configurações de caixa de correio.
Configurar Office login único do Add-in
Nesta seção, você atualizará o registro do aplicativo para dar suporte Office SSO (SSO).
Selecione Expor uma API. Na seção Escopos definidos por esta API, selecione Adicionar um escopo. Quando solicitado a definir um URI de ID do aplicativo, de definir o valor como
api://localhost:3000/YOUR_APP_ID_HERE
,YOUR_APP_ID_HERE
substituindo pela ID do aplicativo. Escolha Salvar e continuar.Preencha os campos da seguinte forma e selecione Adicionar escopo.
- Nome do escopo:
access_as_user
- Who podem consentir?: Administradores e usuários
- Nome de exibição de consentimento do administrador:
Access the app as the user
- Descrição do consentimento do administrador:
Allows Office Add-ins to call the app's web APIs as the current user.
- Nome de exibição de consentimento do usuário:
Access the app as you
- Descrição do consentimento do usuário:
Allows Office Add-ins to call the app's web APIs as you.
- Estado: Habilitado
- Nome do escopo:
Na seção Aplicativos cliente autorizados, selecione Adicionar um aplicativo cliente. Insira uma ID do cliente na lista a seguir, habilita o escopo em Escopos Autorizados e selecione Adicionar aplicativo. Repita esse processo para cada uma das IDs do cliente na lista.
d3590ed6-52b3-4102-aeff-aad2292ab01c
(Microsoft Office)ea5a67f6-b6f3-4338-b240-c655ddc3cc8e
(Microsoft Office)57fb890c-0dab-4253-a5e0-7188c88b2bb4
(Office na Web)08e18876-6177-487e-b8b5-cf950c1e598c
(Office na Web)
Adicionar autenticação do Azure AD
Neste exercício, você permitirá Office SSO (SSO) de complemento único no complemento e estenderá a API Web para dar suporte ao fluxo em nome do usuário. Isso é necessário para obter o token de acesso OAuth necessário para chamar o microsoft Graph.
Visão Geral
Office O SSO do add-in fornece um token de acesso, mas esse token só permite que o add-in chame sua própria API da Web. Ele não habilita o acesso direto ao microsoft Graph. O processo funciona da seguinte forma.
- O complemento obtém um token chamando getAccessToken. A audiência desse token (a declaração) é a ID do aplicativo do registro do aplicativo
aud
do complemento. - O complemento envia esse token no
Authorization
header quando faz uma chamada para a API da Web. - A API da Web valida o token e, em seguida, usa o fluxo em nome do fluxo para trocar esse token por um token Graph Microsoft. A audiência desse novo token é
https://graph.microsoft.com
. - A API da Web usa o novo token para fazer chamadas para o microsoft Graph e retorna os resultados de volta para o add-in.
Configurar a solução
Abra ./.env e atualize o e com a ID do aplicativo e o segredo do
AZURE_APP_ID
cliente do registro doAZURE_CLIENT_SECRET
aplicativo.Importante
Se você estiver usando o controle de origem, como git, agora seria um bom momento para excluir o arquivo .env do controle de origem para evitar o vazamento inadvertida da ID do aplicativo e o segredo do cliente.
Abra ./manifest/manifest.xml e substitua todas as instâncias de pelo ID do aplicativo
YOUR_APP_ID_HERE
do registro do aplicativo.Crie um novo arquivo no diretório ./src/addin chamado config.js e adicione o código a seguir, substituindo pela ID do aplicativo do
YOUR_APP_ID_HERE
registro do aplicativo.authConfig = { clientId: 'YOUR_APP_ID_HERE' };
Implementar login
Abra ./src/api/auth.ts e adicione as instruções a seguir
import
na parte superior do arquivo.import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as msal from '@azure/msal-node';
Adicione o seguinte código após as
import
instruções.// Initialize an MSAL confidential client const msalClient = new msal.ConfidentialClientApplication({ auth: { clientId: process.env.AZURE_APP_ID!, clientSecret: process.env.AZURE_CLIENT_SECRET! } }); const keyClient = jwksClient({ jwksUri: 'https://login.microsoftonline.com/common/discovery/v2.0/keys' }); // Parses the JWT header and retrieves the appropriate public key function getSigningKey(header: JwtHeader, callback: SigningKeyCallback): void { if (header) { keyClient.getSigningKey(header.kid!, (err, key) => { if (err) { callback(err, undefined); } else { callback(null, key.getPublicKey()); } }); } } // Validates a JWT and returns it if valid async function validateJwt(authHeader: string): Promise<string | null> { return new Promise((resolve, reject) => { const token = authHeader.split(' ')[1]; // Ensure that the audience matches the app ID // and the signature is valid const validationOptions = { audience: process.env.AZURE_APP_ID }; jwt.verify(token, getSigningKey, validationOptions, (err, payload) => { if (err) { console.log(`Verify error: ${JSON.stringify(err)}`); resolve(null); } else { resolve(token); } }); }); } // Gets a Graph token from the API token contained in the // auth header export async function getTokenOnBehalfOf(authHeader: string): Promise<string | undefined> { // Validate the supplied token if present const token = await validateJwt(authHeader); if (token) { const result = await msalClient.acquireTokenOnBehalfOf({ oboAssertion: token, skipCache: true, scopes: ['https://graph.microsoft.com/.default'] }); return result?.accessToken; } }
Este código inicializa um cliente confidencial do MSALe exporta uma função para obter um token Graph do token enviado pelo complemento.
Adicione o código a seguir antes da
export default authRouter;
linha.// Checks if the add-in token can be silently exchanged // for a Graph token. If it can, the user is considered // authenticated. If not, then the add-in needs to do an // interactive login so the user can consent. authRouter.get('/status', async function(req, res) { // Validate access token const authHeader = req.headers['authorization']; if (authHeader) { try { const graphToken = await getTokenOnBehalfOf(authHeader); // If a token was returned, consent is already // granted if (graphToken) { console.log(`Graph token: ${graphToken}`); res.status(200).json({ status: 'authenticated' }); } else { // Respond that consent is required res.status(200).json({ status: 'consent_required' }); } } catch (error) { // Respond that consent is required if the error indicates, // otherwise return the error. const payload = error.name === 'InteractionRequiredAuthError' ? { status: 'consent_required' } : { status: 'error', error: error}; res.status(200).json(payload); } } else { // No auth header res.status(401).end(); } } );
Esse código implementa uma API ( ) que verifica se o token de complemento pode ser trocado silenciosamente por um token
GET /auth/status
Graph de usuário. O add-in usará essa API para determinar se ele precisa apresentar um logon interativo ao usuário.Abra ./src/addin/taskpane.js e adicione o seguinte código ao arquivo.
// Handle to authentication pop dialog let authDialog = undefined; // Build a base URL from the current location function getBaseUrl() { return location.protocol + '//' + location.hostname + (location.port ? ':' + location.port : ''); } // Process the response back from the auth dialog function processConsent(result) { const message = JSON.parse(result.message); authDialog.close(); if (message.status === 'success') { showMainUi(); } else { const error = JSON.stringify(message.result, Object.getOwnPropertyNames(message.result)); showStatus(`An error was returned from the consent dialog: ${error}`, true); } } // Use the Office Dialog API to show the interactive // login UI function showConsentPopup() { const authDialogUrl = `${getBaseUrl()}/consent.html`; Office.context.ui.displayDialogAsync(authDialogUrl, { height: 60, width: 30, promptBeforeOpen: false }, (result) => { if (result.status === Office.AsyncResultStatus.Succeeded) { authDialog = result.value; authDialog.addEventHandler(Office.EventType.DialogMessageReceived, processConsent); } else { // Display error const error = JSON.stringify(error, Object.getOwnPropertyNames(error)); showStatus(`Could not open consent prompt dialog: ${error}`, true); } }); } // Inform the user we need to get their consent function showConsentUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Consent for Microsoft Graph access needed' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'In order to access your calendar, we need to get your permission to access the Microsoft Graph.' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'We only need to do this once, unless you revoke your permission.' }).appendTo('.container'); $('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: 'Please click or tap the button below to give permission (opens a popup window).' }).appendTo('.container'); $('<button/>', { class: 'primary-button', text: 'Give permission' }).on('click', showConsentPopup) .appendTo('.container'); } // Display a status function showStatus(message, isError) { $('.status').empty(); $('<div/>', { class: `status-card ms-depth-4 ${isError ? 'error-msg' : 'success-msg'}` }).append($('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: isError ? 'An error occurred' : 'Success' })).append($('<p/>', { class: 'ms-fontSize-16 ms-fontWeight-regular', text: message })).appendTo('.status'); } function toggleOverlay(show) { $('.overlay').css('display', show ? 'block' : 'none'); }
Este código adiciona funções para atualizar a interface do usuário e para usar a API Office dialog para iniciar um fluxo de autenticação interativa.
Adicione a seguinte função para implementar uma interface do usuário principal temporária.
function showMainUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Authenticated!' }).appendTo('.container'); }
Substitua a chamada
Office.onReady
existente pelo seguinte.Office.onReady(info => { // Only run if we're inside Excel if (info.host === Office.HostType.Excel) { $(async function() { let apiToken = ''; try { apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); console.log(`API Token: ${apiToken}`); } catch (error) { console.log(`getAccessToken error: ${JSON.stringify(error)}`); // Fall back to interactive login showConsentUi(); } // Call auth status API to see if we need to get consent const authStatusResponse = await fetch(`${getBaseUrl()}/auth/status`, { headers: { authorization: `Bearer ${apiToken}` } }); const authStatus = await authStatusResponse.json(); if (authStatus.status === 'consent_required') { showConsentUi(); } else { // report error if (authStatus.status === 'error') { const error = JSON.stringify(authStatus.error, Object.getOwnPropertyNames(authStatus.error)); showStatus(`Error checking auth status: ${error}`, true); } else { showMainUi(); } } }); } });
Considere o que esse código faz.
- Quando o painel de tarefas é carregado pela primeira vez, ele chama para obter um token com escopo para a API web do
getAccessToken
complemento. - Ele usa esse token para chamar a API para verificar se o usuário deu consentimento para os escopos Graph
/auth/status
Microsoft ainda.- Se o usuário não consentiu, ele usa uma janela pop-up para obter o consentimento do usuário por meio de um logon interativo.
- Se o usuário tiver consentido, ele carregará a interface do usuário principal.
- Quando o painel de tarefas é carregado pela primeira vez, ele chama para obter um token com escopo para a API web do
Obter consentimento do usuário
Mesmo que o add-in esteja usando o SSO, o usuário ainda precisa concordar com o complemento acessando seus dados por meio do Microsoft Graph. Obter consentimento é um processo único. Depois que o usuário tiver concedido o consentimento, o token SSO poderá ser trocado por um token Graph sem qualquer interação do usuário. Nesta seção, você implementará a experiência de consentimento no complemento usando msal-browser.
Crie um novo arquivo no diretório ./src/addin chamado consent.js adicionar o código a seguir.
'use strict'; const msalClient = new msal.PublicClientApplication({ auth: { clientId: authConfig.clientId } }); const msalRequest = { scopes: [ 'https://graph.microsoft.com/.default' ] }; // Function that handles the redirect back to this page // once the user has signed in and granted consent function handleResponse(response) { localStorage.removeItem('msalCallbackExpected'); if (response !== null) { localStorage.setItem('msalAccountId', response.account.homeId); Office.context.ui.messageParent(JSON.stringify({ status: 'success', result: response.accessToken })); } } Office.initialize = function () { if (Office.context.ui.messageParent) { // Let MSAL process a redirect response if that's what // caused this page to load. msalClient.handleRedirectPromise() .then(handleResponse) .catch((error) => { console.log(error); Office.context.ui.messageParent(JSON.stringify({ status: 'failure', result: error })); }); // If we're not expecting a callback (because this is // the first time the page has loaded), then start the // login process if (!localStorage.getItem('msalCallbackExpected')) { // Set the msalCallbackExpected property so we don't // make repeated token requests localStorage.setItem('msalCallbackExpected', 'yes'); // If the user has signed into this machine before // do a token request, otherwise do a login if (localStorage.getItem('msalAccountId')) { msalClient.acquireTokenRedirect(msalRequest); } else { msalClient.loginRedirect(msalRequest); } } } }
Esse código faz logon para o usuário, solicitando o conjunto de permissões do Microsoft Graph que estão configuradas no registro do aplicativo.
Crie um novo arquivo no diretório ./src/addin chamado consent.html e adicione o código a seguir.
<!DOCTYPE html> <html> <head> <script src="https://appsforoffice.microsoft.com/lib/beta/hosted/office.js"></script> <script src="https://alcdn.msauth.net/browser/2.6.1/js/msal-browser.min.js"></script> <script src="config.js"></script> <script src="consent.js"></script> </head> <body class="ms-Fabric"> <p>Authenticating...</p> </body> </html>
Este código implementa uma página HTML básica para carregar o arquivoconsent.js. Esta página será carregada em uma caixa de diálogo pop-up.
Salve todas as suas alterações e reinicie o aplicativo.
Carregue seu arquivo manifest.xml usando as mesmas etapas em Side-load the add-in in Excel.
Selecione o botão Importar Calendário na guia Início para abrir o painel de tarefas.
Selecione o botão Dar permissão no painel de tarefas para iniciar a caixa de diálogo de consentimento em uma janela pop-up. Entre e conceda consentimento.
O painel de tarefas é atualizado com um "Autenticado!" Mensagem. Você pode verificar os tokens da seguinte forma.
- Nas ferramentas de desenvolvedor do seu navegador, o token de API é mostrado no Console.
- Na CLI em que você está executando o servidor Node.js, o token Graph é impresso.
Você pode comparar esses tokens em https://jwt.ms . Observe que a audiência do token de API ( ) é definida como a ID do aplicativo do registro do aplicativo e o
aud
escopo ( ) éscp
access_as_user
.
Obter uma exibição de calendário
Neste exercício, você incorporará o Microsoft Graph no aplicativo. Para esse aplicativo, você usará a biblioteca microsoft-graph-client para fazer chamadas para o Microsoft Graph.
Obtenha eventos de calendário do Outlook
Comece adicionando uma API para obter uma exibição de calendário do calendário do usuário.
Abra ./src/api/graph.ts e adicione as instruções a seguir à
import
parte superior do arquivo.import { zonedTimeToUtc } from 'date-fns-tz'; import { findIana } from 'windows-iana'; import * as graph from '@microsoft/microsoft-graph-client'; import { Event, MailboxSettings } from 'microsoft-graph'; import 'isomorphic-fetch'; import { getTokenOnBehalfOf } from './auth';
Adicione a seguinte função para inicializar o Microsoft Graph SDK e retornar um Client.
async function getAuthenticatedClient(authHeader: string): Promise<graph.Client> { const accessToken = await getTokenOnBehalfOf(authHeader); return graph.Client.init({ authProvider: (done) => { // Call the callback with the // access token done(null, accessToken!); } }); }
Adicione a função a seguir para obter o fuso horário do usuário a partir de suas configurações de caixa de correio e para converter esse valor em um identificador de fuso horário IANA.
interface TimeZones { // The string returned by Microsoft Graph // Could be Windows name or IANA identifier. graph: string; // The IANA identifier iana: string; } async function getTimeZones(client: graph.Client): Promise<TimeZones> { // Get mailbox settings to determine user's // time zone const settings: MailboxSettings = await client .api('/me/mailboxsettings') .get(); // Time zone from Graph can be in IANA format or a // Windows time zone name. If Windows, convert to IANA const ianaTzs = findIana(settings.timeZone!) const ianaTz = ianaTzs ? ianaTzs[0] : null; const returnValue: TimeZones = { graph: settings.timeZone!, iana: ianaTz ?? settings.timeZone! }; return returnValue; }
Adicione a seguinte função (abaixo da
const graphRouter = Router();
linha) para implementar um ponto de extremidade da API (GET /graph/calendarview
).graphRouter.get('/calendarview', async function(req, res) { const authHeader = req.headers['authorization']; if (authHeader) { try { const client = await getAuthenticatedClient(authHeader); const viewStart = req.query['viewStart']?.toString(); const viewEnd = req.query['viewEnd']?.toString(); const timeZones = await getTimeZones(client); // Convert the start and end times into UTC from the user's time zone const utcViewStart = zonedTimeToUtc(viewStart!, timeZones.iana); const utcViewEnd = zonedTimeToUtc(viewEnd!, timeZones.iana); // GET events in the specified window of time const eventPage: graph.PageCollection = await client .api('/me/calendarview') // Header causes start and end times to be converted into // the requested time zone .header('Prefer', `outlook.timezone="${timeZones.graph}"`) // Specify the start and end of the calendar view .query({ startDateTime: utcViewStart.toISOString(), endDateTime: utcViewEnd.toISOString() }) // Only request the fields used by the app .select('subject,start,end,organizer') // Sort the results by the start time .orderby('start/dateTime') // Limit to at most 25 results in a single request .top(25) .get(); const events: any[] = []; // Set up a PageIterator to process the events in the result // and request subsequent "pages" if there are more than 25 // on the server const callback: graph.PageIteratorCallback = (event) => { // Add each event into the array events.push(event); return true; }; const iterator = new graph.PageIterator(client, eventPage, callback, { headers: { 'Prefer': `outlook.timezone="${timeZones.graph}"` } }); await iterator.iterate(); // Return the array of events res.status(200).json(events); } catch (error) { console.log(error); res.status(500).json(error); } } else { // No auth header res.status(401).end(); } } );
Considere o que esse código faz.
- Ele obtém o fuso horário do usuário e usa isso para converter o início e o fim do exibição de calendário solicitado em valores UTC.
- Ele faz um
GET
ao ponto de extremidade Graph/me/calendarview
API.- Ele usa a função para definir o header, fazendo com que os horários de início e término dos eventos retornados sejam ajustados ao fuso
header
Prefer: outlook.timezone
horário do usuário. - Ele usa a
query
função para adicionar osstartDateTime
endDateTime
parâmetros e, definindo o início e o final do exibição de calendário. - Ele usa a
select
função para solicitar apenas os campos usados pelo complemento. - Ele usa a
orderby
função para classificar os resultados pela hora de início. - Ele usa a
top
função para limitar os resultados em uma única solicitação a 25.
- Ele usa a função para definir o header, fazendo com que os horários de início e término dos eventos retornados sejam ajustados ao fuso
- Ele usa um objeto PageIteratorCallback para iterar pelos resultados e fazer solicitações adicionais se mais páginas de resultados estão disponíveis.
Atualizar a interface do usuário
Agora vamos atualizar o painel de tarefas para permitir que o usuário especifique uma data inicial e final para o exibição de calendário.
Abra ./src/addin/taskpane.js e substitua a função
showMainUi
existente pelo seguinte.function showMainUi() { $('.container').empty(); // Use luxon to calculate the start // and end of the current week. Use // those dates to set the initial values // of the date pickers const now = luxon.DateTime.local(); const startOfWeek = now.startOf('week'); const endOfWeek = now.endOf('week'); $('<h2/>', { class: 'ms-fontSize-24 ms-fontWeight-semibold', text: 'Select a date range to import' }).appendTo('.container'); // Create the import form $('<form/>').on('submit', getCalendar) .append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Start' })).append($('<input/>', { class: 'form-input', type: 'date', value: startOfWeek.toISODate(), id: 'viewStart' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'End' })).append($('<input/>', { class: 'form-input', type: 'date', value: endOfWeek.toISODate(), id: 'viewEnd' })).append($('<input/>', { class: 'primary-button', type: 'submit', id: 'importButton', value: 'Import' })).appendTo('.container'); $('<hr/>').appendTo('.container'); $('<h2/>', { class: 'ms-fontSize-24 ms-fontWeight-semibold', text: 'Add event to calendar' }).appendTo('.container'); // Create the new event form $('<form/>').on('submit', createEvent) .append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Subject' })).append($('<input/>', { class: 'form-input', type: 'text', required: true, id: 'eventSubject' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'Start' })).append($('<input/>', { class: 'form-input', type: 'datetime-local', required: true, id: 'eventStart' })).append($('<label/>', { class: 'ms-fontSize-16 ms-fontWeight-semibold', text: 'End' })).append($('<input/>', { class: 'form-input', type: 'datetime-local', required: true, id: 'eventEnd' })).append($('<input/>', { class: 'primary-button', type: 'submit', id: 'importButton', value: 'Create' })).appendTo('.container'); }
Este código adiciona um formulário simples para que o usuário possa especificar uma data de início e término. Ele também implementa um segundo formulário para criar um novo evento. Esse formulário não faz nada por enquanto, você implementará esse recurso na próxima seção.
Adicione o código a seguir ao arquivo para criar uma tabela na planilha ativa que contém os eventos recuperados do exibição de calendário.
const DAY_MILLISECONDS = 86400000; const DAY_MINUTES = 1440; const EXCEL_DATE_OFFSET = 25569; // Excel date cells require an OLE Automation date format // You can use the Moment-MSDate plug-in // (https://docs.microsoft.com/office/dev/add-ins/excel/excel-add-ins-ranges-advanced#work-with-dates-using-the-moment-msdate-plug-in) // Or you can do the conversion yourself function convertDateToOAFormat(dateTime) { const date = new Date(dateTime); // Get the time zone offset for the browser's time zone // since all of the dates here are handled in that time zone const tzOffset = date.getTimezoneOffset() / DAY_MINUTES; // Calculate the OLE Automation date, which is // the number of days since midnight, December 30, 1899 const oaDate = date.getTime() / DAY_MILLISECONDS + EXCEL_DATE_OFFSET - tzOffset; return oaDate; } async function writeEventsToSheet(events) { await Excel.run(async (context) => { const sheet = context.workbook.worksheets.getActiveWorksheet(); const eventsTable = sheet.tables.add('A1:D1', true); // Create the header row eventsTable.getHeaderRowRange().values = [[ 'Subject', 'Organizer', 'Start', 'End' ]]; // Create the data rows const data = []; events.forEach((event) => { data.push([ event.subject, event.organizer.emailAddress.name, convertDateToOAFormat(event.start.dateTime), convertDateToOAFormat(event.end.dateTime) ]); }); eventsTable.rows.add(null, data); const tableRange = eventsTable.getRange(); tableRange.numberFormat = [["[$-409]m/d/yy h:mm AM/PM;@"]]; tableRange.format.autofitColumns(); tableRange.format.autofitRows(); try { await context.sync(); } catch (err) { console.log(`Error: ${JSON.stringify(err)}`); showStatus(err, true); } }); }
Adicione a seguinte função para chamar a API de exibição de calendário.
async function getCalendar(evt) { evt.preventDefault(); toggleOverlay(true); try { const apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); const viewStart = $('#viewStart').val(); const viewEnd = $('#viewEnd').val(); const requestUrl = `${getBaseUrl()}/graph/calendarview?viewStart=${viewStart}&viewEnd=${viewEnd}`; const response = await fetch(requestUrl, { headers: { authorization: `Bearer ${apiToken}` } }); if (response.ok) { const events = await response.json(); writeEventsToSheet(events); showStatus(`Imported ${events.length} events`, false); } else { const error = await response.json(); showStatus(`Error getting events from calendar: ${JSON.stringify(error)}`, true); } toggleOverlay(false); } catch (err) { console.log(`Error: ${JSON.stringify(err)}`); showStatus(`Exception getting events from calendar: ${JSON.stringify(error)}`, true); } }
Salve todas as alterações, reinicie o servidor e atualize o painel de tarefas no Excel (feche todos os painéis de tarefas abertos e reaberto).
Escolha datas de início e término e escolha Importar.
Criar um novo evento
Nesta seção, você adicionará a capacidade de criar eventos no calendário do usuário.
Implementar a API
Abra ./src/api/graph.ts e adicione o código a seguir para implementar uma nova API de evento (
POST /graph/newevent
).graphRouter.post('/newevent', async function(req, res) { const authHeader = req.headers['authorization']; if (authHeader) { try { const client = await getAuthenticatedClient(authHeader); const timeZones = await getTimeZones(client); // Create a new Graph Event object const newEvent: Event = { subject: req.body['eventSubject'], start: { dateTime: req.body['eventStart'], timeZone: timeZones.graph }, end: { dateTime: req.body['eventEnd'], timeZone: timeZones.graph } }; // POST /me/events await client.api('/me/events') .post(newEvent); // Send a 201 Created res.status(201).end(); } catch (error) { console.log(error); res.status(500).json(error); } } else { // No auth header res.status(401).end(); } } );
Abra ./src/addin/taskpane.js e adicione a função a seguir para chamar a nova API de evento.
async function createEvent(evt) { evt.preventDefault(); toggleOverlay(true); const apiToken = await OfficeRuntime.auth.getAccessToken({ allowSignInPrompt: true }); const payload = { eventSubject: $('#eventSubject').val(), eventStart: $('#eventStart').val(), eventEnd: $('#eventEnd').val() }; const requestUrl = `${getBaseUrl()}/graph/newevent`; const response = await fetch(requestUrl, { method: 'POST', headers: { authorization: `Bearer ${apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { showStatus('Event created', false); } else { const error = await response.json(); showStatus(`Error creating event: ${JSON.stringify(error)}`, true); } toggleOverlay(false); }
Salve todas as alterações, reinicie o servidor e atualize o painel de tarefas no Excel (feche todos os painéis de tarefas abertos e reaberto).
Preencha o formulário e escolha Criar. Verifique se o evento foi adicionado ao calendário do usuário.
Parabéns!
Você concluiu o tutorial Office de Graph Do Microsoft. Agora que você tem um complemento de trabalho que chama a Microsoft Graph, você pode experimentar e adicionar novos recursos. Visite a visão geral do microsoft Graph para ver todos os dados que você pode acessar com o Microsoft Graph.
Comentários
Forneça qualquer comentário sobre este tutorial no repositório GitHub .
Tem algum problema com essa seção? Se tiver, envie seus comentários para que possamos melhorar esta seção.