Crear Office complementos con Microsoft Graph
Este tutorial le enseña a crear un complemento de Office para Excel que usa la API de Microsoft Graph para recuperar información de calendario para un usuario.
Sugerencia
Si prefiere descargar el tutorial completado, puede descargar o clonar el repositorio GitHub archivo.
Requisitos previos
Antes de iniciar esta demostración, debes tenerNode.js e Yarn instalados en el equipo de desarrollo. Si no tienes Node.js o Yarn, visita el vínculo anterior para ver las opciones de descarga.
Nota
Windows usuarios deben instalar Python y Visual Studio Build Tools para admitir módulos NPM que deben compilarse desde C/C++. El Node.js en Windows ofrece una opción para instalar automáticamente estas herramientas. Como alternativa, puede seguir las instrucciones en https://github.com/nodejs/node-gyp#on-windows.
También debe tener una cuenta personal de Microsoft con un buzón en Outlook.com, o una cuenta de Trabajo o escuela de Microsoft. Si no tienes una cuenta de Microsoft, hay un par de opciones para obtener una cuenta gratuita:
- Puedes suscribirte a una nueva cuenta personal de Microsoft.
- Puedes suscribirte al programa Microsoft 365 desarrolladores para obtener una suscripción Microsoft 365 gratuita.
Nota
Este tutorial se escribió con Node versión 14.15.0 e Yarn versión 1.22.0. Los pasos de esta guía pueden funcionar con otras versiones, pero eso no se ha probado.
Comentarios
Proporcione cualquier comentario sobre este tutorial en el repositorio GitHub usuario.
Crear un complemento para Office
En este ejercicio, creará una solución Office complemento con Express. La solución constará de dos partes.
- El complemento, implementado como archivos HTML estáticos y JavaScript.
- Un Node.js/Express que sirve el complemento e implementa una API web para recuperar datos del complemento.
Crear el servidor
Abra la interfaz de línea de comandos (CLI), vaya a un directorio donde desee crear el proyecto y ejecute el siguiente comando para generar un archivo package.json.
yarn init
Escriba valores para los mensajes según corresponda. Si no está seguro, los valores predeterminados están bien.
Ejecute los siguientes comandos para instalar dependencias.
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
Ejecute el siguiente comando para generar un archivo tsconfig.json.
tsc --init
Abra ./tsconfig.json en un editor de texto y realice los siguientes cambios.
- Cambie el valor
target
aes6
. - Descomprima el
outDir
valor y estadúdelo en./dist
. - Descomprima el
rootDir
valor y estadúdelo en./src
.
- Cambie el valor
Abra ./package.json y agregue la siguiente propiedad al JSON.
"scripts": { "start": "nodemon ./src/server.ts", "build": "tsc --project ./" },
Ejecute el siguiente comando para generar e instalar certificados de desarrollo para el complemento.
npx office-addin-dev-certs install
Si se le pide confirmación, confirme las acciones. Una vez completado el comando, verá un resultado similar al siguiente.
You now have trusted access to https://localhost. Certificate: <path>\localhost.crt Key: <path>\localhost.key
Cree un nuevo archivo denominado .env en la raíz del proyecto y agregue el siguiente código.
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'
Reemplace
PATH_TO_LOCALHOST.CRT
por la ruta de acceso a localhost.crt yPATH_TO_LOCALHOST.KEY
por la ruta de acceso a la salida localhost.key por el comando anterior.Cree un nuevo directorio en la raíz del proyecto denominado src.
Cree dos directorios en el directorio ./src : addin y api.
Cree un nuevo archivo denominado auth.ts en el directorio ./src/api y agregue el siguiente código.
import Router from 'express-promise-router'; const authRouter = Router(); // TODO: Implement this router export default authRouter;
Cree un nuevo archivo denominado graph.ts en el directorio ./src/api y agregue el siguiente código.
import Router from 'express-promise-router'; const graphRouter = Router(); // TODO: Implement this router export default graphRouter;
Cree un nuevo archivo denominado server.ts en el directorio ./src y agregue el siguiente código.
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}`); });
Crear el complemento
Cree un nuevo archivo denominado taskpane.html en el directorio ./src/addin y agregue el siguiente código.
<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>
Cree un nuevo archivo denominado taskpane.css en el directorio ./src/addin y agregue el siguiente código.
.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%; }
Cree un nuevo archivo denominado taskpane.js en el directorio ./src/addin y agregue el siguiente código.
// TEMPORARY CODE TO VERIFY ADD-IN LOADS 'use strict'; Office.onReady(info => { if (info.host === Office.HostType.Excel) { $(function() { $('p').text('Hello World!!'); }); } });
Cree un nuevo directorio en el directorio .src/addin denominado assets.
Agregue tres archivos PNG en este directorio de acuerdo con la tabla siguiente.
Nombre de archivo Tamaño en píxeles icon-80.png 80x80 icon-32.png 32x32 icon-16.png 16x16 Nota
Puede usar cualquier imagen que desee para este paso. También puedes descargar las imágenes usadas en este ejemplo directamente desde GitHub.
Cree un nuevo directorio en la raíz del proyecto denominado manifiesto.
Cree un nuevo archivo denominado manifest.xml en la carpeta ./manifest y agregue el siguiente código. Reemplace
NEW_GUID_HERE
por un NUEVO 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>
Cargue de forma lateral el complemento en Excel
Inicie el servidor ejecutando el siguiente comando.
yarn start
Abra el explorador y vaya a
https://localhost:3000/taskpane.html
. Debería ver unNot loaded
mensaje.En el explorador, ve a Office.com e inicia sesión. Seleccione Crear en la barra de herramientas de la izquierda y, a continuación, seleccione Hoja de cálculo.
Seleccione la pestaña Insertar y, a continuación , Office complementos.
Seleccione Upload Mi complemento y, a continuación, seleccione Examinar. Upload el archivo ./manifest/manifest.xml.
Seleccione el botón Importar calendario de la ficha Inicio para abrir el panel de tareas.
Después de que se abra el panel de tareas, debería ver un
Hello World!
mensaje.
Registrar la aplicación en el portal
En este ejercicio, creará un nuevo registro de aplicaciones web de Azure AD mediante el Centro Azure Active Directory administración.
Abra un explorador y vaya al centro de administración de Azure Active Directory. Inicie sesión con una cuenta personal (también conocida como: cuenta Microsoft) o una cuenta profesional o educativa.
Seleccione Azure Active Directory en el panel de navegación izquierdo y, a continuación, seleccione Registros de aplicaciones en Administrar.
Seleccione Nuevo registro. En la página Registrar una aplicación, establezca los valores siguientes.
- Establezca Nombre como
Office Add-in Graph Tutorial
. - Establezca Tipos de cuenta admitidos en Cuentas en cualquier directorio de organización y cuentas personales de Microsoft.
- En URI de redirección, establezca la primera lista desplegable en
Single-page application (SPA)
y establezca el valorhttps://localhost:3000/consent.html
.
- Establezca Nombre como
Seleccione Registrar. En la Office tutorial de Graph complemento, copie el valor del identificador de aplicación (cliente) y guárdelo, lo necesitará en el paso siguiente.
Seleccione Autenticación en Administrar. Busque la sección Concesión implícita y habilite los tokens de Access y los tokens de id.. Haga clic en Guardar.
Seleccione Certificados y secretos en Administrar. Seleccione el botón Nuevo secreto de cliente. Escriba un valor en Descripción, y seleccione una de las opciones para Expira, y después, Agregar.
Copie el valor del secreto de cliente antes de salir de esta página. Lo necesitará en el siguiente paso.
Importante
El secreto de cliente no se vuelve a mostrar, así que asegúrese de copiarlo en este momento.
Seleccione Permisos de API en Administrar y, a continuación, seleccione Agregar un permiso.
Seleccione Microsoft Graph y, a continuación, Permisos delegados.
Seleccione los siguientes permisos y, a continuación, seleccione Agregar permisos.
- offline_access: esto permitirá a la aplicación actualizar los tokens de acceso cuando expiren.
- Calendars.ReadWrite: esto permitirá que la aplicación lea y escriba en el calendario del usuario.
- MailboxSettings.Read: esto permitirá a la aplicación obtener la zona horaria del usuario desde la configuración de su buzón.
Configurar Office inicio de sesión único del complemento
En esta sección, actualizarás el registro de la aplicación para admitir Office inicio de sesión único (SSO) del complemento.
Seleccione Exponer una API. En la sección Ámbitos definidos por esta API, seleccione Agregar un ámbito. Cuando se le pida que establezca un URI de id. de aplicación, establezca el valor en
api://localhost:3000/YOUR_APP_ID_HERE
, reemplazando por el identificador deYOUR_APP_ID_HERE
aplicación. Elija Guardar y continuar.Rellene los campos de la siguiente manera y seleccione Agregar ámbito.
- Nombre del ámbito:
access_as_user
- Quién¿puede dar su consentimiento?: Administradores y usuarios
- Nombre para mostrar del consentimiento de administrador:
Access the app as the user
- Descripción del consentimiento de administrador:
Allows Office Add-ins to call the app's web APIs as the current user.
- Nombre para mostrar del consentimiento del usuario:
Access the app as you
- Descripción del consentimiento del usuario:
Allows Office Add-ins to call the app's web APIs as you.
- Estado: habilitado
- Nombre del ámbito:
En la sección Aplicaciones cliente autorizadas, seleccione Agregar una aplicación cliente. Escriba un identificador de cliente en la siguiente lista, habilite el ámbito en Ámbitos autorizados y seleccione Agregar aplicación. Repita este proceso para cada uno de los IDs de cliente de la lista.
d3590ed6-52b3-4102-aeff-aad2292ab01c
(Microsoft Office)ea5a67f6-b6f3-4338-b240-c655ddc3cc8e
(Microsoft Office)57fb890c-0dab-4253-a5e0-7188c88b2bb4
(Office en la Web)08e18876-6177-487e-b8b5-cf950c1e598c
(Office en la Web)
Agregar autenticación de Azure AD
En este ejercicio, habilitará Office inicio de sesión único (SSO) del complemento y extenderá la API web para admitir el flujo en nombre del usuario. Esto es necesario para obtener el token de acceso OAuth necesario para llamar a Microsoft Graph.
Información general
Office Sso de complemento proporciona un token de acceso, pero ese token solo permite al complemento llamar a su propia API web. No habilita el acceso directo a microsoft Graph. El proceso funciona de la siguiente manera.
- El complemento obtiene un token llamando a getAccessToken. La audiencia de este token (la notificación) es el identificador de aplicación del registro de la aplicación
aud
del complemento. - El complemento envía este token en el
Authorization
encabezado cuando realiza una llamada a la API web. - La API web valida el token y, a continuación, usa el flujo en nombre de para intercambiar este token por un token Graph Microsoft. La audiencia de este nuevo token es
https://graph.microsoft.com
. - La API web usa el nuevo token para realizar llamadas a microsoft Graph y devuelve los resultados al complemento.
Configurar la solución
Abra ./.env y actualice el y con el identificador de aplicación y el secreto de
AZURE_APP_ID
cliente desde el registro de laAZURE_CLIENT_SECRET
aplicación.Importante
Si usas el control de código fuente como git, ahora sería un buen momento para excluir el archivo .env del control de código fuente para evitar la pérdida involuntaria del identificador de la aplicación y el secreto de cliente.
Abra ./manifest/manifest.xml y reemplace todas las instancias de
YOUR_APP_ID_HERE
con el identificador de aplicación desde el registro de la aplicación.Cree un nuevo archivo en el directorio ./src/addin denominado config.js y agregue el siguiente código, reemplazando con el identificador de aplicación desde el registro
YOUR_APP_ID_HERE
de la aplicación.authConfig = { clientId: 'YOUR_APP_ID_HERE' };
Implementar el inicio de sesión
Abra ./src/api/auth.ts y agregue las siguientes
import
instrucciones en la parte superior del archivo.import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as msal from '@azure/msal-node';
Agregue el siguiente código después de
import
las instrucciones.// 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 un cliente confidencial de MSALy exporta una función para obtener un token Graph del token enviado por el complemento.
Agregue el siguiente código antes de la
export default authRouter;
línea.// 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(); } } );
Este código implementa una API ( ) que comprueba si el token de complemento se puede intercambiar silenciosamente por un
GET /auth/status
token Graph. El complemento usará esta API para determinar si necesita presentar un inicio de sesión interactivo al usuario.Abra ./src/addin/taskpane.js y agregue el siguiente código al archivo.
// 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 agrega funciones para actualizar la interfaz de usuario y para usar la API de diálogo Office para iniciar un flujo de autenticación interactivo.
Agregue la siguiente función para implementar una interfaz de usuario principal temporal.
function showMainUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Authenticated!' }).appendTo('.container'); }
Reemplace la llamada
Office.onReady
existente por la siguiente.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(); } } }); } });
Tenga en cuenta lo que hace este código.
- Cuando el panel de tareas se carga por primera vez, llama para obtener un
getAccessToken
ámbito de token para la API web del complemento. - Usa ese token para llamar a la API para comprobar si el usuario ha dado su consentimiento a los ámbitos Graph
/auth/status
Microsoft.- Si el usuario no ha dado su consentimiento, usa una ventana emergente para obtener el consentimiento del usuario a través de un inicio de sesión interactivo.
- Si el usuario ha dado su consentimiento, carga la interfaz de usuario principal.
- Cuando el panel de tareas se carga por primera vez, llama para obtener un
Obtener el consentimiento del usuario
Aunque el complemento usa SSO, el usuario todavía tiene que dar su consentimiento para que el complemento tenga acceso a sus datos a través de Microsoft Graph. Obtener el consentimiento es un proceso único. Una vez que el usuario ha concedido el consentimiento, el token de SSO se puede intercambiar por un token Graph sin ninguna interacción del usuario. En esta sección, implementará la experiencia de consentimiento en el complemento con msal-browser.
Cree un nuevo archivo en el directorio ./src/addin denominado consent.js y agregue el siguiente código.
'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); } } } }
Este código inicia sesión para el usuario, solicitando el conjunto de permisos de Microsoft Graph que están configurados en el registro de la aplicación.
Cree un nuevo archivo en el directorio ./src/addin denominado consent.html y agregue el siguiente código.
<!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 una página HTML básica para cargar el consent.js archivo. Esta página se cargará en un cuadro de diálogo emergente.
Guarde todos los cambios y reinicie el servidor.
Vuelva a cargar el archivomanifest.xml con los mismos pasos en Side-load the add-in in Excel.
Seleccione el botón Importar calendario en la ficha Inicio para abrir el panel de tareas.
Seleccione el botón Conceder permiso en el panel de tareas para iniciar el cuadro de diálogo de consentimiento en una ventana emergente. Inicie sesión y conceda el consentimiento.
El panel de tareas se actualiza con un "¡Autenticado!" Mensaje. Puedes comprobar los tokens de la siguiente manera.
- En las herramientas para desarrolladores del brower, el token de API se muestra en la consola.
- En la CLI donde se ejecuta el servidor Node.js, se imprime Graph token.
Puede comparar estos tokens en https://jwt.ms . Observe que la audiencia del token de API ( ) se establece en el identificador de aplicación del registro de la aplicación y el
aud
ámbito ( ) esscp
access_as_user
.
Obtener una vista de calendario
En este ejercicio, incorporará Microsoft Graph en la aplicación. Para esta aplicación, usará la biblioteca de microsoft-graph-client para realizar llamadas a Microsoft Graph.
Obtener eventos del calendario desde Outlook
Empiece agregando una API para obtener una vista de calendario del calendario del usuario.
Abra ./src/api/graph.ts y agregue las siguientes
import
instrucciones a la parte superior del archivo.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';
Agregue la siguiente función para inicializar el SDK de Microsoft Graph y devolver un objeto 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!); } }); }
Agregue la siguiente función para obtener la zona horaria del usuario de su configuración de buzón de correo y para convertir ese valor en un identificador de zona horaria 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; }
Agregue la siguiente función (debajo de la
const graphRouter = Router();
línea) para implementar un extremo de 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(); } } );
Tenga en cuenta lo que hace este código.
- Obtiene la zona horaria del usuario y la usa para convertir el inicio y el final de la vista de calendario solicitada en valores UTC.
- Realiza una aplicación
GET
al punto de conexión Graph/me/calendarview
API.- Usa la función para establecer el encabezado, lo que hace que las horas de inicio y finalización de los eventos devueltos se ajusten a la zona
header
Prefer: outlook.timezone
horaria del usuario. - Usa la función
query
para agregar los parámetrosstartDateTime
endDateTime
and, estableciendo el inicio y el final de la vista de calendario. - Usa la
select
función para solicitar solo los campos usados por el complemento. - Usa la función
orderby
para ordenar los resultados por hora de inicio. - Usa la
top
función para limitar los resultados de una sola solicitud a 25.
- Usa la función para establecer el encabezado, lo que hace que las horas de inicio y finalización de los eventos devueltos se ajusten a la zona
- Usa un objeto PageIteratorCallback para iterar los resultados y realizar solicitudes adicionales si hay más páginas de resultados disponibles.
Actualizar la interfaz de usuario
Ahora vamos a actualizar el panel de tareas para permitir al usuario especificar una fecha de inicio y finalización para la vista de calendario.
Abra ./src/addin/taskpane.js y reemplace la función
showMainUi
existente por la siguiente.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 agrega un formulario simple para que el usuario pueda especificar una fecha de inicio y finalización. También implementa un segundo formulario para crear un nuevo evento. Ese formulario no hace nada por ahora, implementará esa característica en la siguiente sección.
Agregue el siguiente código al archivo para crear una tabla en la hoja de cálculo activa que contenga los eventos recuperados de la vista de calendario.
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); } }); }
Agregue la siguiente función para llamar a la API de vista de calendario.
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); } }
Guarde todos los cambios, reinicie el servidor y actualice el panel de tareas en Excel (cierre los paneles de tareas abiertos y vuelva a abrir).
Elija fechas de inicio y finalización y elija Importar.
Crear un nuevo evento
En esta sección, agregará la capacidad de crear eventos en el calendario del usuario.
Implementar la API
Abra ./src/api/graph.ts y agregue el siguiente código para implementar una nueva API de eventos (
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 y agregue la siguiente función para llamar a la nueva API de eventos.
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); }
Guarde todos los cambios, reinicie el servidor y actualice el panel de tareas en Excel (cierre los paneles de tareas abiertos y vuelva a abrir).
Rellene el formulario y elija Crear. Compruebe que el evento se agrega al calendario del usuario.
¡Enhorabuena!
Ha completado el tutorial Office complemento de Microsoft Graph tutorial. Ahora que tiene un complemento de trabajo que llama a Microsoft Graph, puede experimentar y agregar nuevas características. Visite la información general de Microsoft Graph para ver todos los datos a los que puede acceder con Microsoft Graph.
Comentarios
Proporcione cualquier comentario sobre este tutorial en el repositorio GitHub archivo.
¿Tiene algún problema con esta sección? Si es así, envíenos sus comentarios para que podamos mejorarla.