Créer des Office avec Microsoft Graph
Ce didacticiel vous apprend à créer un Office pour Excel qui utilise l’API Microsoft Graph pour récupérer les informations de calendrier d’un utilisateur.
Conseil
Si vous préférez simplement télécharger le didacticiel terminé, vous pouvez télécharger ou cloner le GitHub complet.
Conditions préalables
Avant de commencer cette démonstration, vous devez avoir installéNode.js et Sons sur votre ordinateur de développement. Si vous n’avez pas de Node.js ou Der, consultez le lien précédent pour obtenir les options de téléchargement.
Notes
Windows utilisateurs devront peut-être installer Python et Visual Studio Build Tools pour prendre en charge les modules NPM qui doivent être compilés à partir de C/C++. Le programme Node.js installer sur Windows offre la possibilité d’installer automatiquement ces outils. Vous pouvez également suivre les instructions sur https://github.com/nodejs/node-gyp#on-windows.
Vous devez également avoir un compte Microsoft personnel avec une boîte aux lettres sur Outlook.com, ou un compte scolaire ou scolaire Microsoft. Si vous n’avez pas de compte Microsoft, deux options s’offrent à vous pour obtenir un compte gratuit :
- Vous pouvez vous inscrire à un nouveau compte Microsoft personnel.
- Vous pouvez vous inscrire au programme Microsoft 365 développeur pour obtenir un abonnement Microsoft 365 gratuit.
Notes
Ce didacticiel a été écrit avec Node version 14.15.0 et Fin version 1.22.0. Les étapes de ce guide peuvent fonctionner avec d’autres versions, mais elles n’ont pas été testées.
Commentaires
N’hésitez pas à nous faire part de vos commentaires sur ce didacticiel dans GitHub référentiel.
Créer un complément Office
Dans cet exercice, vous allez créer une solution de Office à l’aide d’Express. La solution se compose de deux parties.
- Le add-in, implémenté en tant que fichiers HTML et JavaScript statiques.
- Un Node.js/Express qui sert le add-in et implémente une API web pour récupérer des données pour le add-in.
Créer le serveur
Ouvrez votre interface de ligne de commande (CLI), accédez à un répertoire dans lequel vous souhaitez créer votre projet et exécutez la commande suivante pour générer un fichier package.json.
yarn init
Entrez des valeurs pour les invites selon le cas. Si vous n’êtes pas sûr, les valeurs par défaut sont corrects.
Exécutez les commandes suivantes pour installer les dépendances.
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
Exécutez la commande suivante pour générer un fichier tsconfig.json.
tsc --init
Ouvrez ./tsconfig.json dans un éditeur de texte et a apporté les modifications suivantes.
- Changez la
target
valeur enes6
. - Désécommant la
outDir
valeur et définissez-la sur./dist
. - Désécommant la
rootDir
valeur et définissez-la sur./src
.
- Changez la
Ouvrez ./package.json et ajoutez la propriété suivante au JSON.
"scripts": { "start": "nodemon ./src/server.ts", "build": "tsc --project ./" },
Exécutez la commande suivante pour générer et installer des certificats de développement pour votre add-in.
npx office-addin-dev-certs install
Si vous êtes invité à confirmer, confirmez les actions. Une fois la commande terminée, vous verrez une sortie semblable à celle-ci.
You now have trusted access to https://localhost. Certificate: <path>\localhost.crt Key: <path>\localhost.key
Créez un fichier nommé .env à la racine de votre projet et ajoutez le code suivant.
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'
Remplacez
PATH_TO_LOCALHOST.CRT
par le chemin d’accès à localhost.crtPATH_TO_LOCALHOST.KEY
et par le chemin d’accès à la sortie localhost.key par la commande précédente.Créez un répertoire à la racine de votre projet nommé src.
Créez deux répertoires dans le répertoire ./src : le addin et l’api.
Créez un fichier nommé auth.ts dans le répertoire ./src/api et ajoutez le code suivant.
import Router from 'express-promise-router'; const authRouter = Router(); // TODO: Implement this router export default authRouter;
Créez un fichier nommé graph.ts dans le répertoire ./src/api et ajoutez le code suivant.
import Router from 'express-promise-router'; const graphRouter = Router(); // TODO: Implement this router export default graphRouter;
Créez un fichier nommé server.ts dans le répertoire ./src et ajoutez le code suivant.
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}`); });
Créer le complément
Créez un fichier nommé taskpane.html dans le répertoire ./src/addin et ajoutez le code suivant.
<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>
Créez un fichier nommé taskpane.css dans le répertoire ./src/addin et ajoutez le code suivant.
.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%; }
Créez un fichier nommé taskpane.js dans le répertoire ./src/addin et ajoutez le code suivant.
// TEMPORARY CODE TO VERIFY ADD-IN LOADS 'use strict'; Office.onReady(info => { if (info.host === Office.HostType.Excel) { $(function() { $('p').text('Hello World!!'); }); } });
Créez un répertoire dans les ressources nommées du répertoire .src/addin.
Ajoutez trois fichiers PNG dans ce répertoire selon le tableau suivant.
Nom de fichier Taille en pixels icon-80.png 80x80 icon-32.png 32x32 icon-16.png 16x16 Notes
Vous pouvez utiliser n’importe quelle image que vous souhaitez pour cette étape. Vous pouvez également télécharger les images utilisées dans cet exemple directement à partir GitHub.
Créez un répertoire à la racine du manifeste nommé du projet.
Créez un fichier nommé manifest.xml dans le dossier ./manifest et ajoutez le code suivant. Remplacez
NEW_GUID_HERE
par un nouveau GUID, commeb4fa03b8-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>
Charger le Excel
Démarrez le serveur en exécutant la commande suivante.
yarn start
Ouvrez votre navigateur et accédez à
https://localhost:3000/taskpane.html
. Vous devriez voir unNot loaded
message.Dans votre navigateur, Office.com et connectez-vous. Sélectionnez Créer dans la barre d’outils de gauche, puis feuille de calcul.
Sélectionnez l’onglet Insérer, puis sélectionnez Office des modules.
Sélectionnez Télécharger Mon add-in, puis parcourir. Télécharger fichier ./manifest/manifest.xml.
Sélectionnez le bouton Importer le calendrier sous l’onglet Accueil pour ouvrir lepane des tâches.
Une fois lepane des tâches ouvert, vous devez voir un
Hello World!
message.
Inscrire l’application sur le portail
Dans cet exercice, vous allez créer une inscription d’application web Azure AD à l’aide Azure Active Directory’administration.
Ouvrez un navigateur et accédez au Centre d’administration Azure Active Directory. Connectez-vous à l’aide d’un compte personnel (compte Microsoft) ou d’un compte professionnel ou scolaire.
Sélectionnez Azure Active Directory dans le volet de navigation gauche, puis sélectionnez Inscriptions d’applications sous Gérer.
Sélectionnez Nouvelle inscription. Sur la page Inscrire une application, définissez les valeurs comme suit.
- Définissez le Nom sur
Office Add-in Graph Tutorial
. - Définissez les Types de comptes pris en charge sur Comptes dans un annuaire organisationnel et comptes personnels Microsoft.
- Sous URI de redirection, définissez la première flèche déroulante sur
Single-page application (SPA)
, et la valeur surhttps://localhost:3000/consent.html
.
- Définissez le Nom sur
Sélectionner Inscription. Sur la page Office de didacticiel Graph, copiez la valeur de l’ID d’application (client) et enregistrez-la. Vous en aurez besoin à l’étape suivante.
Sous Gérer, sélectionnez Authentification. Recherchez la section d’octroi implicite et activez les jetons d’accès et les jetons d’ID. Cliquez sur Enregistrer.
Sélectionnez Certificats et secrets sous Gérer. Sélectionnez le bouton Nouveau secret client. Entrez une valeur dans Description, sélectionnez une des options pour Expire le, puis sélectionnez Ajouter.
Copiez la valeur du secret client avant de quitter cette page. Vous en aurez besoin à l’étape suivante.
Important
Ce secret client n’apparaîtra plus jamais, aussi veillez à le copier maintenant.
Sélectionnez les autorisations d’API sous Gérer, puis sélectionnez Ajouter une autorisation.
Sélectionnez Microsoft Graph, puis Autorisations déléguées.
Sélectionnez les autorisations suivantes, puis sélectionnez Ajouter des autorisations.
- offline_access - cela permet à l’application d’actualiser les jetons d’accès lorsqu’ils expirent.
- Calendars.ReadWrite : cela permettra à l’application de lire et d’écrire dans le calendrier de l’utilisateur.
- MailboxSettings.Read : cela permettra à l’application d’obtenir le fuseau horaire de l’utilisateur à partir de ses paramètres de boîte aux lettres.
Configurer Office l' sign-on unique du add-in
Dans cette section, vous allez mettre à jour l’inscription de l’application pour prendre en charge Office' sign-in unique (SSO).
Sélectionnez Exposer une API. Dans les étendues définies par cette section API, sélectionnez Ajouter une étendue. Lorsque vous y sont invités pour définir un URI d’ID d’application, définissez la valeur sur , en remplaçant
api://localhost:3000/YOUR_APP_ID_HERE
parYOUR_APP_ID_HERE
l’ID d’application. Choose Save and continue.Remplissez les champs comme suit et sélectionnez Ajouter une étendue.
- Nom de l’étendue :
access_as_user
- Qui pouvez-vous donner votre consentement ? : Administrateurs et utilisateurs
- Nom complet du consentement de l’administrateur :
Access the app as the user
- Description du consentement de l’administrateur :
Allows Office Add-ins to call the app's web APIs as the current user.
- Nom complet du consentement de l’utilisateur :
Access the app as you
- Description du consentement de l’utilisateur :
Allows Office Add-ins to call the app's web APIs as you.
- État : activé
- Nom de l’étendue :
Dans la section Applications clientes autorisées, sélectionnez Ajouter une application cliente. Entrez un ID client dans la liste suivante, activez l’étendue sous Étendues autorisées, puis sélectionnez Ajouter une application. Répétez ce processus pour chacun des ID clients de la liste.
d3590ed6-52b3-4102-aeff-aad2292ab01c
(Microsoft Office)ea5a67f6-b6f3-4338-b240-c655ddc3cc8e
(Microsoft Office)57fb890c-0dab-4253-a5e0-7188c88b2bb4
(Office sur le web)08e18876-6177-487e-b8b5-cf950c1e598c
(Office sur le web)
Ajouter une authentification Azure AD
Dans cet exercice, vous allez activer Office SSO (Single Sign-on) dans le module et étendre l’API web pour prendre en charge le flux « de la part de». Cette étape est nécessaire pour obtenir le jeton d’accès OAuth nécessaire pour appeler l’Graph Microsoft.
Vue d’ensemble
Office L' ssO du add-in fournit un jeton d’accès, mais ce jeton permet uniquement au add-in d’appeler sa propre API web. Il n’autorise pas l’accès direct au microsoft Graph. Le processus fonctionne comme suit.
- Le add-in obtient un jeton en appelant getAccessToken. L’audience de ce jeton (la revendication) est
aud
l’ID d’application de l’inscription de l’application du module. - Le add-in envoie ce jeton dans l’en-tête lorsqu’il effectue un
Authorization
appel à l’API web. - L’API web valide le jeton, puis utilise le flux « de la part de » pour échanger ce jeton contre un jeton Microsoft Graph. L’audience de ce nouveau jeton est
https://graph.microsoft.com
. - L’API web utilise le nouveau jeton pour appeler le Graph Microsoft et renvoie les résultats au module.
Configurer la solution
Ouvrez ./.env et mettez à jour l’ID d’application et la
AZURE_APP_ID
secret client à partir deAZURE_CLIENT_SECRET
l’inscription de votre application.Important
Si vous utilisez un contrôle source tel que Git, il est temps d’exclure le fichier .env du contrôle source afin d’éviter toute fuite accidentelle de votre ID d’application et de votre secret client.
Ouvrez ./manifest/manifest.xml et remplacez toutes les instances par l’ID de l’application à partir de
YOUR_APP_ID_HERE
l’inscription de votre application.Créez un fichier dans le répertoire ./src/addin nommé config.js et ajoutez le code suivant, en remplaçant par l’ID de l’application à partir de l’inscription de
YOUR_APP_ID_HERE
votre application.authConfig = { clientId: 'YOUR_APP_ID_HERE' };
Implémentation de la connexion
Ouvrez ./src/api/auth.ts et ajoutez les
import
instructions suivantes en haut du fichier.import jwt, { SigningKeyCallback, JwtHeader } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as msal from '@azure/msal-node';
Ajoutez le code suivant après
import
les instructions.// 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; } }
Ce code initialise un client confidentiel MSALet exporte une fonction pour obtenir un jeton Graph à partir du jeton envoyé par le module.
Ajoutez le code suivant avant la
export default authRouter;
ligne.// 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(); } } );
Ce code implémente une API ( ) qui vérifie si le jeton de la application peut être échangé silencieusement contre
GET /auth/status
un jeton Graph. Le add-in utilisera cette API pour déterminer s’il doit présenter une connexion interactive à l’utilisateur.Ouvrez ./src/addin/taskpane.js et ajoutez le code suivant au fichier.
// 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'); }
Ce code ajoute des fonctions pour mettre à jour l’interface utilisateur et utiliser l’API Office Dialog pour initier un flux d’authentification interactif.
Ajoutez la fonction suivante pour implémenter une interface utilisateur principale temporaire.
function showMainUi() { $('.container').empty(); $('<p/>', { class: 'ms-fontSize-24 ms-fontWeight-bold', text: 'Authenticated!' }).appendTo('.container'); }
Remplacez
Office.onReady
l’appel existant par ce qui suit.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(); } } }); } });
Considérez ce que fait ce code.
- Lorsque le volet Des tâches se charge pour la première fois, il appelle pour obtenir un jeton
getAccessToken
d’étendue pour l’API web du add-in. - Il utilise ce jeton pour appeler l’API afin de vérifier si l’utilisateur a donné son consentement aux étendues Graph
/auth/status
Microsoft.- Si l’utilisateur n’a pas donné son consentement, il utilise une fenêtre pop-up pour obtenir son consentement par le biais d’une connexion interactive.
- Si l’utilisateur a donné son consentement, il charge l’interface utilisateur principale.
- Lorsque le volet Des tâches se charge pour la première fois, il appelle pour obtenir un jeton
Obtention du consentement de l’utilisateur
Même si le add-in utilise l' luiso, l’utilisateur doit toujours consentir à ce qu’il accède à ses données via Microsoft Graph. L’obtention du consentement est un processus à durée de vie. Une fois que l’utilisateur a donné son consentement, le jeton DSO peut être échangé contre un jeton Graph sans aucune interaction de l’utilisateur. Dans cette section, vous allez implémenter l’expérience de consentement dans le add-in à l’aide de msal-browser.
Créez un fichier dans le répertoire ./src/addin nommé consent.js et ajoutez le code suivant.
'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); } } } }
Ce code se connecte pour l’utilisateur, demandant l’ensemble des autorisations microsoft Graph configurées lors de l’inscription de l’application.
Créez un fichier dans le répertoire ./src/addin nommé consent.html et ajoutez le code suivant.
<!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>
Ce code implémente une page HTML de base pour charger consent.js fichier. Cette page sera chargée dans une boîte de dialogue de fenêtre.
Enregistrez toutes vos modifications et redémarrez le serveur.
Re-chargez votremanifest.xml à l’aide des mêmes étapes de chargement de version latérale du Excel .
Sélectionnez le bouton Importer le calendrier sous l’onglet Accueil pour ouvrir le volet Des tâches.
Sélectionnez le bouton Accorder l’autorisation dans le volet Des tâches pour lancer la boîte de dialogue de consentement dans une fenêtre pop-up. Connectez-vous et accordez le consentement.
Le volet Des tâches est mis à jour avec un « Authentifié ! » Message. Vous pouvez vérifier les jetons comme suit.
- Dans les outils de développement de votre navigateur, le jeton d’API s’affiche dans la console.
- Dans votre CLI où vous exécutez le serveur Node.js, le jeton Graph est imprimé.
Vous pouvez comparer ces jetons à https://jwt.ms . Notez que l’audience du jeton d’API ( ) est définie sur l’ID d’application de l’inscription de votre application, et que
aud
l’étendue ( ) estscp
access_as_user
.
Obtenir un affichage Calendrier
Dans cet exercice, vous allez incorporer Microsoft Graph dans l’application. Pour cette application, vous allez utiliser la bibliothèque cliente microsoft-graph pour appeler Microsoft Graph.
Récupérer les événements de calendrier à partir d’Outlook
Commencez par ajouter une API pour obtenir un affichage Calendrier à partir du calendrier de l’utilisateur.
Ouvrez ./src/api/graph.ts et ajoutez les
import
instructions suivantes en haut du fichier.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';
Ajoutez la fonction suivante pour initialiser le SDK Microsoft Graph et renvoyer un 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!); } }); }
Ajoutez la fonction suivante pour obtenir le fuseau horaire de l’utilisateur à partir de ses paramètres de boîte aux lettres et pour convertir cette valeur en identificateur de fuseau horaire 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; }
Ajoutez la fonction suivante (sous la
const graphRouter = Router();
ligne) pour implémenter un point de terminaison d’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(); } } );
Considérez ce que fait ce code.
- Il obtient le fuseau horaire de l’utilisateur et l’utilise pour convertir le début et la fin de l’affichage Calendrier demandé en valeurs UTC.
- Il s’agit
GET
du point/me/calendarview
de terminaison Graph API.- Elle utilise la fonction pour définir l’en-tête, ce qui a pour effet d’ajuster les heures de début et de fin des événements renvoyés au fuseau horaire de
header
Prefer: outlook.timezone
l’utilisateur. - Il utilise la fonction pour ajouter les paramètres et définir le début et la
query
startDateTime
fin deendDateTime
l’affichage Calendrier. - Il utilise la fonction pour demander uniquement les champs utilisés par
select
le add-in. - Elle utilise la
orderby
fonction pour trier les résultats par heure de début. - Il utilise la
top
fonction pour limiter à 25 les résultats d’une seule requête.
- Elle utilise la fonction pour définir l’en-tête, ce qui a pour effet d’ajuster les heures de début et de fin des événements renvoyés au fuseau horaire de
- Il utilise un objet PageIteratorCallback pour itérer dans les résultats et effectuer des demandes supplémentaires si d’autres pages de résultats sont disponibles.
Mettre à jour l’interface utilisateur
Maintenant, nous allons mettre à jour le volet Des tâches pour permettre à l’utilisateur de spécifier une date de début et de fin pour l’affichage Calendrier.
Ouvrez ./src/addin/taskpane.js et remplacez la fonction
showMainUi
existante par ce qui suit.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'); }
Ce code ajoute un formulaire simple afin que l’utilisateur puisse spécifier une date de début et de fin. Il implémente également un second formulaire pour la création d’un événement. Ce formulaire n’a rien à faire pour le moment, vous allez implémenter cette fonctionnalité dans la section suivante.
Ajoutez le code suivant au fichier pour créer une table dans la feuille de calcul active contenant les événements extraits de l’affichage Calendrier.
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); } }); }
Ajoutez la fonction suivante pour appeler l’API d’affichage calendrier.
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); } }
Enregistrez toutes vos modifications, redémarrez le serveur et actualisez le volet Des tâches dans Excel (fermez les volets Des tâches ouverts et ré-ouvrez).
Choisissez les dates de début et de fin, puis importer.
Créer un événement
Dans cette section, vous allez ajouter la possibilité de créer des événements sur le calendrier de l’utilisateur.
Implémenter l’API
Ouvrez ./src/api/graph.ts et ajoutez le code suivant pour implémenter une nouvelle API d’événement (
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(); } } );
Ouvrez ./src/addin/taskpane.js et ajoutez la fonction suivante pour appeler la nouvelle API d’événement.
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); }
Enregistrez toutes vos modifications, redémarrez le serveur et actualisez le volet Des tâches dans Excel (fermez les volets Des tâches ouverts et ré-ouvrez).
Remplissez le formulaire et choisissez Créer. Vérifiez que l’événement est ajouté au calendrier de l’utilisateur.
Félicitations !
Vous avez terminé le didacticiel Office’aide du Graph Microsoft. Maintenant que vous disposez d’un module de travail qui appelle Microsoft Graph, vous pouvez expérimenter et ajouter de nouvelles fonctionnalités. Consultez la vue d’ensemble de Microsoft Graph pour voir toutes les données accessibles avec Microsoft Graph.
Commentaires
N’hésitez pas à nous faire part de vos commentaires sur ce didacticiel dans GitHub référentiel.
Vous avez un problème avec cette section ? Si c'est le cas, faites-nous part de vos commentaires pour que nous puissions l'améliorer.