Microsoft Graph を使って Node.js Express アプリを構築する
このチュートリアルでは、Microsoft Node.js API を使用してユーザーの予定表情報を取得する Graph Express Web アプリを構築する方法について説明します。
ヒント
完成したチュートリアルをダウンロードするだけの場合は、2 つの方法でダウンロードできます。
- クイック スタート Node.jsダウンロードして 、作業コードを数分で取得します。
- リポジトリをダウンロードまたは複製GitHubします。
前提条件
このデモを開始する前に、開発 Node.js インストールする必要があります。 インストールされていない場合はNode.js前のリンクにアクセスしてダウンロード オプションを確認してください。
注意
Windows C/C++ からコンパイルする必要Visual Studio Build Tools NPM モジュールをサポートするために、Python と windows をインストールする必要がある場合があります。 このNode.jsインストーラー Windowsこれらのツールを自動的にインストールするオプションを提供します。 または、以下の手順に従います https://github.com/nodejs/node-gyp#on-windows。
また、Outlook.com 上のメールボックスを持つ個人用 Microsoft アカウント、または Microsoft の仕事用または学校用のアカウントを持っている必要があります。 Microsoft アカウントをお持ちでない場合は、無料アカウントを取得するためのオプションが 2 つご利用できます。
- 新しい 個人用 Microsoft アカウントにサインアップできます。
- 開発者プログラムにサインアップして、Microsoft 365サブスクリプションをMicrosoft 365できます。
注意
このチュートリアルは、ノード バージョン 14.15.0 で記述されています。 このガイドの手順は、他のバージョンでも動作しますが、テストされていない場合があります。
フィードバック
このチュートリアルに関するフィードバックは、リポジトリのGitHubしてください。
Node.js Express Web アプリを作成する
この演習では、Express を 使用して Web アプリをビルドします。
CLI を開き、ファイルを作成する権限を持つディレクトリに移動し、次のコマンドを実行して、ハンドルバーをレンダリング エンジンとして使用する新しい Express アプリを作成します。
npx express-generator --hbs graph-tutorial
Express ジェネレーターは、Express アプリと呼ばれる新
graph-tutorial
しいディレクトリを作成し、スキャフォールディングします。ディレクトリに移動し
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 へようこそ" というメッセージが表示されます。 そのメッセージが表示しない場合は、「Express の開始ガイド 」を参照してください。
ノード パッケージのインストール
次に進む前に、後で使用する追加のパッケージをインストールします。
- .env ファイルから 値を読み込む場合の dotenv。
- 日付/時刻の値を書式設定する date-fns 。
- windows-iana を使用して、Windowsタイム ゾーン名を IANA タイム ゾーンの ID に変換します。
- アプリ内のフラッシュ エラー メッセージに接続します。
- メモリ内のサーバー 側セッションに値を格納する express-session。
- ルート ハンドラーが Promise を返すのを許可する express-promise-router。
- フォーム データを解析 および検証するための express-validator。
- アクセス トークンを認証および取得する msal-node 。
- Microsoft-graph-client を使用して Microsoft Graph。
- isomorphic-fetch to polyfill the fetch for Node. ライブラリにはフェッチ ポリフィルが必要
microsoft-graph-client
です。 詳細については、「Microsoft Graph JavaScript クライアント ライブラリ wiki」を参照してください。 - 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.
エラーを解決するには、次のコマンドを実行して、VS ビルド ツールと Python をインストールする管理者特権 (管理者) ターミナル ウィンドウを使用して Windows ビルド ツールをインストールします。
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.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>
このコードでは、 単純なスタイル設定のためにブートストラップ を追加します。 また、ナビゲーション バーを持つグローバル レイアウトも定義します。
./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;
./public/images ディレクトリに、選択 no-profile-photo.png名前の イメージ ファイルを追加します。 この画像は、ユーザーが Microsoft サーバーで写真を持ってない場合に、ユーザーの写真Graph。
変更内容をすべて保存し、サーバーを再起動します。 これで、アプリは非常に異なって見える必要があります。
ポータルでアプリを登録する
この演習では、管理者センターを使用して新AD Azure 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。 これは、Microsoft サーバーを呼び出す必要がある OAuth アクセス トークンを取得するために必要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();
サインインの実装
./app.js
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-node ライブラリを初期化します。
. /routes ディレクトリに新しいファイルを 作成し、 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;
これは、3 つのルートを持つルーターを定義
callback``signout
します。signin
ルート
signin
は関数を呼getAuthCodeUrl
び出してログイン URL を生成し、ブラウザーをその URL にリダイレクトします。ルート
callback
は、サインインが完了した後に Azure がリダイレクトする場所です。 コードは、アクセス トークンのacquireTokenByCode
承認コードを交換する関数を呼び出します。 トークンが取得された後、一時的なエラー値でアクセス トークンを使用してホーム ページにリダイレクトします。 これを使用して、サインインが動作しているか確認してから、次に進む必要があります。 テストする前に、./routes/auth.jsから新しいルーターを使用する Express アプリを構成する必要があります。この
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アクセスできるトークンです。
ただし、このトークンは一時的なものです。 トークンは発行後 1 時間で期限切れになります。 ここで、更新トークンが役に立ちます。 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./ 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'); });
これにより、Microsoft が返す ISO 8601 日付を人間にGraphに書式設定するための Handlebars ヘルパーが実装されます。
calendar.hbs という 名前の ./views ディレクトリに新しいファイルを作成し、次のコードを追加します。
<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); } } );
変更を保存し、サーバーを再起動し、アプリにサインインします。 [予定表] リンクをクリック すると、アプリはイベントのテーブルを表示する必要があります。
新しいイベントを作成する
このセクションでは、ユーザーの予定表にイベントを作成する機能を追加します。
新しいイベント フォームを作成する
newevent.hbs という名前の ./views ディレクトリに新しいファイルを作成し、次のコードを追加します。
<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でアクセスできるすべてのデータを確認Graph。
フィードバック
このチュートリアルに関するフィードバックは、GitHubしてください。
このセクションに問題がある場合 このセクションを改善できるよう、フィードバックをお送りください。