|
| 1 | +import utils from '../utils'; |
| 2 | +import store from '../../store'; |
| 3 | + |
| 4 | +const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com'; |
| 5 | +const appsDomain = null; |
| 6 | +const tokenExpirationMargin = 10 * 60 * 1000; // 10 min |
| 7 | + |
| 8 | +// const scopeMap = { |
| 9 | +// profile: [ |
| 10 | +// 'https://www.googleapis.com/auth/userinfo.profile', |
| 11 | +// ], |
| 12 | +// gdrive: [ |
| 13 | +// 'https://www.googleapis.com/auth/drive.install', |
| 14 | +// store.getters['data/settings'].gdriveFullAccess === true ? |
| 15 | +// 'https://www.googleapis.com/auth/drive' : |
| 16 | +// 'https://www.googleapis.com/auth/drive.file', |
| 17 | +// ], |
| 18 | +// blogger: [ |
| 19 | +// 'https://www.googleapis.com/auth/blogger', |
| 20 | +// ], |
| 21 | +// picasa: [ |
| 22 | +// 'https://www.googleapis.com/auth/photos', |
| 23 | +// ], |
| 24 | +// }; |
| 25 | + |
| 26 | +const request = (googleToken, options) => utils.request({ |
| 27 | + ...options, |
| 28 | + headers: { |
| 29 | + ...options.headers, |
| 30 | + Authorization: `Bearer ${googleToken.accessToken}`, |
| 31 | + }, |
| 32 | +}); |
| 33 | + |
| 34 | +const saveFile = (googleToken, data, appData) => { |
| 35 | + const options = { |
| 36 | + method: 'POST', |
| 37 | + url: 'https://www.googleapis.com/upload/drive/v2/files', |
| 38 | + headers: {}, |
| 39 | + }; |
| 40 | + if (appData) { |
| 41 | + options.method = 'PUT'; |
| 42 | + options.url = `https://www.googleapis.com/drive/v2/files/${appData.id}`; |
| 43 | + options.headers['if-match'] = appData.etag; |
| 44 | + } |
| 45 | + const metadata = { |
| 46 | + title: data.name, |
| 47 | + parents: [{ |
| 48 | + id: 'appDataFolder', |
| 49 | + }], |
| 50 | + properties: Object.keys(data) |
| 51 | + .filter(key => key !== 'name' && key !== 'tx') |
| 52 | + .map(key => ({ |
| 53 | + key, |
| 54 | + value: JSON.stringify(data[key]), |
| 55 | + visibility: 'PUBLIC', |
| 56 | + })), |
| 57 | + }; |
| 58 | + const media = null; |
| 59 | + const boundary = `-------${utils.uid()}`; |
| 60 | + const delimiter = `\r\n--${boundary}\r\n`; |
| 61 | + const closeDelimiter = `\r\n--${boundary}--`; |
| 62 | + if (media) { |
| 63 | + let multipartRequestBody = ''; |
| 64 | + multipartRequestBody += delimiter; |
| 65 | + multipartRequestBody += 'Content-Type: application/json\r\n\r\n'; |
| 66 | + multipartRequestBody += JSON.stringify(metadata); |
| 67 | + multipartRequestBody += delimiter; |
| 68 | + multipartRequestBody += 'Content-Type: application/json\r\n\r\n'; |
| 69 | + multipartRequestBody += JSON.stringify(media); |
| 70 | + multipartRequestBody += closeDelimiter; |
| 71 | + return request(googleToken, { |
| 72 | + ...options, |
| 73 | + params: { |
| 74 | + uploadType: 'multipart', |
| 75 | + }, |
| 76 | + headers: { |
| 77 | + ...options.headers, |
| 78 | + 'Content-Type': `multipart/mixed; boundary="${boundary}"`, |
| 79 | + }, |
| 80 | + body: multipartRequestBody, |
| 81 | + }); |
| 82 | + } |
| 83 | + return request(googleToken, { |
| 84 | + ...options, |
| 85 | + body: metadata, |
| 86 | + }).then(res => ({ |
| 87 | + id: res.body.id, |
| 88 | + etag: res.body.etag, |
| 89 | + })); |
| 90 | +}; |
| 91 | + |
| 92 | +export default { |
| 93 | + startOauth2(scopes, sub = null, silent = false) { |
| 94 | + return utils.startOauth2( |
| 95 | + 'https://accounts.google.com/o/oauth2/v2/auth', { |
| 96 | + client_id: clientId, |
| 97 | + response_type: 'token', |
| 98 | + scope: scopes.join(' '), |
| 99 | + hd: appsDomain, |
| 100 | + login_hint: sub, |
| 101 | + prompt: silent ? 'none' : null, |
| 102 | + }, silent) |
| 103 | + // Call the tokeninfo endpoint |
| 104 | + .then(data => utils.request({ |
| 105 | + method: 'POST', |
| 106 | + url: 'https://www.googleapis.com/oauth2/v3/tokeninfo', |
| 107 | + params: { |
| 108 | + access_token: data.accessToken, |
| 109 | + }, |
| 110 | + }).then((res) => { |
| 111 | + // Check the returned client ID consistency |
| 112 | + if (res.body.aud !== clientId) { |
| 113 | + throw new Error('Client ID inconsistent.'); |
| 114 | + } |
| 115 | + // Check the returned sub consistency |
| 116 | + if (sub && res.body.sub !== sub) { |
| 117 | + throw new Error('Google account ID not expected.'); |
| 118 | + } |
| 119 | + // Build token object including scopes and sub |
| 120 | + return { |
| 121 | + scopes, |
| 122 | + accessToken: data.accessToken, |
| 123 | + expiresOn: Date.now() + (data.expiresIn * 1000), |
| 124 | + sub: res.body.sub, |
| 125 | + isLogin: !store.getters['data/loginToken'], |
| 126 | + }; |
| 127 | + })) |
| 128 | + // Call the tokeninfo endpoint |
| 129 | + .then(googleToken => request(googleToken, { |
| 130 | + method: 'GET', |
| 131 | + url: 'https://www.googleapis.com/plus/v1/people/me', |
| 132 | + }).then((res) => { |
| 133 | + // Add name to googleToken |
| 134 | + googleToken.name = res.body.displayName; |
| 135 | + const existingToken = store.getters['data/googleTokens'][googleToken.sub]; |
| 136 | + if (existingToken) { |
| 137 | + if (!sub) { |
| 138 | + throw new Error('Google account already linked.'); |
| 139 | + } |
| 140 | + // Add isLogin and lastChangeId to googleToken |
| 141 | + googleToken.isLogin = existingToken.isLogin; |
| 142 | + googleToken.lastChangeId = existingToken.lastChangeId; |
| 143 | + } |
| 144 | + // Add googleToken to googleTokens |
| 145 | + store.dispatch('data/setGoogleToken', googleToken); |
| 146 | + return googleToken; |
| 147 | + })); |
| 148 | + }, |
| 149 | + refreshToken(scopes, googleToken) { |
| 150 | + const sub = googleToken.sub; |
| 151 | + const lastToken = store.getters['data/googleTokens'][sub]; |
| 152 | + const mergedScopes = [...new Set([ |
| 153 | + ...scopes, |
| 154 | + ...lastToken.scopes, |
| 155 | + ])]; |
| 156 | + |
| 157 | + return Promise.resolve() |
| 158 | + .then(() => { |
| 159 | + if (mergedScopes.length === lastToken.scopes.length) { |
| 160 | + return lastToken; |
| 161 | + } |
| 162 | + // New scopes are requested, popup an authorize window |
| 163 | + return this.startOauth2(mergedScopes, sub); |
| 164 | + }) |
| 165 | + .then((refreshedToken) => { |
| 166 | + if (refreshedToken.expiresOn > Date.now() + tokenExpirationMargin) { |
| 167 | + // Token is fresh enough |
| 168 | + return refreshedToken; |
| 169 | + } |
| 170 | + // Token is almost outdated, try to take one in background |
| 171 | + return this.startOauth2(mergedScopes, sub, true) |
| 172 | + // If it fails try to popup a window |
| 173 | + .catch(() => this.startOauth2(mergedScopes, sub)); |
| 174 | + }); |
| 175 | + }, |
| 176 | + getChanges(googleToken) { |
| 177 | + let changes = []; |
| 178 | + return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken) |
| 179 | + .then((refreshedToken) => { |
| 180 | + const lastChangeId = refreshedToken.lastChangeId || 0; |
| 181 | + const getPage = pageToken => request(refreshedToken, { |
| 182 | + method: 'GET', |
| 183 | + url: 'https://www.googleapis.com/drive/v2/changes', |
| 184 | + params: { |
| 185 | + pageToken, |
| 186 | + startChangeId: pageToken || !lastChangeId ? null : lastChangeId + 1, |
| 187 | + spaces: 'appDataFolder', |
| 188 | + fields: 'nextPageToken,items(deleted,file/id,file/etag,file/title,file/properties(key,value))', |
| 189 | + }, |
| 190 | + }).then((res) => { |
| 191 | + changes = changes.concat(res.body.items); |
| 192 | + if (res.body.nextPageToken) { |
| 193 | + return getPage(res.body.nextPageToken); |
| 194 | + } |
| 195 | + return changes; |
| 196 | + }); |
| 197 | + |
| 198 | + return getPage(); |
| 199 | + }); |
| 200 | + }, |
| 201 | + updateLastChangeId(googleToken, changes) { |
| 202 | + const refreshedToken = store.getters['data/googleTokens'][googleToken.sub]; |
| 203 | + let lastChangeId = refreshedToken.lastChangeId || 0; |
| 204 | + changes.forEach((change) => { |
| 205 | + if (change.id > lastChangeId) { |
| 206 | + lastChangeId = change.id; |
| 207 | + } |
| 208 | + }); |
| 209 | + if (lastChangeId !== refreshedToken.lastChangeId) { |
| 210 | + store.dispatch('data/setGoogleToken', { |
| 211 | + ...refreshedToken, |
| 212 | + lastChangeId, |
| 213 | + }); |
| 214 | + } |
| 215 | + }, |
| 216 | + insertData(googleToken, data) { |
| 217 | + return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken) |
| 218 | + .then(refreshedToken => saveFile(refreshedToken, data)); |
| 219 | + }, |
| 220 | + updateData(googleToken, data, appData) { |
| 221 | + return this.refreshToken(['https://www.googleapis.com/auth/drive.appdata'], googleToken) |
| 222 | + .then(refreshedToken => saveFile(refreshedToken, data, appData)); |
| 223 | + }, |
| 224 | +}; |
0 commit comments