Skip to content

Commit 1e74fb0

Browse files
author
Benoit Schweblin
committed
Added google helper
1 parent 855e6cb commit 1e74fb0

File tree

13 files changed

+672
-127
lines changed

13 files changed

+672
-127
lines changed

src/components/NavigationBar.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</button>
1212
</div>
1313
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
14-
<div class="navigation-bar__spinner">
14+
<div class="navigation-bar__spinner" v-show="showSpinner">
1515
<div class="spinner"></div>
1616
</div>
1717
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
@@ -75,6 +75,9 @@ export default {
7575
...mapGetters('layout', [
7676
'styles',
7777
]),
78+
showSpinner() {
79+
return !this.$store.state.queue.isEmpty;
80+
},
7881
titleWidth() {
7982
if (!this.mounted) {
8083
return 0;

src/components/SideBar.vue

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,16 @@
1313
</div>
1414
<div class="side-bar__inner">
1515
<div v-if="panel === 'menu'" class="side-bar__panel side-bar__panel--menu">
16-
<side-bar-item @click.native="signin">
16+
<side-bar-item v-if="!loginToken" @click.native="signin">
1717
<icon-login slot="icon"></icon-login>
1818
<div>Sign in with Google</div>
1919
<span>Have all your files and settings backed up and synced.</span>
2020
</side-bar-item>
21-
<side-bar-item @click.native="signin">
22-
<icon-login slot="icon"></icon-login>
23-
<div>Sign in on CouchDB</div>
24-
<span>Save and collaborate on a CouchDB hosted by you.</span>
25-
</side-bar-item>
21+
<!-- <side-bar-item @click.native="signin">
22+
<icon-login slot="icon"></icon-login>
23+
<div>Sign in on CouchDB</div>
24+
<span>Save and collaborate on a CouchDB hosted by you.</span>
25+
</side-bar-item> -->
2626
<side-bar-item @click.native="panel = 'toc'">
2727
<icon-toc slot="icon"></icon-toc>
2828
Table of contents
@@ -44,12 +44,13 @@
4444
</template>
4545

4646
<script>
47-
import { mapActions } from 'vuex';
47+
import { mapGetters, mapActions } from 'vuex';
4848
import Toc from './Toc';
4949
import SideBarItem from './SideBarItem';
5050
import markdownSample from '../data/markdownSample.md';
5151
import markdownConversionSvc from '../services/markdownConversionSvc';
52-
import userSvc from '../services/userSvc';
52+
import googleHelper from '../services/helpers/googleHelper';
53+
import syncSvc from '../services/syncSvc';
5354
5455
const panelNames = {
5556
menu: 'Menu',
@@ -72,6 +73,9 @@ export default {
7273
markdownSample: markdownConversionSvc.highlight(markdownSample),
7374
}),
7475
computed: {
76+
...mapGetters('data', [
77+
'loginToken',
78+
]),
7579
panelName() {
7680
return panelNames[this.panel];
7781
},
@@ -81,7 +85,10 @@ export default {
8185
'toggleSideBar',
8286
]),
8387
signin() {
84-
userSvc.signinWithGoogle();
88+
googleHelper.startOauth2([
89+
'openid',
90+
'https://www.googleapis.com/auth/drive.appdata',
91+
]).then(() => syncSvc.requestSync());
8592
},
8693
},
8794
};
@@ -111,6 +118,10 @@ export default {
111118
left: 1000px;
112119
}
113120
121+
.side-bar__panel--menu {
122+
padding: 5px;
123+
}
124+
114125
.side-bar__panel--help {
115126
padding: 0 10px 40px 20px;
116127

src/components/SideBarItem.vue

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
text-align: left;
1515
padding: 10px 12px;
1616
height: auto;
17-
margin: 5px;
1817
1918
span {
2019
display: inline-block;

src/index.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import Vue from 'vue';
22
import './extensions/';
3-
import './services/syncSvc';
43
import './services/optional';
54
import './icons/';
65
import App from './components/App';

src/services/helpers/googleHelper.js

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
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

Comments
 (0)