onedrive.js 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. // References
  2. // - https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow
  3. //
  4. // Note:
  5. // - SPA refresh tokens expire after 24h, but each refresh operation returns a new refresh_token, extending the expiration.
  6. // - Browser extensions cannot use the native app authorization flow due to Microsoft's restrictions.
  7. import { dumpQuery, getUniqId, loadQuery, noop } from '@/common';
  8. import { FORM_URLENCODED, VM_HOME } from '@/common/consts';
  9. import { objectGet } from '@/common/object';
  10. import {
  11. BaseService,
  12. getCodeChallenge,
  13. getCodeVerifier,
  14. getItemFilename,
  15. getURI,
  16. INIT_ERROR,
  17. INIT_RETRY,
  18. INIT_UNAUTHORIZED,
  19. isScriptFile,
  20. openAuthPage,
  21. register,
  22. } from './base';
  23. const config = {
  24. client_id: process.env.SYNC_ONEDRIVE_CLIENT_ID,
  25. redirect_uri: VM_HOME + 'auth_onedrive.html',
  26. };
  27. const OneDrive = BaseService.extend({
  28. name: 'onedrive',
  29. displayName: 'OneDrive',
  30. urlPrefix: 'https://graph.microsoft.com/v1.0',
  31. async requestAuth() {
  32. try {
  33. await this.loadData({
  34. url: '/drive/special/approot',
  35. responseType: 'json',
  36. });
  37. } catch (err) {
  38. let code = INIT_ERROR;
  39. if (err.status === 401) {
  40. code = INIT_RETRY;
  41. } else if (
  42. err.status === 400 &&
  43. objectGet(err, 'data.error') === 'invalid_grant'
  44. ) {
  45. code = INIT_UNAUTHORIZED;
  46. }
  47. return { code, error: err };
  48. }
  49. },
  50. async list() {
  51. let files = [];
  52. let url = '/drive/special/approot/children';
  53. while (url) {
  54. const data = await this.loadData({
  55. url,
  56. responseType: 'json',
  57. });
  58. url = data['@odata.nextLink'] || '';
  59. files = [
  60. ...files,
  61. ...data.value
  62. .filter((item) => item.file && isScriptFile(item.name))
  63. .map(normalize),
  64. ];
  65. }
  66. return files;
  67. },
  68. get(item) {
  69. const name = getItemFilename(item);
  70. return this.loadData({
  71. url: `/drive/special/approot:/${encodeURIComponent(name)}:/content`,
  72. });
  73. },
  74. put(item, data) {
  75. const name = getItemFilename(item);
  76. return this.loadData({
  77. method: 'PUT',
  78. url: `/drive/special/approot:/${encodeURIComponent(name)}:/content`,
  79. headers: {
  80. 'Content-Type': 'application/octet-stream',
  81. },
  82. body: data,
  83. responseType: 'json',
  84. }).then(normalize);
  85. },
  86. remove(item) {
  87. // return 204
  88. const name = getItemFilename(item);
  89. return this.loadData({
  90. method: 'DELETE',
  91. url: `/drive/special/approot:/${encodeURIComponent(name)}`,
  92. }).catch(noop);
  93. },
  94. async authorize() {
  95. this.session = {
  96. state: getUniqId(),
  97. codeVerifier: getCodeVerifier(),
  98. };
  99. const params = {
  100. client_id: config.client_id,
  101. scope: 'openid profile Files.ReadWrite.AppFolder offline_access',
  102. response_type: 'code',
  103. redirect_uri: config.redirect_uri,
  104. state: this.session.state,
  105. ...(await getCodeChallenge(this.session.codeVerifier)),
  106. };
  107. const url = `https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?${dumpQuery(
  108. params,
  109. )}`;
  110. openAuthPage(url, config.redirect_uri);
  111. },
  112. matchAuth(url) {
  113. const redirectUri = `${config.redirect_uri}?`;
  114. if (!url.startsWith(redirectUri)) return;
  115. const query = loadQuery(url.slice(redirectUri.length));
  116. const { state, codeVerifier } = this.session || {};
  117. this.session = null;
  118. if (query.state !== state || !query.code) return;
  119. return {
  120. code: query.code,
  121. code_verifier: codeVerifier,
  122. };
  123. },
  124. async finishAuth(payload) {
  125. await this.authorized({
  126. code: payload.code,
  127. code_verifier: payload.code_verifier,
  128. grant_type: 'authorization_code',
  129. redirect_uri: config.redirect_uri,
  130. });
  131. },
  132. revoke() {
  133. this.config.set({
  134. uid: null,
  135. token: null,
  136. refresh_token: null,
  137. });
  138. return this.prepare();
  139. },
  140. async authorized(params) {
  141. const data = await this.loadData({
  142. method: 'POST',
  143. url: 'https://login.microsoftonline.com/consumers/oauth2/v2.0/token',
  144. prefix: '',
  145. headers: {
  146. 'Content-Type': FORM_URLENCODED,
  147. },
  148. body: dumpQuery(
  149. Object.assign(
  150. {
  151. client_id: config.client_id,
  152. },
  153. params,
  154. ),
  155. ),
  156. responseType: 'json',
  157. });
  158. if (!data.access_token) throw data;
  159. this.config.set({
  160. token: data.access_token,
  161. refresh_token: data.refresh_token || params.refresh_token,
  162. });
  163. },
  164. });
  165. if (config.client_id) register(OneDrive);
  166. function normalize(item) {
  167. return {
  168. name: item.name,
  169. size: item.size,
  170. uri: getURI(item.name),
  171. // modified: new Date(item.lastModifiedDateTime).getTime(),
  172. };
  173. }