Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

microsoftCalendar.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. /* @flow */
  2. import { Client } from '@microsoft/microsoft-graph-client';
  3. import rs from 'jsrsasign';
  4. import { createDeferred } from '../../../../modules/util/helpers';
  5. import parseURLParams from '../../base/config/parseURLParams';
  6. import { parseStandardURIString } from '../../base/util';
  7. import { getShareInfoText } from '../../invite';
  8. import { setCalendarAPIAuthState } from '../actions';
  9. /**
  10. * Constants used for interacting with the Microsoft API.
  11. *
  12. * @private
  13. * @type {object}
  14. */
  15. const MS_API_CONFIGURATION = {
  16. /**
  17. * The URL to use when authenticating using Microsoft API.
  18. * @type {string}
  19. */
  20. AUTH_ENDPOINT:
  21. 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?',
  22. CALENDAR_ENDPOINT: '/me/calendars',
  23. /**
  24. * The Microsoft API scopes to request access for calendar.
  25. *
  26. * @type {string}
  27. */
  28. MS_API_SCOPES: 'openid profile Calendars.ReadWrite',
  29. /**
  30. * See https://docs.microsoft.com/en-us/azure/active-directory/develop/
  31. * v2-oauth2-implicit-grant-flow#send-the-sign-in-request. This value is
  32. * needed for passing in the proper domain_hint value when trying to refresh
  33. * a token silently.
  34. *
  35. *
  36. * @type {string}
  37. */
  38. MS_CONSUMER_TENANT: '9188040d-6c67-4c5b-b112-36a304b66dad',
  39. /**
  40. * The redirect URL to be used by the Microsoft API on successful
  41. * authentication.
  42. *
  43. * @type {string}
  44. */
  45. REDIRECT_URI: `${window.location.origin}/static/msredirect.html`
  46. };
  47. /**
  48. * Store the window from an auth request. That way it can be reused if a new
  49. * request comes in and it can be used to indicate a request is in progress.
  50. *
  51. * @private
  52. * @type {Object|null}
  53. */
  54. let popupAuthWindow = null;
  55. /**
  56. * A stateless collection of action creators that implements the expected
  57. * interface for interacting with the Microsoft API in order to get calendar
  58. * data.
  59. *
  60. * @type {Object}
  61. */
  62. export const microsoftCalendarApi = {
  63. /**
  64. * Retrieves the current calendar events.
  65. *
  66. * @param {number} fetchStartDays - The number of days to go back
  67. * when fetching.
  68. * @param {number} fetchEndDays - The number of days to fetch.
  69. * @returns {function(Dispatch<*>, Function): Promise<CalendarEntries>}
  70. */
  71. getCalendarEntries(fetchStartDays: ?number, fetchEndDays: ?number) {
  72. return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
  73. const state = getState()['features/calendar-sync'] || {};
  74. const token = state.msAuthState && state.msAuthState.accessToken;
  75. if (!token) {
  76. return Promise.reject('Not authorized, please sign in!');
  77. }
  78. const client = Client.init({
  79. authProvider: done => done(null, token)
  80. });
  81. return client
  82. .api(MS_API_CONFIGURATION.CALENDAR_ENDPOINT)
  83. .get()
  84. .then(response => {
  85. const calendarIds = response.value.map(en => en.id);
  86. const getEventsPromises = calendarIds.map(id =>
  87. requestCalendarEvents(
  88. client, id, fetchStartDays, fetchEndDays));
  89. return Promise.all(getEventsPromises);
  90. })
  91. // get .value of every element from the array of results,
  92. // which is an array of events and flatten it to one array
  93. // of events
  94. .then(result => [].concat(...result))
  95. .then(entries => entries.map(e => formatCalendarEntry(e)));
  96. };
  97. },
  98. /**
  99. * Returns the email address for the currently logged in user.
  100. *
  101. * @returns {function(Dispatch<*, Function>): Promise<string>}
  102. */
  103. getCurrentEmail(): Function {
  104. return (dispatch: Dispatch<*>, getState: Function) => {
  105. const { msAuthState = {} }
  106. = getState()['features/calendar-sync'] || {};
  107. const email = msAuthState.userSigninName || '';
  108. return Promise.resolve(email);
  109. };
  110. },
  111. /**
  112. * Sets the application ID to use for interacting with the Microsoft API.
  113. *
  114. * @returns {function(): Promise<void>}
  115. */
  116. load(): Function {
  117. return () => Promise.resolve();
  118. },
  119. /**
  120. * Prompts the participant to sign in to the Microsoft API Client Library.
  121. *
  122. * @returns {function(Dispatch<*>, Function): Promise<void>}
  123. */
  124. signIn(): Function {
  125. return (dispatch: Dispatch<*>, getState: Function) => {
  126. // Ensure only one popup window at a time.
  127. if (popupAuthWindow) {
  128. popupAuthWindow.focus();
  129. return Promise.reject('Sign in already in progress.');
  130. }
  131. const signInDeferred = createDeferred();
  132. const guids = {
  133. authState: generateGuid(),
  134. authNonce: generateGuid()
  135. };
  136. dispatch(setCalendarAPIAuthState(guids));
  137. const { microsoftApiApplicationClientID }
  138. = getState()['features/base/config'];
  139. const authUrl = getAuthUrl(
  140. microsoftApiApplicationClientID,
  141. guids.authState,
  142. guids.authNonce);
  143. const h = 600;
  144. const w = 480;
  145. popupAuthWindow = window.open(
  146. authUrl,
  147. 'Auth M$',
  148. `width=${w}, height=${h}, top=${
  149. (screen.height / 2) - (h / 2)}, left=${
  150. (screen.width / 2) - (w / 2)}`);
  151. const windowCloseCheck = setInterval(() => {
  152. if (popupAuthWindow && popupAuthWindow.closed) {
  153. signInDeferred.reject(
  154. 'Popup closed before completing auth.');
  155. popupAuthWindow = null;
  156. window.removeEventListener('message', handleAuth);
  157. clearInterval(windowCloseCheck);
  158. } else if (!popupAuthWindow) {
  159. // This case probably happened because the user completed
  160. // auth.
  161. clearInterval(windowCloseCheck);
  162. }
  163. }, 500);
  164. /**
  165. * Callback with scope access to other variables that are part of
  166. * the sign in request.
  167. *
  168. * @param {Object} event - The event from the post message.
  169. * @private
  170. * @returns {void}
  171. */
  172. function handleAuth({ data }) {
  173. if (!data || data.type !== 'ms-login') {
  174. return;
  175. }
  176. window.removeEventListener('message', handleAuth);
  177. popupAuthWindow && popupAuthWindow.close();
  178. popupAuthWindow = null;
  179. const params = getParamsFromHash(data.url);
  180. const tokenParts = getValidatedTokenParts(
  181. params, guids, microsoftApiApplicationClientID);
  182. if (!tokenParts) {
  183. signInDeferred.reject('Invalid token received');
  184. return;
  185. }
  186. dispatch(setCalendarAPIAuthState({
  187. authState: undefined,
  188. accessToken: tokenParts.accessToken,
  189. idToken: tokenParts.idToken,
  190. tokenExpires: params.tokenExpires,
  191. userDomainType: tokenParts.userDomainType,
  192. userSigninName: tokenParts.userSigninName
  193. }));
  194. signInDeferred.resolve();
  195. }
  196. window.addEventListener('message', handleAuth);
  197. return signInDeferred.promise;
  198. };
  199. },
  200. /**
  201. * Returns whether or not the user is currently signed in.
  202. *
  203. * @returns {function(Dispatch<*>, Function): Promise<boolean>}
  204. */
  205. _isSignedIn(): Function {
  206. return (dispatch: Dispatch<*>, getState: Function) => {
  207. const now = new Date().getTime();
  208. const state
  209. = getState()['features/calendar-sync'].msAuthState || {};
  210. const tokenExpires = parseInt(state.tokenExpires, 10);
  211. const isExpired = now > tokenExpires && !isNaN(tokenExpires);
  212. if (state.accessToken && isExpired) {
  213. // token expired, let's refresh it
  214. return dispatch(this._refreshAuthToken())
  215. .then(() => true)
  216. .catch(() => false);
  217. }
  218. return Promise.resolve(state.accessToken && !isExpired);
  219. };
  220. },
  221. /**
  222. * Renews an existing auth token so it can continue to be used.
  223. *
  224. * @private
  225. * @returns {function(Dispatch<*>, Function): Promise<void>}
  226. */
  227. _refreshAuthToken(): Function {
  228. return (dispatch: Dispatch<*>, getState: Function) => {
  229. const { microsoftApiApplicationClientID }
  230. = getState()['features/base/config'];
  231. const { msAuthState = {} }
  232. = getState()['features/calendar-sync'] || {};
  233. const refreshAuthUrl = getAuthRefreshUrl(
  234. microsoftApiApplicationClientID,
  235. msAuthState.userDomainType,
  236. msAuthState.userSigninName);
  237. const iframe = document.createElement('iframe');
  238. iframe.setAttribute('id', 'auth-iframe');
  239. iframe.setAttribute('name', 'auth-iframe');
  240. iframe.setAttribute('style', 'display: none');
  241. iframe.setAttribute('src', refreshAuthUrl);
  242. const signInPromise = new Promise(resolve => {
  243. iframe.onload = () => {
  244. resolve(iframe.contentWindow.location.hash);
  245. };
  246. });
  247. // The check for body existence is done for flow, which also runs
  248. // against native where document.body may not be defined.
  249. if (!document.body) {
  250. return Promise.reject(
  251. 'Cannot refresh auth token in this environment');
  252. }
  253. document.body.appendChild(iframe);
  254. return signInPromise.then(hash => {
  255. const params = getParamsFromHash(hash);
  256. dispatch(setCalendarAPIAuthState({
  257. accessToken: params.access_token,
  258. idToken: params.id_token,
  259. tokenExpires: params.tokenExpires
  260. }));
  261. });
  262. };
  263. },
  264. /**
  265. * Updates calendar event by generating new invite URL and editing the event
  266. * adding some descriptive text and location.
  267. *
  268. * @param {string} id - The event id.
  269. * @param {string} calendarId - The id of the calendar to use.
  270. * @param {string} location - The location to save to the event.
  271. * @returns {function(Dispatch<*>): Promise<string|never>}
  272. */
  273. updateCalendarEvent(id: string, calendarId: string, location: string) {
  274. return (dispatch: Dispatch<*>, getState: Function): Promise<*> => {
  275. const state = getState()['features/calendar-sync'] || {};
  276. const token = state.msAuthState && state.msAuthState.accessToken;
  277. if (!token) {
  278. return Promise.reject('Not authorized, please sign in!');
  279. }
  280. const { dialInNumbersUrl } = getState()['features/base/config'];
  281. const text = getShareInfoText(
  282. location, dialInNumbersUrl !== undefined, true/* use html */);
  283. const client = Client.init({
  284. authProvider: done => done(null, token)
  285. });
  286. return client
  287. .api(`/me/events/${id}`)
  288. .get()
  289. .then(description => {
  290. const body = description.body;
  291. if (description.bodyPreview) {
  292. body.content = `${description.bodyPreview}<br><br>`;
  293. }
  294. // replace all new lines from the text with html <br>
  295. // to make it pretty
  296. body.content += text.split('\n').join('<br>');
  297. return client
  298. .api(`/me/calendar/events/${id}`)
  299. .patch({
  300. body,
  301. location: {
  302. 'displayName': location
  303. }
  304. });
  305. });
  306. };
  307. }
  308. };
  309. /**
  310. * Parses the Microsoft calendar entries to a known format.
  311. *
  312. * @param {Object} entry - The Microsoft calendar entry.
  313. * @private
  314. * @returns {{
  315. * calendarId: string,
  316. * description: string,
  317. * endDate: string,
  318. * id: string,
  319. * location: string,
  320. * startDate: string,
  321. * title: string
  322. * }}
  323. */
  324. function formatCalendarEntry(entry) {
  325. return {
  326. calendarId: entry.calendarId,
  327. description: entry.body.content,
  328. endDate: entry.end.dateTime,
  329. id: entry.id,
  330. location: entry.location.displayName,
  331. startDate: entry.start.dateTime,
  332. title: entry.subject
  333. };
  334. }
  335. /**
  336. * Generate a guid to be used for verifying token validity.
  337. *
  338. * @private
  339. * @returns {string} The generated string.
  340. */
  341. function generateGuid() {
  342. const buf = new Uint16Array(8);
  343. window.crypto.getRandomValues(buf);
  344. return `${s4(buf[0])}${s4(buf[1])}-${s4(buf[2])}-${s4(buf[3])}-${
  345. s4(buf[4])}-${s4(buf[5])}${s4(buf[6])}${s4(buf[7])}`;
  346. }
  347. /**
  348. * Constructs and returns the URL to use for renewing an auth token.
  349. *
  350. * @param {string} appId - The Microsoft application id to log into.
  351. * @param {string} userDomainType - The domain type of the application as
  352. * provided by Microsoft.
  353. * @param {string} userSigninName - The email of the user signed into the
  354. * integration with Microsoft.
  355. * @private
  356. * @returns {string} - The auth URL.
  357. */
  358. function getAuthRefreshUrl(appId, userDomainType, userSigninName) {
  359. return [
  360. getAuthUrl(appId, 'undefined', 'undefined'),
  361. 'prompt=none',
  362. `domain_hint=${userDomainType}`,
  363. `login_hint=${userSigninName}`
  364. ].join('&');
  365. }
  366. /**
  367. * Constructs and returns the auth URL to use for login.
  368. *
  369. * @param {string} appId - The Microsoft application id to log into.
  370. * @param {string} authState - The authState guid to use.
  371. * @param {string} authNonce - The authNonce guid to use.
  372. * @private
  373. * @returns {string} - The auth URL.
  374. */
  375. function getAuthUrl(appId, authState, authNonce) {
  376. const authParams = [
  377. 'response_type=id_token+token',
  378. `client_id=${appId}`,
  379. `redirect_uri=${MS_API_CONFIGURATION.REDIRECT_URI}`,
  380. `scope=${MS_API_CONFIGURATION.MS_API_SCOPES}`,
  381. `state=${authState}`,
  382. `nonce=${authNonce}`,
  383. 'response_mode=fragment'
  384. ].join('&');
  385. return `${MS_API_CONFIGURATION.AUTH_ENDPOINT}${authParams}`;
  386. }
  387. /**
  388. * Converts a url from an auth redirect into an object of parameters passed
  389. * into the url.
  390. *
  391. * @param {string} url - The string to parse.
  392. * @private
  393. * @returns {Object}
  394. */
  395. function getParamsFromHash(url) {
  396. const params = parseURLParams(parseStandardURIString(url), true, 'hash');
  397. // Get the number of seconds the token is valid for, subtract 5 minutes
  398. // to account for differences in clock settings and convert to ms.
  399. const expiresIn = (parseInt(params.expires_in, 10) - 300) * 1000;
  400. const now = new Date();
  401. const expireDate = new Date(now.getTime() + expiresIn);
  402. params.tokenExpires = expireDate.getTime().toString();
  403. return params;
  404. }
  405. /**
  406. * Converts the parameters from a Microsoft auth redirect into an object of
  407. * token parts. The value "null" will be returned if the params do not produce
  408. * a valid token.
  409. *
  410. * @param {Object} tokenInfo - The token object.
  411. * @param {Object} guids - The guids for authState and authNonce that should
  412. * match in the token.
  413. * @param {Object} appId - The Microsoft application this token is for.
  414. * @private
  415. * @returns {Object|null}
  416. */
  417. function getValidatedTokenParts(tokenInfo, guids, appId) {
  418. // Make sure the token matches the request source by matching the GUID.
  419. if (tokenInfo.state !== guids.authState) {
  420. return null;
  421. }
  422. const idToken = tokenInfo.id_token;
  423. // A token must exist to be valid.
  424. if (!idToken) {
  425. return null;
  426. }
  427. const tokenParts = idToken.split('.');
  428. if (tokenParts.length !== 3) {
  429. return null;
  430. }
  431. const payload
  432. = rs.KJUR.jws.JWS.readSafeJSONString(rs.b64utoutf8(tokenParts[1]));
  433. if (payload.nonce !== guids.authNonce
  434. || payload.aud !== appId
  435. || payload.iss
  436. !== `https://login.microsoftonline.com/${payload.tid}/v2.0`) {
  437. return null;
  438. }
  439. const now = new Date();
  440. // Adjust by 5 minutes to allow for inconsistencies in system clocks.
  441. const notBefore = new Date((payload.nbf - 300) * 1000);
  442. const expires = new Date((payload.exp + 300) * 1000);
  443. if (now < notBefore || now > expires) {
  444. return null;
  445. }
  446. return {
  447. accessToken: tokenInfo.access_token,
  448. idToken,
  449. userDisplayName: payload.name,
  450. userDomainType:
  451. payload.tid === MS_API_CONFIGURATION.MS_CONSUMER_TENANT
  452. ? 'consumers' : 'organizations',
  453. userSigninName: payload.preferred_username
  454. };
  455. }
  456. /**
  457. * Retrieves calendar entries from a specific calendar.
  458. *
  459. * @param {Object} client - The Microsoft-graph-client initialized.
  460. * @param {string} calendarId - The calendar ID to use.
  461. * @param {number} fetchStartDays - The number of days to go back
  462. * when fetching.
  463. * @param {number} fetchEndDays - The number of days to fetch.
  464. * @returns {Promise<any> | Promise}
  465. * @private
  466. */
  467. function requestCalendarEvents( // eslint-disable-line max-params
  468. client,
  469. calendarId,
  470. fetchStartDays,
  471. fetchEndDays): Promise<*> {
  472. const startDate = new Date();
  473. const endDate = new Date();
  474. startDate.setDate(startDate.getDate() + fetchStartDays);
  475. endDate.setDate(endDate.getDate() + fetchEndDays);
  476. const filter = `Start/DateTime ge '${
  477. startDate.toISOString()}' and End/DateTime lt '${
  478. endDate.toISOString()}'`;
  479. return client
  480. .api(`/me/calendars/${calendarId}/events`)
  481. .filter(filter)
  482. .select('id,subject,start,end,location,body')
  483. .orderby('createdDateTime DESC')
  484. .get()
  485. .then(result => result.value.map(item => {
  486. return {
  487. ...item,
  488. calendarId
  489. };
  490. }));
  491. }
  492. /**
  493. * Converts the passed in number to a string and ensure it is at least 4
  494. * characters in length, prepending 0's as needed.
  495. *
  496. * @param {number} num - The number to pad and convert to a string.
  497. * @private
  498. * @returns {string} - The number converted to a string.
  499. */
  500. function s4(num) {
  501. let ret = num.toString(16);
  502. while (ret.length < 4) {
  503. ret = `0${ret}`;
  504. }
  505. return ret;
  506. }