Browse Source

feat(sync/onedrive): renew token after expired

Gerald 9 years ago
parent
commit
b8ff5d37c9

+ 38 - 24
src/background/sync/index.js

@@ -1,5 +1,4 @@
 var sync = function () {
-  var METAFILE = 'Violentmonkey';
   var services = [];
   var servicesReady = [];
   var queue, nextQueue = [];
@@ -89,7 +88,7 @@ var sync = function () {
       service = new Service(name);
       setTimeout(function () {
         services.push(service);
-        inited && service.prepare();
+        inited && service.checkSync();
       });
     } else {
       // get existent instance
@@ -145,7 +144,7 @@ var sync = function () {
   function init() {
     inited = true;
     services.forEach(function (service) {
-      service.prepare();
+      service.checkSync();
     });
   }
   function getFilename(uri) {
@@ -174,6 +173,7 @@ var sync = function () {
     displayName: 'BaseService',
     delayTime: 1000,
     urlPrefix: '',
+    metaFile: 'Violentmonkey',
     delay: function (time) {
       if (time == null) time = this.delayTime;
       return new Promise(function (resolve, reject) {
@@ -188,6 +188,7 @@ var sync = function () {
         'idle',
         'initializing',
         'authorized',
+        'authorizing',  // in case some services require asynchronous requests to get access_tokens
         'unauthorized',
         'error',
       ], null, _this.onStateChange),
@@ -219,8 +220,12 @@ var sync = function () {
       var _this = this;
       var token = _this.token = _this.config.get('token');
       _this.initHeaders();
-      (token ? Promise.resolve(_this.user()) : Promise.reject())
-      .then(function (text) {
+      return (token ? Promise.resolve(_this.user()) : Promise.reject());
+    },
+    checkSync: function () {
+      var _this = this;
+      return _this.prepare()
+      .then(function () {
         _this.authState.set('authorized');
         servicesReady.push(_this);
         sync(_this);
@@ -232,7 +237,7 @@ var sync = function () {
           } else {
             _this.authState.set('error');
           }
-          _this.syncState.set('error');
+          _this.syncState.set('idle');
           _this.config.setOption('enabled', false);
         } else {
           _this.authState.set('unauthorized');
@@ -240,6 +245,18 @@ var sync = function () {
       });
     },
     user: function () {},
+    getMeta: function () {
+      var _this = this;
+      return _this.get(_this.metaFile)
+      .then(function (data) {
+        return JSON.parse(data);
+      }, function (res) {
+        if (res.status === 404) {
+          return {};
+        }
+        throw res;
+      });
+    },
     initHeaders: function () {
       var headers = this.headers = {};
       var token = this.token;
@@ -259,7 +276,9 @@ var sync = function () {
       return lastFetch.then(function () {
         return new Promise(function (resolve, reject) {
           var xhr = new XMLHttpRequest;
-          xhr.open(options.method || 'GET', _this.urlPrefix + options.url, true);
+          var prefix = options.prefix;
+          if (prefix == null) prefix = _this.urlPrefix;
+          xhr.open(options.method || 'GET', prefix + options.url, true);
           var headers = _.assign({}, _this.headers, options.headers);
           if (options.body && typeof options.body === 'object') {
             headers['Content-Type'] = 'application/json';
@@ -300,26 +319,21 @@ var sync = function () {
       if (!_this.authState.is('authorized') || !_this.config.getOption('enabled'))
         return Promise.resolve();
       _this.syncState.set('syncing');
-      return Promise.all([
-        _this.list(),
-        _this.get(METAFILE)
-        .then(function (data) {
-          return JSON.parse(data);
-        }, function (res) {
-          if (res.status === 404) {
-            return {};
-          }
-          throw res;
-        }),
-        vmdb.getScriptsByIndex('position'),
-      ]).then(function (res) {
+      return _this.getMeta()
+      .then(function (meta) {
+        return Promise.all([
+          meta,
+          _this.list(),
+          vmdb.getScriptsByIndex('position'),
+        ]);
+      }).then(function (res) {
         var remote = {
-          data: res[0],
-          meta: res[1],
+          meta: res[0],
+          data: res[1],
         };
         var local = {
-          data: res[2],
           meta: _this.config.get('meta', {}),
+          data: res[2],
         };
         var firstSync = !local.meta.timestamp;
         var outdated = !local.meta.timestamp || remote.meta.timestamp > local.meta.timestamp;
@@ -417,7 +431,7 @@ var sync = function () {
           if (!remote.meta.timestamp || putRemote.length || delRemote.length) {
             remoteChanged = true;
             remote.meta.timestamp = Date.now();
-            promises.push(_this.put(METAFILE, JSON.stringify(remote.meta)));
+            promises.push(_this.put(_this.metaFile, JSON.stringify(remote.meta)));
           }
           if (!local.meta.timestamp || getRemote.length || delLocal.length || remoteChanged || outdated) {
             local.meta.timestamp = remote.meta.timestamp;

+ 74 - 17
src/background/sync/onedrive.js

@@ -1,15 +1,18 @@
 setTimeout(function () {
-  var config = {
+  var config = _.assign({
     client_id: '000000004418358A',
     redirect_uri: 'https://violentmonkey.github.io/auth_onedrive.html',
-  };
+  }, JSON.parse(
+    // assume this is secret
+    window.atob('eyJjbGllbnRfc2VjcmV0Ijoiajl4M09WRXRIdmhpSEtEV09HcXV5TWZaS2s5NjA0MEgifQ==')
+  ));
 
   function authenticate() {
     var params = {
-      response_type: 'token',
+      response_type: 'code',
       client_id: config.client_id,
       redirect_uri: config.redirect_uri,
-      scope: 'onedrive.appfolder',
+      scope: 'onedrive.appfolder wl.offline_access',
     };
     var url = 'https://login.live.com/oauth20_authorize.srf';
     var qs = searchParams.dump(params);
@@ -17,21 +20,43 @@ setTimeout(function () {
     chrome.tabs.create({url: url});
   }
   function checkAuthenticate(url) {
-    var redirect_uri = config.redirect_uri + '#';
+    var redirect_uri = config.redirect_uri + '?code=';
     if (url.slice(0, redirect_uri.length) === redirect_uri) {
-      authorized(url.slice(redirect_uri.length));
+      onedrive.authState.set('authorizing');
+      authorized({
+        code: url.slice(redirect_uri.length),
+      }).then(function () {
+        onedrive.prepare();
+      });
       return true;
     }
   }
-  function authorized(raw) {
-    var data = searchParams.load(raw);
-    if (data.access_token) {
-      onedrive.config.set({
-        uid: data.user_id,
-        token: data.access_token,
-      });
-      onedrive.prepare();
-    }
+  function authorized(params) {
+    return onedrive.request({
+      method: 'POST',
+      url: 'https://login.live.com/oauth20_token.srf',
+      prefix: '',
+      headers: {
+        'Content-Type': 'application/x-www-form-urlencoded',
+      },
+      body: searchParams.dump(_.assign({}, {
+        client_id: config.client_id,
+        client_secret: config.client_secret,
+        redirect_uri: config.redirect_uri,
+        grant_type: 'authorization_code',
+      }, params)),
+    }).then(function (text) {
+      var data = JSON.parse(text);
+      if (data.access_token) {
+        onedrive.config.set({
+          uid: data.user_id,
+          token: data.access_token,
+          refresh_token: data.refresh_token,
+        });
+      } else {
+        throw data;
+      }
+    });
   }
   function normalize(item) {
     return {
@@ -45,10 +70,42 @@ setTimeout(function () {
     name: 'onedrive',
     displayName: 'OneDrive',
     urlPrefix: 'https://api.onedrive.com/v1.0',
+    refreshToken: function () {
+      var _this = this;
+      var refresh_token = _this.config.get('refresh_token');
+      return authorized({
+        refresh_token: refresh_token,
+        grant_type: 'refresh_token',
+      }).then(function () {
+        return _this.prepare();
+      });
+    },
     user: function () {
-      return this.request({
-        url: '/drive',
+      var _this = this;
+      return requestUser()
+      .catch(function (res) {
+        if (res.status === 401) {
+          return _this.refreshToken().then(requestUser);
+        }
+        throw res;
+      });
+      function requestUser() {
+        return _this.request({
+          url: '/drive',
+        });
+      }
+    },
+    getMeta: function () {
+      var _this = this;
+      return getMeta()
+      .catch(function (res) {
+        if (res.status === 401) {
+          return _this.refreshToken().then(getMeta);
+        }
       });
+      function getMeta() {
+        return sync.BaseService.prototype.getMeta.call(_this);
+      }
     },
     list: function () {
       var _this = this;

+ 10 - 5
src/options/templates/sync-service.html

@@ -2,19 +2,24 @@
   <input type=checkbox data-check="<%= it.name + 'Enabled' %>" data-sync="<%= it.name %>" <%=
   it.enabled ? 'checked' : ''
   %> <%=
-  it.authorized ? '' : 'disabled'
+  it.authState === 'authorized' ? '' : 'disabled'
   %>>
   <span><%= _.i18n('labelSyncTo', it.displayName || it.name) %></span>
 </label>
-<button data-auth="<%= it.name %>" <%= it.unauthorized ? '' : 'disabled' %>><%=
-  it.authorized ? _.i18n('buttonAuthorized') : _.i18n('buttonAuthorize')
+<button data-auth="<%= it.name %>" <%=
+  it.authState === 'unauthorized' ? '' : 'disabled'
+%>><%=
+  {
+    authorized: _.i18n('buttonAuthorized'),
+    authorizing: _.i18n('buttonAuthorizing'),
+  }[it.authState] || _.i18n('buttonAuthorize')
 %></button>
 <button <%= !_.includes(['authorized', 'error'], it.authState) || it.syncing ? 'disabled' : '' %> class="sync-start">
   <i class="fa fa-refresh"></i>
 </button>
 <span><%=
-  it.initializing ? _.i18n('msgSyncInit') :
-  it.error ? _.i18n('msgSyncError') :
+  it.authState === 'initializing' ? _.i18n('msgSyncInit') :
+  it.syncState === 'error' ? _.i18n('msgSyncError') :
   it.syncing ? _.i18n('msgSyncing') :
   it.lastSync ? _.i18n('lastSync', it.lastSync) : ''
 %></span>

+ 0 - 4
src/options/views/sync-service.js

@@ -7,10 +7,6 @@ var SyncServiceView = BaseView.extend({
   _render: function () {
     var it = this.model.toJSON();
     it.enabled = _.options.get(it.name + 'Enabled');
-    it.initializing = it.authState === 'initializing';
-    it.authorized = it.authState === 'authorized';
-    it.unauthorized = it.authState === 'unauthorized';
-    it.error = it.syncState === 'error';
     it.syncing = it.syncState === 'syncing';
     it.lastSync = it.timestamp && new Date(it.timestamp).toLocaleString();
     this.$el.html(this.templateFn(it));