Fixed google authorization popup
This commit is contained in:
parent
0a39710dac
commit
73cea1879d
@ -14,6 +14,10 @@ module.exports = {
|
||||
plugins: [
|
||||
'html'
|
||||
],
|
||||
globals: {
|
||||
"NODE_ENV": false,
|
||||
"VERSION": false
|
||||
},
|
||||
// check if imports actually resolve
|
||||
'settings': {
|
||||
'import/resolver': {
|
||||
|
@ -1,4 +1,5 @@
|
||||
var path = require('path')
|
||||
var webpack = require('webpack')
|
||||
var utils = require('./utils')
|
||||
var config = require('../config')
|
||||
var vueLoaderConfig = require('./vue-loader.conf')
|
||||
@ -74,6 +75,9 @@ module.exports = {
|
||||
plugins: [
|
||||
new StylelintPlugin({
|
||||
files: ['**/*.vue', '**/*.scss']
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
VERSION: JSON.stringify(require('../package.json').version)
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ module.exports = merge(baseWebpackConfig, {
|
||||
devtool: 'source-map',
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': config.dev.env
|
||||
NODE_ENV: config.dev.env.NODE_ENV
|
||||
}),
|
||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
|
@ -28,7 +28,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
||||
plugins: [
|
||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||
new webpack.DefinePlugin({
|
||||
'process.env': env
|
||||
NODE_ENV: env.NODE_ENV
|
||||
}),
|
||||
new webpack.optimize.UglifyJsPlugin({
|
||||
compress: {
|
||||
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div v-if="ready" class="app" :class="{'app--loading': loading}">
|
||||
<splash-screen v-if="!ready"></splash-screen>
|
||||
<div v-else class="app" :class="{'app--loading': loading}">
|
||||
<layout></layout>
|
||||
<modal v-if="showModal"></modal>
|
||||
<notification></notification>
|
||||
</div>
|
||||
<div v-else class="app__spash-screen"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@ -13,6 +13,7 @@ import { mapState } from 'vuex';
|
||||
import Layout from './Layout';
|
||||
import Modal from './Modal';
|
||||
import Notification from './Notification';
|
||||
import SplashScreen from './SplashScreen';
|
||||
|
||||
// Global directives
|
||||
Vue.directive('focus', {
|
||||
@ -26,6 +27,7 @@ export default {
|
||||
Layout,
|
||||
Modal,
|
||||
Notification,
|
||||
SplashScreen,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="layout">
|
||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar && !styles.showExplorer}">
|
||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
||||
<explorer></explorer>
|
||||
</div>
|
||||
|
@ -3,6 +3,7 @@
|
||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||
@ -42,6 +43,7 @@ import editorEngineSvc from '../services/editorEngineSvc';
|
||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||
import SettingsModal from './modals/SettingsModal';
|
||||
import TemplatesModal from './modals/TemplatesModal';
|
||||
import AboutModal from './modals/AboutModal';
|
||||
import HtmlExportModal from './modals/HtmlExportModal';
|
||||
import LinkModal from './modals/LinkModal';
|
||||
import ImageModal from './modals/ImageModal';
|
||||
@ -69,6 +71,7 @@ export default {
|
||||
FilePropertiesModal,
|
||||
SettingsModal,
|
||||
TemplatesModal,
|
||||
AboutModal,
|
||||
HtmlExportModal,
|
||||
LinkModal,
|
||||
ImageModal,
|
||||
|
@ -22,13 +22,13 @@
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="isSyncPossible" :disabled="isSyncRequested || offline" @click="requestSync">
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync">
|
||||
<icon-sync></icon-sync>
|
||||
</button>
|
||||
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" v-if="publishLocations.length" :disabled="isPublishRequested || offline" @click="requestPublish">
|
||||
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish">
|
||||
<icon-upload></icon-upload>
|
||||
</button>
|
||||
</div>
|
||||
@ -148,12 +148,12 @@ export default {
|
||||
'toggleSideBar',
|
||||
]),
|
||||
requestSync() {
|
||||
if (!this.isSyncRequested) {
|
||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||
syncSvc.requestSync();
|
||||
}
|
||||
},
|
||||
requestPublish() {
|
||||
if (!this.isPublishRequested) {
|
||||
if (this.publishLocations.length && !this.isPublishRequested) {
|
||||
publishSvc.requestPublish();
|
||||
}
|
||||
},
|
||||
|
@ -90,7 +90,7 @@ export default {
|
||||
height: 100%;
|
||||
|
||||
hr {
|
||||
margin: 10px;
|
||||
margin: 10px 15px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
22
src/components/SplashScreen.vue
Normal file
22
src/components/SplashScreen.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="splash-screen">
|
||||
<div class="splash-screen__inner background-logo"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.splash-screen {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
.splash-screen__inner {
|
||||
margin: 0 auto;
|
||||
max-width: 600px;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
39
src/components/UserImage.vue
Normal file
39
src/components/UserImage.vue
Normal file
@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="user-image" :style="{'background-image': url}">
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
|
||||
const promised = {};
|
||||
|
||||
export default {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
url() {
|
||||
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
|
||||
return userInfo && `url('${userInfo.imageUrl}')`;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (!promised[this.userId] && !this.$store.state.offline) {
|
||||
promised[this.userId] = true;
|
||||
googleHelper.getUser(this.userId)
|
||||
.catch(() => {
|
||||
promised[this.userId] = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.user-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
</style>
|
@ -90,75 +90,13 @@ textarea {
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
opacity: 0.33;
|
||||
background-color: transparent;
|
||||
cursor: inherit;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.form-entry__label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: #a0a0a0;
|
||||
|
||||
.form-entry--focused & {
|
||||
color: darken($link-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__field {
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.form-entry--focused & {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__actions {
|
||||
text-align: right;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.form-entry__button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 6px;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__radio,
|
||||
.form-entry__checkbox {
|
||||
margin: 0.25em 1em;
|
||||
|
||||
input {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__info {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
line-height: 1.4;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.textfield {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
@ -289,3 +227,8 @@ textarea {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-background {
|
||||
background: no-repeat center url('../assets/logo.svg');
|
||||
background-size: contain;
|
||||
}
|
||||
|
@ -5,6 +5,12 @@
|
||||
<div>Sign in with Google</div>
|
||||
<span>Back up and sync all your files, folders and settings.</span>
|
||||
</menu-entry>
|
||||
<div v-else class="menu-entry flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon menu-entry__icon--image">
|
||||
<user-image :user-id="loginToken.sub"></user-image>
|
||||
</div>
|
||||
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||
</div>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('sync')">
|
||||
<icon-sync slot="icon"></icon-sync>
|
||||
@ -49,12 +55,14 @@
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import UserImage from '../UserImage';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
UserImage,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('data', [
|
||||
|
@ -10,6 +10,8 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.menu-entry {
|
||||
text-align: left;
|
||||
padding: 10px;
|
||||
@ -30,4 +32,9 @@
|
||||
margin-right: 12px;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.menu-entry__icon--image {
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
@ -16,13 +16,16 @@
|
||||
<span>Sign out and clean local data.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="about">
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
<span>About StackEdit</span>
|
||||
</menu-entry>
|
||||
<a href="editor" target="_blank" class="menu-entry button flex flex--row flex--align-center">
|
||||
<div class="menu-entry__icon flex flex--column flex--center">
|
||||
<icon-alert></icon-alert>
|
||||
<icon-open-in-new></icon-open-in-new>
|
||||
</div>
|
||||
<div class="flex flex--column">
|
||||
<div>StackEdit v4</div>
|
||||
<span>Deprecated.</span>
|
||||
<span>Go back to StackEdit 4</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@ -49,6 +52,9 @@ export default {
|
||||
return this.$store.dispatch('modal/reset')
|
||||
.then(() => localDbSvc.removeDb());
|
||||
},
|
||||
about() {
|
||||
return this.$store.dispatch('modal/open', 'about');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
54
src/components/modals/AboutModal.vue
Normal file
54
src/components/modals/AboutModal.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--about-modal">
|
||||
<div class="modal__inner-2">
|
||||
<div class="logo-background"></div>
|
||||
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||
<hr>
|
||||
<a target="_blank" href="https://github.com/benweet/stackedit/">StackEdit on GitHub</a> /
|
||||
<a target="_blank" href="https://github.com/benweet/stackedit/issues">issue tracker</a>
|
||||
<br>
|
||||
<a target="_blank" href="https://chrome.google.com/webstore/detail/stackedit/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> — thanks for your review!
|
||||
<br>
|
||||
<a target="_blank" href="https://twitter.com/stackedit/">StackEdit on Twitter</a>
|
||||
<hr>
|
||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
version: VERSION,
|
||||
}),
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal__inner-1--about-modal {
|
||||
text-align: center;
|
||||
|
||||
.logo-background {
|
||||
height: 75px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.app-version {
|
||||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 160px;
|
||||
max-width: 100%;
|
||||
margin: 1.5em auto;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -23,4 +23,67 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.form-entry {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.form-entry__label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
color: #a0a0a0;
|
||||
|
||||
.form-entry--focused & {
|
||||
color: darken($link-color, 10%);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__field {
|
||||
border: 1px solid #d8d8d8;
|
||||
border-radius: $border-radius-base;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.form-entry--focused & {
|
||||
border-color: $link-color;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__actions {
|
||||
text-align: right;
|
||||
margin: 0.25em;
|
||||
}
|
||||
|
||||
.form-entry__button {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
padding: 6px;
|
||||
display: inline-block;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__radio,
|
||||
.form-entry__checkbox {
|
||||
margin: 0.25em 1em;
|
||||
|
||||
input {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-entry__info {
|
||||
font-size: 0.75em;
|
||||
opacity: 0.5;
|
||||
line-height: 1.4;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Vue from 'vue';
|
||||
import Provider from './Provider';
|
||||
import FormatBold from './FormatBold';
|
||||
import FormatItalic from './FormatItalic';
|
||||
import FormatQuoteClose from './FormatQuoteClose';
|
||||
@ -40,9 +41,8 @@ import OpenInNew from './OpenInNew';
|
||||
import Information from './Information';
|
||||
import Alert from './Alert';
|
||||
import SignalOff from './SignalOff';
|
||||
// Providers
|
||||
import Provider from './Provider';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
Vue.component('iconFormatItalic', FormatItalic);
|
||||
Vue.component('iconFormatQuoteClose', FormatQuoteClose);
|
||||
@ -84,5 +84,3 @@ Vue.component('iconOpenInNew', OpenInNew);
|
||||
Vue.component('iconInformation', Information);
|
||||
Vue.component('iconAlert', Alert);
|
||||
Vue.component('iconSignalOff', SignalOff);
|
||||
// Providers
|
||||
Vue.component('iconProvider', Provider);
|
||||
|
@ -6,7 +6,7 @@ import './icons/';
|
||||
import App from './components/App';
|
||||
import store from './store';
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
if (NODE_ENV === 'production') {
|
||||
OfflinePluginRuntime.install();
|
||||
}
|
||||
|
||||
|
266
src/services/networkSvc.js
Normal file
266
src/services/networkSvc.js
Normal file
@ -0,0 +1,266 @@
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
|
||||
const scriptLoadingPromises = Object.create(null);
|
||||
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
|
||||
const networkTimeout = 30 * 1000; // 30 sec
|
||||
let isConnectionDown = false;
|
||||
|
||||
export default {
|
||||
loadScript(url) {
|
||||
if (!scriptLoadingPromises[url]) {
|
||||
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.onload = resolve;
|
||||
script.onerror = () => {
|
||||
scriptLoadingPromises[url] = null;
|
||||
reject();
|
||||
};
|
||||
script.src = url;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
return scriptLoadingPromises[url];
|
||||
},
|
||||
startOauth2(url, params = {}, silent = false) {
|
||||
// Build the authorize URL
|
||||
const state = utils.uid();
|
||||
params.state = state;
|
||||
params.redirect_uri = utils.oauth2RedirectUri;
|
||||
const authorizeUrl = utils.addQueryParams(url, params);
|
||||
|
||||
let iframeElt;
|
||||
let wnd;
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
iframeElt = document.createElement('iframe');
|
||||
iframeElt.style.position = 'absolute';
|
||||
iframeElt.style.left = '-9999px';
|
||||
iframeElt.src = authorizeUrl;
|
||||
document.body.appendChild(iframeElt);
|
||||
wnd = iframeElt.contentWindow;
|
||||
} else {
|
||||
// Open a tab otherwise
|
||||
wnd = window.open(authorizeUrl);
|
||||
if (!wnd) {
|
||||
return Promise.reject('The authorize window was blocked.');
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let checkClosedInterval;
|
||||
let closeTimeout;
|
||||
let msgHandler;
|
||||
let clean = () => {
|
||||
clearInterval(checkClosedInterval);
|
||||
if (!silent && !wnd.closed) {
|
||||
wnd.close();
|
||||
}
|
||||
if (iframeElt) {
|
||||
document.body.removeChild(iframeElt);
|
||||
}
|
||||
clearTimeout(closeTimeout);
|
||||
window.removeEventListener('message', msgHandler);
|
||||
clean = () => Promise.resolve(); // Prevent from cleaning several times
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
if (silent) {
|
||||
iframeElt.onerror = () => clean()
|
||||
.then(() => reject('Unknown error.'));
|
||||
closeTimeout = setTimeout(
|
||||
() => clean()
|
||||
.then(() => {
|
||||
isConnectionDown = true;
|
||||
store.commit('setOffline', true);
|
||||
store.commit('updateLastOfflineCheck');
|
||||
reject('You are offline.');
|
||||
}),
|
||||
networkTimeout);
|
||||
} else {
|
||||
closeTimeout = setTimeout(
|
||||
() => clean()
|
||||
.then(() => reject('Timeout.')),
|
||||
oauth2AuthorizationTimeout);
|
||||
}
|
||||
|
||||
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean()
|
||||
.then(() => {
|
||||
const data = {};
|
||||
`${event.data}`.slice(1).split('&').forEach((param) => {
|
||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||
data[key] = value;
|
||||
});
|
||||
if (data.error || data.state !== state) {
|
||||
reject('Could not get required authorization.');
|
||||
} else {
|
||||
resolve({
|
||||
accessToken: data.access_token,
|
||||
code: data.code,
|
||||
expiresIn: data.expires_in,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('message', msgHandler);
|
||||
if (!silent) {
|
||||
checkClosedInterval = setInterval(() => wnd.closed && clean()
|
||||
.then(() => reject('Authorize window was closed.')), 250);
|
||||
}
|
||||
});
|
||||
},
|
||||
request(configParam, offlineCheck = false) {
|
||||
let retryAfter = 500; // 500 ms
|
||||
const maxRetryAfter = 30 * 1000; // 30 sec
|
||||
const config = Object.assign({}, configParam);
|
||||
config.timeout = config.timeout || networkTimeout;
|
||||
config.headers = Object.assign({}, config.headers);
|
||||
if (config.body && typeof config.body === 'object') {
|
||||
config.body = JSON.stringify(config.body);
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
function parseHeaders(xhr) {
|
||||
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
||||
return pairs.reduce((headers, header) => {
|
||||
const split = header.trim().split(':');
|
||||
const key = split.shift().trim().toLowerCase();
|
||||
const value = split.join(':').trim();
|
||||
headers[key] = value;
|
||||
return headers;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function isRetriable(err) {
|
||||
if (err.status === 403) {
|
||||
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
||||
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
|
||||
}
|
||||
return err.status === 429 || (err.status >= 500 && err.status < 600);
|
||||
}
|
||||
|
||||
const attempt =
|
||||
() => new Promise((resolve, reject) => {
|
||||
if (offlineCheck) {
|
||||
store.commit('updateLastOfflineCheck');
|
||||
}
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
let timeoutId;
|
||||
|
||||
xhr.onload = () => {
|
||||
if (offlineCheck) {
|
||||
store.commit('setOffline', false);
|
||||
}
|
||||
clearTimeout(timeoutId);
|
||||
const result = {
|
||||
status: xhr.status,
|
||||
headers: parseHeaders(xhr),
|
||||
body: xhr.responseText,
|
||||
};
|
||||
if (!config.raw) {
|
||||
try {
|
||||
result.body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
reject(result);
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
if (offlineCheck) {
|
||||
store.commit('setOffline', true);
|
||||
reject('You are offline.');
|
||||
} else {
|
||||
reject('Network request failed.');
|
||||
}
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
xhr.abort();
|
||||
if (offlineCheck) {
|
||||
store.commit('setOffline', true);
|
||||
reject('You are offline.');
|
||||
} else {
|
||||
reject('Network request timeout.');
|
||||
}
|
||||
}, config.timeout);
|
||||
|
||||
const url = utils.addQueryParams(config.url, config.params);
|
||||
xhr.open(config.method || 'GET', url);
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const value = config.headers[key];
|
||||
if (value) {
|
||||
xhr.setRequestHeader(key, `${value}`);
|
||||
}
|
||||
});
|
||||
xhr.send(config.body || null);
|
||||
})
|
||||
.catch((err) => {
|
||||
// Try again later in case of retriable error
|
||||
if (isRetriable(err) && retryAfter < maxRetryAfter) {
|
||||
return new Promise(
|
||||
(resolve) => {
|
||||
setTimeout(resolve, retryAfter);
|
||||
// Exponential backoff
|
||||
retryAfter *= 2;
|
||||
})
|
||||
.then(attempt);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return attempt();
|
||||
},
|
||||
};
|
||||
|
||||
function checkOffline() {
|
||||
const isBrowserOffline = window.navigator.onLine === false;
|
||||
if (!isBrowserOffline &&
|
||||
store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() &&
|
||||
utils.isUserActive()
|
||||
) {
|
||||
store.commit('updateLastOfflineCheck');
|
||||
new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
let timeout;
|
||||
const cleaner = (cb, res) => () => {
|
||||
clearTimeout(timeout);
|
||||
cb(res);
|
||||
document.head.removeChild(script);
|
||||
};
|
||||
script.onload = cleaner(resolve);
|
||||
script.onerror = cleaner(reject);
|
||||
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
|
||||
try {
|
||||
document.head.appendChild(script); // This can fail with bad network
|
||||
timeout = setTimeout(cleaner(reject), networkTimeout);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
isConnectionDown = false;
|
||||
}, () => {
|
||||
isConnectionDown = true;
|
||||
});
|
||||
}
|
||||
const offline = isBrowserOffline || isConnectionDown;
|
||||
if (store.state.offline !== offline) {
|
||||
store.commit('setOffline', offline);
|
||||
if (offline) {
|
||||
store.dispatch('notification/error', 'You are offline.');
|
||||
} else {
|
||||
store.dispatch('notification/info', 'You are back online!');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.setInterval(checkOffline, 1000);
|
||||
window.addEventListener('online', checkOffline);
|
||||
window.addEventListener('offline', checkOffline);
|
@ -1,4 +1,4 @@
|
||||
import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
|
||||
let Dropbox;
|
||||
@ -10,7 +10,7 @@ const getAppKey = (fullAccess) => {
|
||||
return 'sw0hlixhr8q1xk0';
|
||||
};
|
||||
|
||||
const request = (token, options, args) => utils.request({
|
||||
const request = (token, options, args) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
@ -23,7 +23,7 @@ const request = (token, options, args) => utils.request({
|
||||
|
||||
export default {
|
||||
startOauth2(fullAccess, sub = null, silent = false) {
|
||||
return utils.startOauth2(
|
||||
return networkSvc.startOauth2(
|
||||
'https://www.dropbox.com/oauth2/authorize', {
|
||||
client_id: getAppKey(fullAccess),
|
||||
response_type: 'token',
|
||||
@ -54,7 +54,7 @@ export default {
|
||||
if (Dropbox) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return utils.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
|
||||
return networkSvc.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
|
||||
.then(() => {
|
||||
Dropbox = window.Dropbox;
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
|
||||
let clientId = 'cbf0cf25cfd026be23e1';
|
||||
@ -7,7 +8,7 @@ if (utils.origin === 'https://stackedit.io') {
|
||||
}
|
||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||
|
||||
const request = (token, options) => utils.request({
|
||||
const request = (token, options) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
@ -21,13 +22,13 @@ const request = (token, options) => utils.request({
|
||||
|
||||
export default {
|
||||
startOauth2(scopes, sub = null, silent = false) {
|
||||
return utils.startOauth2(
|
||||
return networkSvc.startOauth2(
|
||||
'https://github.com/login/oauth/authorize', {
|
||||
client_id: clientId,
|
||||
scope: scopes.join(' '),
|
||||
}, silent)
|
||||
// Exchange code with token
|
||||
.then(data => utils.request({
|
||||
.then(data => networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'oauth2/githubToken',
|
||||
params: {
|
||||
@ -37,7 +38,7 @@ export default {
|
||||
})
|
||||
.then(res => res.body))
|
||||
// Call the user info endpoint
|
||||
.then(accessToken => utils.request({
|
||||
.then(accessToken => networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'https://api.github.com/user',
|
||||
params: {
|
||||
|
@ -1,7 +1,9 @@
|
||||
import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
|
||||
const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
|
||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||
const appsDomain = null;
|
||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
||||
let gapi;
|
||||
@ -17,74 +19,100 @@ const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
||||
|
||||
const libraries = ['picker'];
|
||||
|
||||
const request = (token, options) => utils.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
function uploadFile(refreshedToken, name, parents, media = null, mediaType = 'text/plain', fileId = null, ifNotTooLate = cb => res => cb(res)) {
|
||||
return Promise.resolve()
|
||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||
.then(ifNotTooLate(() => {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
};
|
||||
const metadata = { name };
|
||||
if (fileId) {
|
||||
options.method = 'PATCH';
|
||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||
} else if (parents) {
|
||||
// Parents field is not patchable
|
||||
metadata.parents = parents;
|
||||
}
|
||||
if (media) {
|
||||
const boundary = `-------${utils.uid()}`;
|
||||
const delimiter = `\r\n--${boundary}\r\n`;
|
||||
const closeDelimiter = `\r\n--${boundary}--`;
|
||||
let multipartRequestBody = '';
|
||||
multipartRequestBody += delimiter;
|
||||
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
||||
multipartRequestBody += JSON.stringify(metadata);
|
||||
multipartRequestBody += delimiter;
|
||||
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
||||
multipartRequestBody += media;
|
||||
multipartRequestBody += closeDelimiter;
|
||||
options.url = options.url.replace(
|
||||
'https://www.googleapis.com/',
|
||||
'https://www.googleapis.com/upload/');
|
||||
return request(refreshedToken, {
|
||||
...options,
|
||||
params: {
|
||||
uploadType: 'multipart',
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
||||
},
|
||||
body: multipartRequestBody,
|
||||
}).then(res => res.body);
|
||||
}
|
||||
return request(refreshedToken, {
|
||||
...options,
|
||||
body: metadata,
|
||||
}).then(res => res.body);
|
||||
}));
|
||||
}
|
||||
|
||||
function downloadFile(refreshedToken, id) {
|
||||
return request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
||||
raw: true,
|
||||
}).then(res => res.body);
|
||||
}
|
||||
|
||||
export default {
|
||||
request(token, options) {
|
||||
return networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
}, true)
|
||||
.catch((err) => {
|
||||
const reason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
||||
if (reason === 'authError') {
|
||||
// Mark the token as revoked and get a new one
|
||||
store.dispatch('data/setGoogleToken', {
|
||||
...token,
|
||||
expiresOn: 0,
|
||||
});
|
||||
// Refresh token and retry
|
||||
return this.refreshToken(token.scopes, token)
|
||||
.then(refreshedToken => this.request(refreshedToken, options));
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
uploadFileInternal(refreshedToken, name, parents, media = null, mediaType = 'text/plain', fileId = null, ifNotTooLate = cb => res => cb(res)) {
|
||||
return Promise.resolve()
|
||||
// Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
|
||||
.then(ifNotTooLate(() => {
|
||||
const options = {
|
||||
method: 'POST',
|
||||
url: 'https://www.googleapis.com/drive/v3/files',
|
||||
};
|
||||
const metadata = { name };
|
||||
if (fileId) {
|
||||
options.method = 'PATCH';
|
||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||
} else if (parents) {
|
||||
// Parents field is not patchable
|
||||
metadata.parents = parents;
|
||||
}
|
||||
if (media) {
|
||||
const boundary = `-------${utils.uid()}`;
|
||||
const delimiter = `\r\n--${boundary}\r\n`;
|
||||
const closeDelimiter = `\r\n--${boundary}--`;
|
||||
let multipartRequestBody = '';
|
||||
multipartRequestBody += delimiter;
|
||||
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
||||
multipartRequestBody += JSON.stringify(metadata);
|
||||
multipartRequestBody += delimiter;
|
||||
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
||||
multipartRequestBody += media;
|
||||
multipartRequestBody += closeDelimiter;
|
||||
options.url = options.url.replace(
|
||||
'https://www.googleapis.com/',
|
||||
'https://www.googleapis.com/upload/');
|
||||
return this.request(refreshedToken, {
|
||||
...options,
|
||||
params: {
|
||||
uploadType: 'multipart',
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
||||
},
|
||||
body: multipartRequestBody,
|
||||
}).then(res => res.body);
|
||||
}
|
||||
return this.request(refreshedToken, {
|
||||
...options,
|
||||
body: metadata,
|
||||
}).then(res => res.body);
|
||||
}));
|
||||
},
|
||||
downloadFileInternal(refreshedToken, id) {
|
||||
return this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
||||
raw: true,
|
||||
}).then(res => res.body);
|
||||
},
|
||||
getUser(userId) {
|
||||
return networkSvc.request({
|
||||
method: 'GET',
|
||||
url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`,
|
||||
}, true)
|
||||
.then((res) => {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: res.body.id,
|
||||
imageUrl: res.body.image.url,
|
||||
});
|
||||
return res.body;
|
||||
});
|
||||
},
|
||||
startOauth2(scopes, sub = null, silent = false) {
|
||||
return utils.startOauth2(
|
||||
return networkSvc.startOauth2(
|
||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
@ -94,13 +122,13 @@ export default {
|
||||
prompt: silent ? 'none' : null,
|
||||
}, silent)
|
||||
// Call the token info endpoint
|
||||
.then(data => utils.request({
|
||||
.then(data => networkSvc.request({
|
||||
method: 'POST',
|
||||
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
||||
params: {
|
||||
access_token: data.accessToken,
|
||||
},
|
||||
}).then((res) => {
|
||||
}, true).then((res) => {
|
||||
// Check the returned client ID consistency
|
||||
if (res.body.aud !== clientId) {
|
||||
throw new Error('Client ID inconsistent.');
|
||||
@ -125,29 +153,27 @@ export default {
|
||||
};
|
||||
}))
|
||||
// Call the user info endpoint
|
||||
.then(token => request(token, {
|
||||
method: 'GET',
|
||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
||||
}).then((res) => {
|
||||
// Add name to token
|
||||
token.name = res.body.displayName;
|
||||
const existingToken = store.getters['data/googleTokens'][token.sub];
|
||||
if (existingToken) {
|
||||
// We probably retrieved a new token with restricted scopes.
|
||||
// That's no problem, token will be refreshed later with merged scopes.
|
||||
// Save flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
// Save nextPageToken
|
||||
token.nextPageToken = existingToken.nextPageToken;
|
||||
}
|
||||
// Add token to googleTokens
|
||||
store.dispatch('data/setGoogleToken', token);
|
||||
return token;
|
||||
}));
|
||||
.then(token => this.getUser(token.sub)
|
||||
.then((user) => {
|
||||
// Add name to token
|
||||
token.name = user.displayName;
|
||||
const existingToken = store.getters['data/googleTokens'][token.sub];
|
||||
if (existingToken) {
|
||||
// We probably retrieved a new token with restricted scopes.
|
||||
// That's no problem, token will be refreshed later with merged scopes.
|
||||
// Save flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
// Save nextPageToken
|
||||
token.nextPageToken = existingToken.nextPageToken;
|
||||
}
|
||||
// Add token to googleTokens
|
||||
store.dispatch('data/setGoogleToken', token);
|
||||
return token;
|
||||
}));
|
||||
},
|
||||
refreshToken(scopes, token) {
|
||||
const sub = token.sub;
|
||||
@ -168,18 +194,22 @@ export default {
|
||||
// Try to get a new token in background
|
||||
return this.startOauth2(mergedScopes, sub, true)
|
||||
// If it fails try to popup a window
|
||||
.catch(() => utils.checkOnline() // Check that we are online, silent mode is a hack
|
||||
.then(() => store.dispatch('modal/providerRedirection', {
|
||||
.catch((err) => {
|
||||
if (store.state.offline) {
|
||||
throw err;
|
||||
}
|
||||
return store.dispatch('modal/providerRedirection', {
|
||||
providerName: 'Google',
|
||||
onResolve: () => this.startOauth2(mergedScopes, sub),
|
||||
})));
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
loadClientScript() {
|
||||
if (gapi) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return utils.loadScript('https://apis.google.com/js/api.js')
|
||||
return networkSvc.loadScript('https://apis.google.com/js/api.js')
|
||||
.then(() => Promise.all(libraries.map(
|
||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||
callback: resolve,
|
||||
@ -210,7 +240,7 @@ export default {
|
||||
};
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then((refreshedToken) => {
|
||||
const getPage = (pageToken = '1') => request(refreshedToken, {
|
||||
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
||||
method: 'GET',
|
||||
url: 'https://www.googleapis.com/drive/v3/changes',
|
||||
params: {
|
||||
@ -233,26 +263,26 @@ export default {
|
||||
},
|
||||
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
.then(refreshedToken => uploadFile(
|
||||
.then(refreshedToken => this.uploadFileInternal(
|
||||
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
||||
},
|
||||
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then(refreshedToken => uploadFile(
|
||||
.then(refreshedToken => this.uploadFileInternal(
|
||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||
},
|
||||
downloadFile(token, id) {
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||
},
|
||||
downloadAppDataFile(token, id) {
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
||||
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||
},
|
||||
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
||||
return this.refreshToken(driveAppDataScopes, token)
|
||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||
.then(ifNotTooLate(refreshedToken => request(refreshedToken, {
|
||||
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
||||
method: 'DELETE',
|
||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||
})));
|
||||
@ -266,7 +296,7 @@ export default {
|
||||
if (blogId) {
|
||||
return blogId;
|
||||
}
|
||||
return request(refreshedToken, {
|
||||
return this.request(refreshedToken, {
|
||||
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
|
||||
params: {
|
||||
url: blogUrl,
|
||||
@ -299,7 +329,7 @@ export default {
|
||||
options.url += postId;
|
||||
options.body.id = postId;
|
||||
}
|
||||
return request(refreshedToken, options)
|
||||
return this.request(refreshedToken, options)
|
||||
.then(res => res.body);
|
||||
})
|
||||
.then((post) => {
|
||||
@ -319,7 +349,7 @@ export default {
|
||||
options.params.publishDate = published.toISOString();
|
||||
}
|
||||
}
|
||||
return request(refreshedToken, options)
|
||||
return this.request(refreshedToken, options)
|
||||
.then(res => res.body);
|
||||
}));
|
||||
},
|
||||
|
@ -1,10 +1,10 @@
|
||||
import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
|
||||
const clientId = '23361';
|
||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
||||
|
||||
const request = (token, options) => utils.request({
|
||||
const request = (token, options) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
@ -14,7 +14,7 @@ const request = (token, options) => utils.request({
|
||||
|
||||
export default {
|
||||
startOauth2(sub = null, silent = false) {
|
||||
return utils.startOauth2(
|
||||
return networkSvc.startOauth2(
|
||||
'https://public-api.wordpress.com/oauth2/authorize', {
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
|
@ -1,7 +1,7 @@
|
||||
import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
|
||||
const request = (token, options) => utils.request({
|
||||
const request = (token, options) => networkSvc.request({
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers || {},
|
||||
@ -11,7 +11,7 @@ const request = (token, options) => utils.request({
|
||||
|
||||
export default {
|
||||
startOauth2(subdomain, clientId, sub = null, silent = false) {
|
||||
return utils.startOauth2(
|
||||
return networkSvc.startOauth2(
|
||||
`https://${subdomain}.zendesk.com/oauth/authorizations/new`, {
|
||||
client_id: clientId,
|
||||
response_type: 'token',
|
||||
|
@ -35,15 +35,6 @@ window.addEventListener('focus', setLastFocus);
|
||||
// For addQueryParams()
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
||||
// For loadScript()
|
||||
const scriptLoadingPromises = Object.create(null);
|
||||
|
||||
// For startOauth2
|
||||
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
|
||||
|
||||
// For checkOnline
|
||||
const checkOnlineTimeout = 15 * 1000; // 15 sec
|
||||
|
||||
export default {
|
||||
workspaceId,
|
||||
origin,
|
||||
@ -142,216 +133,4 @@ export default {
|
||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||
return urlParser.href;
|
||||
},
|
||||
loadScript(url) {
|
||||
if (!scriptLoadingPromises[url]) {
|
||||
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script');
|
||||
script.onload = resolve;
|
||||
script.onerror = () => {
|
||||
scriptLoadingPromises[url] = null;
|
||||
reject();
|
||||
};
|
||||
script.src = url;
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
return scriptLoadingPromises[url];
|
||||
},
|
||||
startOauth2(url, params = {}, silent = false) {
|
||||
const oauth2Context = {};
|
||||
|
||||
// Build the authorize URL
|
||||
const state = this.uid();
|
||||
params.state = state;
|
||||
params.redirect_uri = this.oauth2RedirectUri;
|
||||
const authorizeUrl = this.addQueryParams(url, params);
|
||||
if (silent) {
|
||||
// Use an iframe as wnd for silent mode
|
||||
oauth2Context.iframeElt = document.createElement('iframe');
|
||||
oauth2Context.iframeElt.style.position = 'absolute';
|
||||
oauth2Context.iframeElt.style.left = '-9999px';
|
||||
oauth2Context.closeTimeout = setTimeout(
|
||||
() => oauth2Context.clean('Unknown error.'),
|
||||
checkOnlineTimeout);
|
||||
oauth2Context.iframeElt.onerror = () => oauth2Context.clean('Unknown error.');
|
||||
oauth2Context.iframeElt.src = authorizeUrl;
|
||||
document.body.appendChild(oauth2Context.iframeElt);
|
||||
oauth2Context.wnd = oauth2Context.iframeElt.contentWindow;
|
||||
} else {
|
||||
// Open a new tab otherwise
|
||||
oauth2Context.wnd = window.open(authorizeUrl);
|
||||
// If window opening has been blocked by the browser
|
||||
if (!oauth2Context.wnd) {
|
||||
return Promise.reject();
|
||||
}
|
||||
oauth2Context.closeTimeout = setTimeout(
|
||||
() => oauth2Context.clean('Timeout.'),
|
||||
oauth2AuthorizationTimeout);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
oauth2Context.clean = (errorMsg) => {
|
||||
clearInterval(oauth2Context.checkClosedInterval);
|
||||
if (!silent && !oauth2Context.wnd.closed) {
|
||||
oauth2Context.wnd.close();
|
||||
}
|
||||
if (oauth2Context.iframeElt) {
|
||||
document.body.removeChild(oauth2Context.iframeElt);
|
||||
}
|
||||
clearTimeout(oauth2Context.closeTimeout);
|
||||
window.removeEventListener('message', oauth2Context.msgHandler);
|
||||
oauth2Context.clean = () => {
|
||||
// Prevent from cleaning several times
|
||||
};
|
||||
if (errorMsg) {
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
};
|
||||
|
||||
oauth2Context.msgHandler = (event) => {
|
||||
if (event.source === oauth2Context.wnd &&
|
||||
event.origin === this.origin
|
||||
) {
|
||||
oauth2Context.clean();
|
||||
const data = {};
|
||||
`${event.data}`.slice(1).split('&').forEach((param) => {
|
||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||
if (key === 'state') {
|
||||
data.state = value;
|
||||
} else if (key === 'access_token') {
|
||||
data.accessToken = value;
|
||||
} else if (key === 'code') {
|
||||
data.code = value;
|
||||
} else if (key === 'expires_in') {
|
||||
data.expiresIn = value;
|
||||
}
|
||||
});
|
||||
if (data.state === state) {
|
||||
resolve(data);
|
||||
return;
|
||||
}
|
||||
reject('Could not get required authorization.');
|
||||
}
|
||||
};
|
||||
window.addEventListener('message', oauth2Context.msgHandler);
|
||||
|
||||
oauth2Context.checkClosedInterval = !silent && setInterval(() => {
|
||||
if (oauth2Context.wnd.closed) {
|
||||
oauth2Context.clean('Authorize window was closed.');
|
||||
}
|
||||
}, 250);
|
||||
});
|
||||
},
|
||||
request(configParam) {
|
||||
let retryAfter = 500; // 500 ms
|
||||
const maxRetryAfter = 30 * 1000; // 30 sec
|
||||
const config = Object.assign({}, configParam);
|
||||
config.timeout = config.timeout || 30 * 1000; // 30 sec
|
||||
config.headers = Object.assign({}, config.headers);
|
||||
if (config.body && typeof config.body === 'object') {
|
||||
config.body = JSON.stringify(config.body);
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
|
||||
function parseHeaders(xhr) {
|
||||
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
||||
return pairs.reduce((headers, header) => {
|
||||
const split = header.trim().split(':');
|
||||
const key = split.shift().trim().toLowerCase();
|
||||
const value = split.join(':').trim();
|
||||
headers[key] = value;
|
||||
return headers;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function isRetriable(err) {
|
||||
switch (err.status) {
|
||||
case 403:
|
||||
{
|
||||
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
||||
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
|
||||
}
|
||||
case 429:
|
||||
return true;
|
||||
default:
|
||||
if (err.status >= 500 && err.status < 600) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const attempt =
|
||||
() => new Promise((resolve, reject) => {
|
||||
const xhr = new window.XMLHttpRequest();
|
||||
let timeoutId;
|
||||
|
||||
xhr.onload = () => {
|
||||
clearTimeout(timeoutId);
|
||||
const result = {
|
||||
status: xhr.status,
|
||||
headers: parseHeaders(xhr),
|
||||
body: xhr.responseText,
|
||||
};
|
||||
if (!config.raw) {
|
||||
try {
|
||||
result.body = JSON.parse(result.body);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (result.status >= 200 && result.status < 300) {
|
||||
resolve(result);
|
||||
return;
|
||||
}
|
||||
reject(result);
|
||||
};
|
||||
|
||||
xhr.onerror = () => {
|
||||
clearTimeout(timeoutId);
|
||||
reject(new Error('Network request failed.'));
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
xhr.abort();
|
||||
reject(new Error('Network request timeout.'));
|
||||
}, config.timeout);
|
||||
|
||||
const url = this.addQueryParams(config.url, config.params);
|
||||
xhr.open(config.method || 'GET', url);
|
||||
Object.keys(config.headers).forEach((key) => {
|
||||
const value = config.headers[key];
|
||||
if (value) {
|
||||
xhr.setRequestHeader(key, `${value}`);
|
||||
}
|
||||
});
|
||||
xhr.send(config.body || null);
|
||||
})
|
||||
.catch((err) => {
|
||||
// Try again later in case of retriable error
|
||||
if (isRetriable(err) && retryAfter < maxRetryAfter) {
|
||||
return new Promise(
|
||||
(resolve) => {
|
||||
setTimeout(resolve, retryAfter);
|
||||
// Exponential backoff
|
||||
retryAfter *= 2;
|
||||
})
|
||||
.then(attempt);
|
||||
}
|
||||
throw err;
|
||||
});
|
||||
|
||||
return attempt();
|
||||
},
|
||||
checkOnline() {
|
||||
const checkStatus = (res) => {
|
||||
if (!res.status || res.status < 200) {
|
||||
throw new Error('Offline...');
|
||||
}
|
||||
};
|
||||
return this.request({
|
||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
||||
timeout: checkOnlineTimeout,
|
||||
})
|
||||
.then(checkStatus, checkStatus);
|
||||
},
|
||||
};
|
||||
|
@ -15,15 +15,17 @@ import explorer from './modules/explorer';
|
||||
import modal from './modules/modal';
|
||||
import notification from './modules/notification';
|
||||
import queue from './modules/queue';
|
||||
import userInfo from './modules/userInfo';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
const debug = NODE_ENV !== 'production';
|
||||
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
ready: false,
|
||||
offline: false,
|
||||
lastOfflineCheck: 0,
|
||||
},
|
||||
getters: {
|
||||
allItemMap: (state) => {
|
||||
@ -39,6 +41,21 @@ const store = new Vuex.Store({
|
||||
setOffline: (state, value) => {
|
||||
state.offline = value;
|
||||
},
|
||||
updateLastOfflineCheck: (state) => {
|
||||
state.lastOfflineCheck = Date.now();
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setOffline: ({ state, commit }, value) => {
|
||||
if (state.offline !== value) {
|
||||
commit('setOffline', value);
|
||||
if (state.offline) {
|
||||
return Promise.reject('You are offline.');
|
||||
}
|
||||
store.dispatch('notification/info', 'You are back online!');
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
},
|
||||
modules: {
|
||||
contentState,
|
||||
@ -54,37 +71,10 @@ const store = new Vuex.Store({
|
||||
modal,
|
||||
notification,
|
||||
queue,
|
||||
userInfo,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
});
|
||||
|
||||
let isConnectionDown = false;
|
||||
let lastConnectionCheck = 0;
|
||||
|
||||
function checkOffline() {
|
||||
const isBrowserOffline = window.navigator.onLine === false;
|
||||
if (!isBrowserOffline && lastConnectionCheck + 30000 < Date.now() && utils.isUserActive()) {
|
||||
lastConnectionCheck = Date.now();
|
||||
utils.checkOnline()
|
||||
.then(() => {
|
||||
isConnectionDown = false;
|
||||
}, () => {
|
||||
isConnectionDown = true;
|
||||
});
|
||||
}
|
||||
const isOffline = isBrowserOffline || isConnectionDown;
|
||||
if (isOffline !== store.state.offline) {
|
||||
store.commit('setOffline', isOffline);
|
||||
if (isOffline) {
|
||||
store.dispatch('notification/info', 'You are offline.');
|
||||
} else {
|
||||
store.dispatch('notification/info', 'You are back online!');
|
||||
}
|
||||
}
|
||||
}
|
||||
utils.setInterval(checkOffline, 1000);
|
||||
window.addEventListener('online', checkOffline);
|
||||
window.addEventListener('offline', checkOffline);
|
||||
|
||||
export default store;
|
||||
|
@ -16,7 +16,7 @@ const constants = {
|
||||
explorerWidth: 250,
|
||||
sideBarWidth: 280,
|
||||
navigationBarHeight: 44,
|
||||
buttonBarWidth: 30,
|
||||
buttonBarWidth: 26,
|
||||
statusBarHeight: 20,
|
||||
};
|
||||
|
||||
@ -99,15 +99,13 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
if (styles.showEditor) {
|
||||
const syncLocations = getters['syncLocation/current'];
|
||||
const publishLocations = getters['publishLocation/current'];
|
||||
const isSyncPossible = getters['data/loginToken'] || syncLocations.length;
|
||||
styles.titleMaxWidth = styles.innerWidth -
|
||||
navigationBarEditButtonsWidth -
|
||||
navigationBarLeftButtonWidth -
|
||||
navigationBarRightButtonWidth -
|
||||
navigationBarSpinnerWidth -
|
||||
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
||||
(isSyncPossible ? navigationBarSyncPublishButtonsWidth : 0) -
|
||||
(publishLocations.length ? navigationBarSyncPublishButtonsWidth : 0) -
|
||||
(navigationBarSyncPublishButtonsWidth * 2) -
|
||||
navigationBarTitleMargin;
|
||||
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
||||
styles.hideLocations = true;
|
||||
|
13
src/store/modules/userInfo.js
Normal file
13
src/store/modules/userInfo.js
Normal file
@ -0,0 +1,13 @@
|
||||
import Vue from 'vue';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
itemMap: {},
|
||||
},
|
||||
mutations: {
|
||||
addItem: (state, item) => {
|
||||
Vue.set(state.itemMap, item.id, item);
|
||||
},
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user