使用 Microsoft Graph 生成 Node.js Express 应用
本教程指导你如何生成使用 Microsoft Graph API 检索用户的日历信息的 Node.js Express Web 应用。
先决条件
在开始此演示之前,应在开发 Node.js 安装此演示。 如果没有安装Node.js,请访问上一链接,查看下载选项。
备注
Windows用户可能需要安装 Python 和 Visual Studio 生成工具 以支持需要从 C/C++ 编译的 NPM 模块。 Node.js安装程序Windows自动安装这些工具的选项。 或者,也可以按照 中的说明进行操作 https://github.com/nodejs/node-gyp#on-windows。
您还应该有一个在 Outlook.com 上拥有邮箱的个人 Microsoft 帐户,或者一个 Microsoft 工作或学校帐户。 如果你没有 Microsoft 帐户,则有几个选项可以获取免费帐户:
- 你可以 注册新的个人 Microsoft 帐户。
- 你可以注册开发人员计划Microsoft 365免费订阅Microsoft 365订阅。
备注
本教程是使用 Node 版本 14.15.0 编写的。 本指南中的步骤可能与其他版本一起运行,但该版本尚未经过测试。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
创建 Node.js Express Web 应用
在此练习中,你将使用 Express 生成 Web 应用。
打开 CLI,导航到你拥有创建文件权限的目录,然后运行以下命令创建一个新的 Express 应用,该应用使用 Handlebars 作为呈现引擎。
npx express-generator --hbs graph-tutorial
Express 生成器创建名为 的新目录
graph-tutorial
,并搭建 Express 应用基架。导航到
graph-tutorial
目录并输入以下命令以安装依赖项。npm install
运行以下命令以使用报告的漏洞更新节点包。
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
使用以下命令启动本地 Web 服务器。
npm start
打开浏览器,并导航到
http://localhost:3000
。 如果一切正常,你将看到"欢迎使用 Express"消息。 如果看不到该邮件,请查看 快速入门指南。
安装节点包
在继续之前,请安装一些你稍后将使用的其他程序包:
- 用于从 .env 文件加载值的 dotenv。
- 用于设置日期/时间值格式的 date-fns。
- 用于将时区名称Windows IANA 时区 ID 的 windows-iana。
- 将闪存 连接到应用中的闪存错误消息。
- express-session ,用于存储内存中服务器端会话中的值。
- express-promise-router ,以允许路由处理程序返回 Promise。
- 用于分析和验证表单数据的 express-validator 。
- 用于验证和 获取访问令牌的 msal 节点。
- 用于调用 Microsoft Graph 的 microsoft-graph-client。
- isomorphic-fetch 以填充 Node 的提取。 库需要提取填充
microsoft-graph-client
。 有关详细信息,请参阅 Microsoft Graph JavaScript 客户端库 Wiki。 - 生成 URL 查询字符串的 qs。
在 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.
若要解决此错误,请运行以下命令,使用提升的 (Administrator) 终端窗口安装 Windows Build Tools,该窗口将安装 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(); });
设计应用
在此部分中,你将实现应用的 UI。
打开 ./views/layout.bms ,将全部内容替换为以下代码。
<!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 。 它还定义具有导航栏的全局布局。
打开 ./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.bms ,并将其内容替换为以下内容。
<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;
在 ./public/images 目录中 添加no-profile-photo.png选择的图像文件。 当用户在 Microsoft Graph 中没有照片时,此图像将用作用户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 节点 库集成到应用程序中。
在应用程序的根目录下创建一个名为 .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);
此代码使用应用的应用 ID 和密码初始化 msal 节点库。
在名为auth.js的 ./routes 目录中 创建新文件,并添加以下代码。
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 在登录完成后重定向的地方。 代码调用 函数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
。 使用 Microsoft 帐户登录并同意请求的权限。 浏览器将重新定向到应用程序,显示令牌。
获取用户详细信息
在名为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
,该函数使用 Microsoft Graph SDK 调用/me
终结点并返回结果。打开 ./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('/'); } );
新代码在会话中保存用户的帐户 ID,从 Microsoft Graph 获取用户的详细信息,并保存到应用的用户存储中。
重新启动服务器并完成登录过程。 你最终应返回主页,但 UI 应更改以指示你已登录。
单击右上角的用户头像以访问 "注销" 链接。 单击 "注销 "将重置会话,并返回到主页。
存储和刷新令牌
此时,应用程序具有访问令牌,该令牌在 Authorization
API 调用标头中发送。 这是允许应用代表用户访问 Microsoft Graph令牌。
但是,此令牌期很短。 令牌在颁发后一小时过期。 此时刷新令牌将变得有效。 OAuth 规范引入了刷新令牌,允许应用请求新的访问令牌,而无需用户重新登录。
由于应用使用的是 msal 节点包,因此无需实现任何令牌存储或刷新逻辑。 应用程序使用默认的 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 为
在名为calendar.js的 ./routes 目录中创建新 文件,并添加以下代码。
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);
重新启动服务器。 登录并单击导航 栏中 的"日历"链接。 如果一切正常,应在用户日历上看到事件被 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 帮助程序,将 Microsoft Graph返回的 ISO 8601 日期格式化为更友好的内容。
在 . /views 目录中新建一个名为 calendar.bms 的文件,并添加以下代码。
<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.bms 的文件,并添加以下代码。
<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
以创建事件。 呼叫完成后,它将重定向回日历视图。保存更改并重新启动该应用。 单击" 日历 "导航项,然后单击"创建 事件" 按钮。 填写值,然后单击"创建 "。 一旦创建了新事件,应用将返回到日历视图。
恭喜!
已完成 Microsoft Node.js Graph教程。 现在,你已经拥有一个调用 Microsoft Graph,你可以试验和添加新功能。 请访问Microsoft Graph概述,查看可以使用 Microsoft Graph 访问的所有数据。
反馈
Please provide any feedback on this tutorial in the GitHub repository.
你有关于此部分的问题? 如果有,请向我们提供反馈,以便我们对此部分作出改进。