Создание приложений Node.js Express с помощью Microsoft Graph
В этом руководстве рассказывается о создании веб-приложения Node.js Express, которое использует API microsoft Graph для получения сведений о календаре для пользователя.
Совет
Если вы предпочитаете просто скачать завершенный учебник, вы можете скачать его двумя способами.
- Скачайте Node.js, чтобы получить рабочий код за несколько минут.
- Скачайте или клонировать GitHub репозиторий.
Необходимые компоненты
Перед запуском этой демонстрации необходимо установить Node.jsна компьютере разработки. Если у вас нет Node.js, посетите предыдущую ссылку для параметров загрузки.
Примечание
Windows пользователям может потребоваться установить Python и Visual Studio Build Tools для поддержки модулей NPM, которые должны быть компилироваться из C/C++. Установщик Node.js на Windows предоставляет возможность автоматической установки этих средств. Кроме того, вы можете следовать инструкциям по https://github.com/nodejs/node-gyp#on-windows.
Вы также должны иметь личную учетную запись Майкрософт с почтовым ящиком на Outlook.com или учетную запись Microsoft work или school. Если у вас нет учетной записи Майкрософт, существует несколько вариантов получения бесплатной учетной записи:
- Вы можете зарегистрироваться на новую личную учетную запись Майкрософт.
- Вы можете зарегистрироваться в программе Microsoft 365 разработчика, чтобы получить бесплатную Microsoft 365 подписку.
Примечание
Этот учебник был написан с версией Node 14.15.0. Действия в этом руководстве могут работать с другими версиями, но они не были проверены.
Отзывы
Обратите внимание на этот учебник в репозитории GitHub.
Создание веб-приложения Node.js Express
В этом упражнении вы будете использовать Express для создания веб-приложения.
Откройте CLI, перейдите в каталог, в котором у вас есть права на создание файлов, и запустите следующую команду, чтобы создать новое приложение Express, которое использует handlebars в качестве двигателя отрисовки.
npx express-generator --hbs graph-tutorial
Генератор Express создает новый каталог под названием
graph-tutorial
и создает приложение Express.Перейдите в
graph-tutorial
каталог и введите следующую команду для установки зависимостей.npm install
Запустите следующую команду, чтобы обновить пакеты Node с сообщаемой уязвимостью.
npm audit fix
Запустите следующую команду для обновления версии Express и других зависимостей.
npm install express@4.17.1 http-errors@1.8.0 morgan@1.10.0 debug@4.3.1 hbs@4.1.2
Чтобы запустить локальный веб-сервер, используйте следующую команду.
npm start
Откройте браузер и перейдите по адресу
http://localhost:3000
. Если все работает, вы увидите сообщение "Добро пожаловать в экспресс". Если вы не видите это сообщение, проверьте руководство по началу работы Express.
Установка пакетов node
Прежде чем двигаться дальше, установите дополнительные пакеты, которые вы будете использовать позже:
- dotenv для загрузки значений из файла .env.
- date-fns для форматирования значений даты и времени.
- windows-iana для перевода имен Windows часового пояса в IANA.
- подключение-вспышка для сообщений об ошибках вспышки в приложении.
- экспресс-сеанс для хранения значений в сеансе сервера в памяти.
- маршрутизатор express-promise-router , позволяющий обработчику маршрутов возвращать обещание.
- экспресс-валидатор для анализа и проверки данных форм.
- msal-node для проверки подлинности и получения маркеров доступа.
- microsoft-graph-client для звонков в Microsoft Graph.
- isomorphic-fetch для полифайла для получения узла. Для библиотеки требуется извлечение
microsoft-graph-client
полифайла. Дополнительные сведения см. Graph вики клиентской библиотеки Microsoft Graph JavaScript. - qs для создания строк url-запросов.
Запустите следующую команду в CLI.
npm install dotenv@10.0.0 date-fns@2.23.0 date-fns-tz@1.1.6 connect-flash@0.1.1 express-validator@6.12.1 npm install express-session@1.17.2 express-promise-router@4.1.0 isomorphic-fetch@3.0.0 npm install @azure/msal-node@1.3.0 @microsoft/microsoft-graph-client@3.0.0 windows-iana@5.0.2
Совет
Windows пользователи могут получить следующее сообщение об ошибке при попытке установить эти пакеты на Windows.
gyp ERR! stack Error: Can't find Python executable "python", you can set the PYTHON env variable.
Чтобы устранить ошибку, запустите следующую команду, чтобы установить инструменты сборки Windows с помощью окна терминала повышенного уровня (Администратор), которое устанавливает средства сборки VS и Python.
npm install --global --production windows-build-tools
Обнови приложение для использования программного
connect-flash
обеспечения и среднегоexpress-session
программного обеспечения. Откройте ./app.js и добавьте следующееrequire
утверждение в верхнюю часть файла.const session = require('express-session'); const flash = require('connect-flash'); const msal = require('@azure/msal-node');
Добавьте следующий код сразу после строки
var app = express();
.// Session middleware // NOTE: Uses default in-memory session store, which is not // suitable for production app.use(session({ secret: 'your_secret_value_here', resave: false, saveUninitialized: false, unset: 'destroy' })); // Flash middleware app.use(flash()); // Set up local vars for template layout app.use(function(req, res, next) { // Read any flashed errors and save // in the response locals res.locals.error = req.flash('error_msg'); // Check for simple error string and // convert to layout's expected format var errs = req.flash('error'); for (var i in errs){ res.locals.error.push({message: 'An error occurred', debug: errs[i]}); } // Check for an authenticated user and load // into response locals if (req.session.userId) { res.locals.user = app.locals.users[req.session.userId]; } next(); });
Проектирование приложения
В этом разделе будет реализован пользовательский интерфейс приложения.
Откройте ./views/layout.hbs и замените все содержимое следующим кодом.
<!DOCTYPE html> <html> <head> <title>Node.js Graph Tutorial</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous"> <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.12.1/css/all.css" crossorigin="anonymous"> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark"> <div class="container"> <a href="/" class="navbar-brand">Node.js Graph Tutorial</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav mr-auto"> <li class="nav-item"> <a href="/" class="nav-link{{#if active.home}} active{{/if}}">Home</a> </li> {{#if user}} <li class="nav-item" data-turbolinks="false"> <a href="/calendar" class="nav-link{{#if active.calendar}} active{{/if}}">Calendar</a> </li> {{/if}} </ul> <ul class="navbar-nav justify-content-end"> <li class="nav-item"> <a class="nav-link" href="https://developer.microsoft.com/graph/docs/concepts/overview" target="_blank"> <i class="fas fa-external-link-alt mr-1"></i>Docs </a> </li> {{#if user}} <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"> {{#if user.avatar}} <img src="{{ user.avatar }}" class="rounded-circle align-self-center mr-2" style="width: 32px;"> {{else}} <i class="far fa-user-circle fa-lg rounded-circle align-self-center mr-2" style="width: 32px;"></i> {{/if}} </a> <div class="dropdown-menu dropdown-menu-right"> <h5 class="dropdown-item-text mb-0">{{ user.displayName }}</h5> <p class="dropdown-item-text text-muted mb-0">{{ user.email }}</p> <div class="dropdown-divider"></div> <a href="/auth/signout" class="dropdown-item">Sign Out</a> </div> </li> {{else}} <li class="nav-item"> <a href="/auth/signin" class="nav-link">Sign In</a> </li> {{/if}} </ul> </div> </div> </nav> <main role="main" class="container"> {{#each error}} <div class="alert alert-danger" role="alert"> <p class="mb-3">{{ this.message }}</p> {{#if this.debug }} <pre class="alert-pre border bg-light p-2"><code>{{ this.debug }}</code></pre> {{/if}} </div> {{/each}} {{{body}}} </main> <!-- Bootstrap/jQuery --> <script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous"></script> <script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" integrity="sha384-9/reFTGAW83EW2RDu2S0VKaIzap3H66lZH81PoYlFhbGU+6BZp6G7niu735Sk7lN" crossorigin="anonymous"></script> <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" integrity="sha384-B4gt1jrGC7Jh4AgTPSdUtOBvfO8shuf57BaghqFfPlYxofvL8/KUEfYiJOMMV+rV" crossorigin="anonymous"></script> </body> </html>
Этот код добавляет Bootstrap для простого стиля. Он также определяет глобальную макет с панели nav.
Откройте таблицы ./public/stylesheets/style.css и замените все содержимое следующим образом.
body { padding-top: 4.5rem; } .alert-pre { word-wrap: break-word; word-break: break-all; white-space: pre-wrap; }
Откройте ./views/index.hbs и замените его содержимое следующим.
<div class="jumbotron"> <h1>Node.js Graph Tutorial</h1> <p class="lead">This sample app shows how to use the Microsoft Graph API to access a user's data from Node.js</p> {{#if user}} <h4>Welcome {{ user.displayName }}!</h4> <p>Use the navigation bar at the top of the page to get started.</p> {{else}} <a href="/auth/signin" class="btn btn-primary btn-large">Click here to sign in</a> {{/if}} </div>
Откройте ./routes/index.js и замените существующий код следующим.
var express = require('express'); var router = express.Router(); /* GET home page. */ router.get('/', function(req, res, next) { let params = { active: { home: true } }; res.render('index', params); }); module.exports = router;
Добавьте файл изображений выбора с именем no-profile-photo.png в каталоге ./public/images . Это изображение будет использоваться в качестве фотографии пользователя, если у пользователя нет фотографии в Microsoft Graph.
Сохраните все изменения и перезапустите сервер. Теперь приложение должно выглядеть совсем по-другому.
Регистрация приложения на портале
В этом упражнении будет создаваться новая регистрация веб-приложений Azure AD с помощью центра администрирования Azure Active Directory администратора.
Откройте браузер и перейдите в Центр администрирования Azure Active Directory. Войдите с помощью личной учетной записи (т.е. учетной записи Microsoft) или рабочей (учебной) учетной записи.
Выберите Azure Active Directory на панели навигации слева, затем выберите Регистрация приложений в разделе Управление.
Выберите Новая регистрация. На странице Зарегистрировать приложение задайте необходимые значения следующим образом.
- Введите имя
Node.js Graph Tutorial
. - Введите поддерживаемые типы учетных записей для учетных записей в любом каталоге организаций и личных учетных записей Microsoft.
- В разделе URI адрес перенаправления введите значение в первом раскрывающемся списке
Web
и задайте значениеhttp://localhost:3000/auth/callback
.
- Введите имя
Нажмите Зарегистрировать. На странице Node.js Graph учебника скопируйте значение ID приложения (клиента) и сохраните его, оно потребуется на следующем шаге.
Выберите Сертификаты и секреты в разделе Управление. Нажмите кнопку Новый секрет клиента. Введите значение в поле Описание, выберите один из параметров Срок действия и нажмите Добавить.
Прежде чем покинуть страницу, скопируйте значение секрета клиента. Оно вам понадобится на следующем шаге.
Важно!
Система никогда не покажет секрет клиента повторно, поэтому убедитесь, что вы скопировали его.
Добавление проверки подлинности с помощью Azure AD
В этом упражнении вы расширит приложение от предыдущего упражнения для поддержки проверки подлинности с помощью Azure AD. Это необходимо для получения необходимого маркера доступа OAuth для вызова microsoft Graph. На этом шаге вы интегрируете библиотеку msal-node в приложение.
Создайте новый файл с именем .env в корне приложения и добавьте следующий код.
OAUTH_APP_ID=YOUR_APP_ID_HERE OAUTH_APP_SECRET=YOUR_CLIENT_SECRET_HERE OAUTH_REDIRECT_URI=http://localhost:3000/auth/callback OAUTH_SCOPES='user.read,calendars.readwrite,mailboxsettings.read' OAUTH_AUTHORITY=https://login.microsoftonline.com/common/
Замените
YOUR_CLIENT_ID_HERE
с помощью ID приложения с портала регистрации приложений иYOUR_CLIENT_SECRET_HERE
замените созданный секрет клиента.Важно!
Если вы используете источник управления, например git, сейчас будет хорошее время, чтобы исключить файл .env из источника управления, чтобы избежать случайной утечки вашего ID приложения и пароль.
Откройте ./app.js и добавьте следующую строку в верхнюю часть файла, чтобы загрузить файл .env .
require('dotenv').config();
Реализация входа в систему
Найдите строку
var app = express();
в ./app.js. Вставьте следующий код после этой строки.// In-memory storage of logged-in users // For demo purposes only, production apps should store // this in a reliable storage app.locals.users = {}; // MSAL config const msalConfig = { auth: { clientId: process.env.OAUTH_APP_ID, authority: process.env.OAUTH_AUTHORITY, clientSecret: process.env.OAUTH_APP_SECRET }, system: { loggerOptions: { loggerCallback(loglevel, message, containsPii) { console.log(message); }, piiLoggingEnabled: false, logLevel: msal.LogLevel.Verbose, } } }; // Create msal application object app.locals.msalClient = new msal.ConfidentialClientApplication(msalConfig);
Этот код инициализирует библиотеку msal-node с кодом приложения и паролем для приложения.
Создайте новый файл в каталоге маршрутов с именемauth.jsи добавьте следующий код.
const router = require('express-promise-router')(); /* GET auth callback. */ router.get('/signin', async function (req, res) { const urlParameters = { scopes: process.env.OAUTH_SCOPES.split(','), redirectUri: process.env.OAUTH_REDIRECT_URI }; try { const authUrl = await req.app.locals .msalClient.getAuthCodeUrl(urlParameters); res.redirect(authUrl); } catch (error) { console.log(`Error: ${error}`); req.flash('error_msg', { message: 'Error getting auth URL', debug: JSON.stringify(error, Object.getOwnPropertyNames(error)) }); res.redirect('/'); } } ); router.get('/callback', async function(req, res) { const tokenRequest = { code: req.query.code, scopes: process.env.OAUTH_SCOPES.split(','), redirectUri: process.env.OAUTH_REDIRECT_URI }; try { const response = await req.app.locals .msalClient.acquireTokenByCode(tokenRequest); // TEMPORARY! // Flash the access token for testing purposes req.flash('error_msg', { message: 'Access token', debug: response.accessToken }); } catch (error) { req.flash('error_msg', { message: 'Error completing authentication', debug: JSON.stringify(error, Object.getOwnPropertyNames(error)) }); } res.redirect('/'); } ); router.get('/signout', async function(req, res) { // Sign out if (req.session.userId) { // Look up the user's account in the cache const accounts = await req.app.locals.msalClient .getTokenCache() .getAllAccounts(); const userAccount = accounts.find(a => a.homeAccountId === req.session.userId); // Remove the account if (userAccount) { req.app.locals.msalClient .getTokenCache() .removeAccount(userAccount); } } // Destroy the user's session req.session.destroy(function (err) { res.redirect('/'); }); } ); module.exports = router;
Это определяет маршрутизатор с тремя маршрутами:
signin
иcallback``signout
.Маршрут
signin
вызывает функциюgetAuthCodeUrl
для создания URL-адреса входа, а затем перенаправляет браузер на этот URL-адрес.Маршрут
callback
— это перенаправление Azure после завершения signin. Код вызывает функциюacquireTokenByCode
для обмена кодом авторизации для маркера доступа. После получения маркера он перенаправляется обратно на домашную страницу с маркером доступа в значении временной ошибки. Мы будем использовать это, чтобы убедиться, что наша входная группа работает, прежде чем двигаться дальше. Перед тестированием необходимо настроить приложение Express для использования нового маршрутизатора из ./routes/auth.js.Метод
signout
регистрит пользователя и уничтожает сеанс.Откройте ./app.js и вставьте следующий код перед строкой
var app = express();
.const authRouter = require('./routes/auth');
Вставьте следующий код после строки
app.use('/', indexRouter);
.app.use('/auth', authRouter);
Запустите сервер и просмотрите .https://localhost:3000
Нажмите кнопку вход, и вы должны быть перенаправлены на https://login.microsoftonline.com
. Вход с учетной записью Майкрософт и согласие на запрашиваемую разрешения. Браузер перенаправит вас в приложение, показывая маркер.
Получение сведений о пользователе
Создайте новый файл в корне проекта с именем graph.jsи добавьте следующий код.
var graph = require('@microsoft/microsoft-graph-client'); require('isomorphic-fetch'); module.exports = { getUserDetails: async function(msalClient, userId) { const client = getAuthenticatedClient(msalClient, userId); const user = await client .api('/me') .select('displayName,mail,mailboxSettings,userPrincipalName') .get(); return user; }, }; function getAuthenticatedClient(msalClient, userId) { if (!msalClient || !userId) { throw new Error( `Invalid MSAL state. Client: ${msalClient ? 'present' : 'missing'}, User ID: ${userId ? 'present' : 'missing'}`); } // Initialize Graph client const client = graph.Client.init({ // Implement an auth provider that gets a token // from the app's MSAL instance authProvider: async (done) => { try { // Get the user's account const account = await msalClient .getTokenCache() .getAccountByHomeId(userId); if (account) { // Attempt to get the token silently // This method uses the token cache and // refreshes expired tokens as needed const response = await msalClient.acquireTokenSilent({ scopes: process.env.OAUTH_SCOPES.split(','), redirectUri: process.env.OAUTH_REDIRECT_URI, account: account }); // First param to callback is the error, // Set to null in success case done(null, response.accessToken); } } catch (err) { console.log(JSON.stringify(err, Object.getOwnPropertyNames(err))); done(err, null); } } }); return client; }
При этом экспортируется
getUserDetails
функция, которая использует SDK/me
microsoft Graph для вызова конечной точки и возврата результата.Откройте ./routes/auth.js и добавьте
require
следующие утверждения в верхнюю часть файла.const graph = require('../graph');
Замените существующий маршрут вызова на следующий код.
router.get('/callback', async function(req, res) { const tokenRequest = { code: req.query.code, scopes: process.env.OAUTH_SCOPES.split(','), redirectUri: process.env.OAUTH_REDIRECT_URI }; try { const response = await req.app.locals .msalClient.acquireTokenByCode(tokenRequest); // Save the user's homeAccountId in their session req.session.userId = response.account.homeAccountId; const user = await graph.getUserDetails(response.accessToken); // Add the user to user storage req.app.locals.users[req.session.userId] = { displayName: user.displayName, email: user.mail || user.userPrincipalName, timeZone: user.mailboxSettings.timeZone }; } catch(error) { req.flash('error_msg', { message: 'Error completing authentication', debug: JSON.stringify(error, Object.getOwnPropertyNames(error)) }); } res.redirect('/'); } );
Новый код сохраняет код учетной записи пользователя в сеансе, получает сведения пользователя из Microsoft Graph и сохраняет его в хранилище пользователя приложения.
Перезапустите сервер и пройдите процедуру регистрации. Вы должны вернуться на домашняя страница, но пользовательский интерфейс должен измениться, чтобы указать, что вы подписаны.
Щелкните аватар пользователя в правом верхнем углу, чтобы получить доступ к ссылке Sign Out . Щелкнув кнопку "Выйти", вы сбросит сеанс и возвращает вас на домашнюю страницу.
Хранение и обновление маркеров
На этом этапе у приложения есть маркер доступа, который отправляется Authorization
в заголовке вызовов API. Это маркер, который позволяет приложению получать доступ к microsoft Graph от имени пользователя.
Однако этот маркер недолговечен. Срок действия маркера истекает через час после его выпуска. Вот здесь и пригодится маркер обновления. В спецификации OAuth вводится маркер обновления, который позволяет приложению запрашивать новый маркер доступа, не требуя, чтобы пользователь снова входил.
Так как приложение использует пакет msal-node, вам не нужно внедрять логику хранения маркеров или обновления. Приложение использует кэш маркера msal-node в памяти по умолчанию, которого достаточно для примера приложения. Производственные приложения должны предоставлять собственный плагин кэширования для сериализации кэша маркеров в безопасной и надежной среде хранения.
Просмотр календаря
В этом упражнении вы будете включать microsoft Graph в приложение. Для этого приложения вы будете использовать библиотеку microsoft-graph-client для звонков в Microsoft Graph.
Получение событий календаря из Outlook
Откройте ./graph.js и добавьте следующую функцию внутри
module.exports
.getCalendarView: async function(accessToken, start, end, timeZone) { const client = getAuthenticatedClient(accessToken); const events = await client .api('/me/calendarview') // Add Prefer header to get back times in user's timezone .header("Prefer", `outlook.timezone="${timeZone}"`) // Add the begin and end of the calendar window .query({ startDateTime: start, endDateTime: end }) // Get just the properties used by the app .select('subject,organizer,start,end') // Order by start time .orderby('start/dateTime') // Get at most 50 results .top(50) .get(); return events; },
Давайте посмотрим, что делает этот код.
- Вызывается URL-адрес
/me/calendarview
. - Метод
header
добавляет загонPrefer: outlook.timezone
в запрос, в результате чего время начала и окончания возвращается в часовом поясе пользователя. - Метод
query
задает параметрыstartDateTime
endDateTime
и параметры представления календаря. - Метод
select
ограничивает поля, возвращенные для каждого события, только теми, которые будут фактически использовать представление. - Метод
orderby
сортировать результаты к началу. - Метод
top
ограничивает результаты до 50 событий.
- Вызывается URL-адрес
Создайте новый файл в каталоге ./routes с именем calendar.jsи добавьте следующий код.
const router = require('express-promise-router')(); const graph = require('../graph.js'); const addDays = require('date-fns/addDays'); const formatISO = require('date-fns/formatISO'); const startOfWeek = require('date-fns/startOfWeek'); const zonedTimeToUtc = require('date-fns-tz/zonedTimeToUtc'); const iana = require('windows-iana'); const { body, validationResult } = require('express-validator'); const validator = require('validator'); /* GET /calendar */ router.get('/', async function(req, res) { if (!req.session.userId) { // Redirect unauthenticated requests to home page res.redirect('/') } else { const params = { active: { calendar: true } }; // Get the user const user = req.app.locals.users[req.session.userId]; // Convert user's Windows time zone ("Pacific Standard Time") // to IANA format ("America/Los_Angeles") const timeZoneId = iana.findIana(user.timeZone)[0]; console.log(`Time zone: ${timeZoneId.valueOf()}`); // Calculate the start and end of the current week // Get midnight on the start of the current week in the user's timezone, // but in UTC. For example, for Pacific Standard Time, the time value would be // 07:00:00Z var weekStart = zonedTimeToUtc(startOfWeek(new Date()), timeZoneId.valueOf()); var weekEnd = addDays(weekStart, 7); console.log(`Start: ${formatISO(weekStart)}`); try { // Get the events const events = await graph.getCalendarView( req.app.locals.msalClient, req.session.userId, formatISO(weekStart), formatISO(weekEnd), user.timeZone); res.json(events.value); } catch (err) { res.send(JSON.stringify(err, Object.getOwnPropertyNames(err))); } } } ); module.exports = router;
Обновление ./app.js для использования этого нового маршрута. Добавьте следующую строку перед строкой
var app = express();
.const calendarRouter = require('./routes/calendar');
Добавьте следующую строку после строки
app.use('/auth', authRouter);
.app.use('/calendar', calendarRouter);
Перезапустите сервер. Войдите и щелкните ссылку Календарь в панели nav. Если все работает надлежащим образом, в календаре пользователя должен появиться дамп событий в формате JSON.
Отображение результатов
Теперь вы можете добавить представление для отображения результатов в более понятной пользователям форме.
Добавьте следующий код в ./app.js после строки
app.set('view engine', 'hbs');
.var hbs = require('hbs'); var parseISO = require('date-fns/parseISO'); var formatDate = require('date-fns/format'); // Helper to format date/time sent by Graph hbs.registerHelper('eventDateTime', function(dateTime) { const date = parseISO(dateTime); return formatDate(date, 'M/d/yy h:mm a'); });
Это реализует помощник handlebars для формата даты ISO 8601, возвращенной корпорацией Майкрософт Graph во что-то более удобное для человека.
Создайте новый файл в каталоге ./views с именем calendar.hbs и добавьте следующий код.
<h1 class="mb-3">Calendar</h1> <a href="/calendar/new" class="btn btn-light btn-sm mb-3">New event</a> <table class="table"> <thead> <tr> <th scope="col">Organizer</th> <th scope="col">Subject</th> <th scope="col">Start</th> <th scope="col">End</th> </tr> </thead> <tbody> {{#each events}} <tr> <td>{{this.organizer.emailAddress.name}}</td> <td>{{this.subject}}</td> <td>{{eventDateTime this.start.dateTime}}</td> <td>{{eventDateTime this.end.dateTime}}</td> </tr> {{/each}} </tbody> </table>
Это позволяет повторять коллекцию событий и добавить для каждого из них строку таблицы.
Теперь обнови маршрут в ./routes/calendar.js , чтобы использовать это представление. Замените существующий маршрут следующим кодом.
/* GET /calendar */ router.get('/', async function(req, res) { if (!req.session.userId) { // Redirect unauthenticated requests to home page res.redirect('/') } else { const params = { active: { calendar: true } }; // Get the user const user = req.app.locals.users[req.session.userId]; // Convert user's Windows time zone ("Pacific Standard Time") // to IANA format ("America/Los_Angeles") const timeZoneId = iana.findIana(user.timeZone)[0]; console.log(`Time zone: ${timeZoneId.valueOf()}`); // Calculate the start and end of the current week // Get midnight on the start of the current week in the user's timezone, // but in UTC. For example, for Pacific Standard Time, the time value would be // 07:00:00Z var weekStart = zonedTimeToUtc(startOfWeek(new Date()), timeZoneId.valueOf()); var weekEnd = addDays(weekStart, 7); console.log(`Start: ${formatISO(weekStart)}`); // Get the access token var accessToken; try { accessToken = await getAccessToken(req.session.userId, req.app.locals.msalClient); } catch (err) { req.flash('error_msg', { message: 'Could not get access token. Try signing out and signing in again.', debug: JSON.stringify(err, Object.getOwnPropertyNames(err)) }); return; } if (accessToken && accessToken.length > 0) { try { // Get the events const events = await graph.getCalendarView( accessToken, formatISO(weekStart), formatISO(weekEnd), user.timeZone); params.events = events.value; } catch (err) { req.flash('error_msg', { message: 'Could not fetch events', debug: JSON.stringify(err, Object.getOwnPropertyNames(err)) }); } } else { req.flash('error_msg', 'Could not get an access token'); } res.render('calendar', params); } } );
Сохраните изменения, перезапустите сервер и вопишитесь в приложение. Щелкните ссылку Календарь , и теперь приложение должно отрисовки таблицы событий.
Создание нового события
В этом разделе вы добавим возможность создания событий в календаре пользователя.
Создание новой формы события
Создайте новый файл в каталоге ./views с именем newevent.hbs и добавьте следующий код.
<form method="POST"> <div class="form-group"> <label>Subject</label> <input class="form-control" name="ev-subject" type="text" value="{{ newEvent.subject }}"> </div> <div class="form-group"> <label>Attendees</label> <input class="form-control" name="ev-attendees" type="text" value="{{ newEvent.attendees }}"> </div> <div class="form-row"> <div class="col"> <div class="form-group"> <label>Start</label> <input class="form-control" name="ev-start" type="datetime-local" value="{{ newEvent.start }}"> </div> </div> <div class="col"> <div class="form-group"> <label>End</label> <input class="form-control" name="ev-end" type="datetime-local" value="{{ newEvent.end }}"> </div> </div> </div> <div class="form-group mb-3"> <label>Body</label> <textarea class="form-control" name="ev-body" rows="3">{{ newEvent.body }}</textarea> </div> <input class="btn btn-primary mr-2" type="submit" value="Create" /> <a class="btn btn-secondary" href="/calendar">Cancel</a> </form>
Добавьте следующий код в файл ./routes/calendar.js перед
module.exports = router;
строкой./* GET /calendar/new */ router.get('/new', function(req, res) { if (!req.session.userId) { // Redirect unauthenticated requests to home page res.redirect('/') } else { res.locals.newEvent = {}; res.render('newevent'); } } );
Это реализует форму для ввода пользователя и отрисовки.
Создание события
Откройте ./graph.js и добавьте следующую функцию внутри
module.exports
.createEvent: async function(accessToken, formData, timeZone) { const client = getAuthenticatedClient(accessToken); // Build a Graph event const newEvent = { subject: formData.subject, start: { dateTime: formData.start, timeZone: timeZone }, end: { dateTime: formData.end, timeZone: timeZone }, body: { contentType: 'text', content: formData.body } }; // Add attendees if present if (formData.attendees) { newEvent.attendees = []; formData.attendees.forEach(attendee => { newEvent.attendees.push({ type: 'required', emailAddress: { address: attendee } }); }); } // POST /me/events await client .api('/me/events') .post(newEvent); },
Этот код использует поля форм для создания объекта события Graph, а затем отправляет запрос POST в конечную точку для создания события в календаре по умолчанию
/me/events
пользователя.Добавьте следующий код в файл ./routes/calendar.js перед
module.exports = router;
строкой./* POST /calendar/new */ router.post('/new', [ body('ev-subject').escape(), // Custom sanitizer converts ;-delimited string // to an array of strings body('ev-attendees').customSanitizer(value => { return value.split(';'); // Custom validator to make sure each // entry is an email address }).custom(value => { value.forEach(element => { if (!validator.isEmail(element)) { throw new Error('Invalid email address'); } }); return true; }), // Ensure start and end are ISO 8601 date-time values body('ev-start').isISO8601(), body('ev-end').isISO8601(), body('ev-body').escape() ], async function(req, res) { if (!req.session.userId) { // Redirect unauthenticated requests to home page res.redirect('/') } else { // Build an object from the form values const formData = { subject: req.body['ev-subject'], attendees: req.body['ev-attendees'], start: req.body['ev-start'], end: req.body['ev-end'], body: req.body['ev-body'] }; // Check if there are any errors with the form values const formErrors = validationResult(req); if (!formErrors.isEmpty()) { let invalidFields = ''; formErrors.errors.forEach(error => { invalidFields += `${error.param.slice(3, error.param.length)},` }); // Preserve the user's input when re-rendering the form // Convert the attendees array back to a string formData.attendees = formData.attendees.join(';'); return res.render('newevent', { newEvent: formData, error: [{ message: `Invalid input in the following fields: ${invalidFields}` }] }); } // Get the access token var accessToken; try { accessToken = await getAccessToken(req.session.userId, req.app.locals.msalClient); } catch (err) { req.flash('error_msg', { message: 'Could not get access token. Try signing out and signing in again.', debug: JSON.stringify(err, Object.getOwnPropertyNames(err)) }); return; } // Get the user const user = req.app.locals.users[req.session.userId]; // Create the event try { await graph.createEvent(accessToken, formData, user.timeZone); } catch (error) { req.flash('error_msg', { message: 'Could not create event', debug: JSON.stringify(error, Object.getOwnPropertyNames(error)) }); } // Redirect back to the calendar view return res.redirect('/calendar'); } } );
Этот код проверяет и дезинфицировать ввод формы, а затем вызывает
graph.createEvent
для создания события. Он перенаправляется обратно в представление календаря после завершения вызова.Сохраните изменения и перезапустите приложение. Щелкните элемент Календарь nav, а затем нажмите кнопку Создать событие. Заполните значения и нажмите кнопку Создать. Приложение возвращается в представление календаря после создания нового события.
Поздравляем!
Вы завершили учебный Node.js Microsoft Graph. Теперь, когда у вас есть рабочее приложение, которое вызывает Microsoft Graph, вы можете экспериментировать и добавлять новые функции. В обзоре microsoft Graph, чтобы увидеть все данные, к ним можно получить доступ с помощью microsoft Graph.
Отзывы
Возникла проблема с этим разделом? Если это так, отправьте нам отзыв, чтобы мы исправили этот раздел.