Added account management modal

This commit is contained in:
Benoit Schweblin 2019-06-16 14:49:50 +01:00
parent 91f8cf3c10
commit 8cf0b87f5f
21 changed files with 445 additions and 64 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
node_modules/ node_modules/
dist/ dist/
.history .history
.idea
npm-debug.log* npm-debug.log*
.vscode .vscode
stackedit_v4 stackedit_v4

13
src/assets/iconGoogle.svg Normal file
View File

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
<defs>
<path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
</defs>
<clipPath id="b">
<use xlink:href="#a" overflow="visible"/>
</clipPath>
<path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
<path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
<path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
<path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
</svg>

After

Width:  |  Height:  |  Size: 729 B

View File

@ -36,6 +36,7 @@ import ImageModal from './modals/ImageModal';
import SyncManagementModal from './modals/SyncManagementModal'; import SyncManagementModal from './modals/SyncManagementModal';
import PublishManagementModal from './modals/PublishManagementModal'; import PublishManagementModal from './modals/PublishManagementModal';
import WorkspaceManagementModal from './modals/WorkspaceManagementModal'; import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
import AccountManagementModal from './modals/AccountManagementModal';
import SponsorModal from './modals/SponsorModal'; import SponsorModal from './modals/SponsorModal';
// Providers // Providers
@ -86,6 +87,7 @@ export default {
SyncManagementModal, SyncManagementModal,
PublishManagementModal, PublishManagementModal,
WorkspaceManagementModal, WorkspaceManagementModal,
AccountManagementModal,
SponsorModal, SponsorModal,
// Providers // Providers
GooglePhotoModal, GooglePhotoModal,

View File

@ -56,7 +56,6 @@ const panelNames = {
history: 'File history', history: 'File history',
export: 'Export to disk', export: 'Export to disk',
import: 'Import from disk', import: 'Import from disk',
more: 'More',
}; };
export default { export default {

View File

@ -88,8 +88,44 @@
Print Print
</menu-entry> </menu-entry>
<hr> <hr>
<menu-entry @click.native="setPanel('more')"> <menu-entry @click.native="settings">
More... <icon-settings slot="icon"></icon-settings>
<div>Settings</div>
<span>Tweak application and keyboard shortcuts.</span>
</menu-entry>
<menu-entry @click.native="templates">
<icon-code-braces slot="icon"></icon-code-braces>
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
<span>Configure Handlebars templates for your exports.</span>
</menu-entry>
<menu-entry @click.native="accounts">
<icon-key slot="icon"></icon-key>
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
<span>Manage access to your external accounts.</span>
</menu-entry>
<hr>
<menu-entry @click.native="exportWorkspace">
<icon-content-save slot="icon"></icon-content-save>
Export workspace backup
</menu-entry>
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
<div class="menu-entry__icon flex flex--column flex--center">
<icon-content-save></icon-content-save>
</div>
<div class="flex flex--column">
Import workspace backup
</div>
</label>
<menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout>
<div>Reset application</div>
<span>Sign out and clean all workspaces.</span>
</menu-entry>
<hr>
<menu-entry @click.native="about">
<icon-help-circle slot="icon"></icon-help-circle>
About StackEdit
</menu-entry> </menu-entry>
</div> </div>
</template> </template>
@ -102,6 +138,8 @@ import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper'; import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc'; import syncSvc from '../../services/syncSvc';
import userSvc from '../../services/userSvc'; import userSvc from '../../services/userSvc';
import backupSvc from '../../services/backupSvc';
import utils from '../../services/utils';
import store from '../../store'; import store from '../../store';
export default { export default {
@ -131,6 +169,13 @@ export default {
publishLocationCount() { publishLocationCount() {
return Object.keys(store.getters['publishLocation/current']).length; return Object.keys(store.getters['publishLocation/current']).length;
}, },
templateCount() {
return Object.keys(store.getters['data/allTemplatesById']).length;
},
accountCount() {
return Object.values(store.getters['data/tokensByType'])
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
},
}, },
methods: { methods: {
...mapActions('data', { ...mapActions('data', {
@ -154,6 +199,64 @@ export default {
print() { print() {
window.print(); window.print();
}, },
onImportBackup(evt) {
const file = evt.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
if (text.match(/\uFFFD/)) {
store.dispatch('notification/error', 'File is not readable.');
} else {
backupSvc.importBackup(text);
}
};
const blob = file.slice(0, 10000000);
reader.readAsText(blob);
}
},
exportWorkspace() {
window.location.href = utils.addQueryParams('app', {
...utils.queryParams,
exportWorkspace: true,
}, true);
window.location.reload();
},
async settings() {
try {
const settings = await store.dispatch('modal/open', 'settings');
store.dispatch('data/setSettings', settings);
} catch (e) {
// Cancel
}
},
async templates() {
try {
const { templates } = await store.dispatch('modal/open', 'templates');
store.dispatch('data/setTemplatesById', templates);
} catch (e) {
// Cancel
}
},
async accounts() {
try {
await store.dispatch('modal/open', 'accountManagement');
} catch (e) {
// Cancel
}
},
async reset() {
try {
await store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true';
window.location.reload();
} catch (e) {
// Cancel
}
},
about() {
store.dispatch('modal/open', 'about');
},
}, },
}; };
</script> </script>

View File

@ -1,23 +1,23 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="side-bar__info" v-if="isCurrentTemp"> <div class="side-bar__info" v-if="isCurrentTemp">
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p> <p>{{currentFileName}} can't be published as it's a temporary file.</p>
</div> </div>
<div v-else> <div v-else>
<div class="side-bar__info" v-if="noToken"> <div class="side-bar__info" v-if="noToken">
<p>You have to <b>link an account</b> to start publishing files.</p> <p>You have to <b>link an account</b> to start publishing files.</p>
</div> </div>
<div class="side-bar__info" v-if="publishLocations.length"> <div class="side-bar__info" v-if="publishLocations.length">
<p><b>{{currentFileName}}</b> is already published.</p> <p>{{currentFileName}} is already published.</p>
<menu-entry @click.native="requestPublish"> <menu-entry @click.native="requestPublish">
<icon-upload slot="icon"></icon-upload> <icon-upload slot="icon"></icon-upload>
<div>Publish now</div> <div>Publish now</div>
<span>Update current file publications.</span> <span>Update publications for {{currentFileName}}.</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="managePublish"> <menu-entry @click.native="managePublish">
<icon-view-list slot="icon"></icon-view-list> <icon-view-list slot="icon"></icon-view-list>
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div> <div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
<span>Manage current file publication locations.</span> <span>Manage publication locations for {{currentFileName}}.</span>
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
@ -157,7 +157,7 @@ export default {
return Object.keys(this.publishLocations).length; return Object.keys(this.publishLocations).length;
}, },
currentFileName() { currentFileName() {
return store.getters['file/current'].name; return `"${store.getters['file/current'].name}"`;
}, },
bloggerTokens() { bloggerTokens() {
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger); return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger);

View File

@ -1,14 +1,14 @@
<template> <template>
<div class="side-bar__panel side-bar__panel--menu"> <div class="side-bar__panel side-bar__panel--menu">
<div class="side-bar__info" v-if="isCurrentTemp"> <div class="side-bar__info" v-if="isCurrentTemp">
<p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p> <p>{{currentFileName}} can't be synced as it's a temporary file.</p>
</div> </div>
<div v-else> <div v-else>
<div class="side-bar__info" v-if="noToken"> <div class="side-bar__info" v-if="noToken">
<p>You have to <b>link an account</b> to start syncing files.</p> <p>You have to <b>link an account</b> to start syncing files.</p>
</div> </div>
<div class="side-bar__info" v-if="syncLocations.length"> <div class="side-bar__info" v-if="syncLocations.length">
<p><b>{{currentFileName}}</b> is already synchronized.</p> <p>{{currentFileName}} is already synchronized.</p>
<menu-entry @click.native="requestSync"> <menu-entry @click.native="requestSync">
<icon-sync slot="icon"></icon-sync> <icon-sync slot="icon"></icon-sync>
<div>Synchronize now</div> <div>Synchronize now</div>
@ -17,7 +17,7 @@
<menu-entry @click.native="manageSync"> <menu-entry @click.native="manageSync">
<icon-view-list slot="icon"></icon-view-list> <icon-view-list slot="icon"></icon-view-list>
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div> <div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
<span>Manage current file synchronized locations.</span> <span>Manage synchronized locations for {{currentFileName}}.</span>
</menu-entry> </menu-entry>
</div> </div>
<hr> <hr>
@ -139,7 +139,7 @@ export default {
return Object.keys(this.syncLocations).length; return Object.keys(this.syncLocations).length;
}, },
currentFileName() { currentFileName() {
return store.getters['file/current'].name; return `"${store.getters['file/current'].name}"`;
}, },
dropboxTokens() { dropboxTokens() {
return tokensToArray(store.getters['data/dropboxTokensBySub']); return tokensToArray(store.getters['data/dropboxTokensBySub']);

View File

@ -9,19 +9,19 @@
<hr> <hr>
<menu-entry @click.native="addCouchdbWorkspace"> <menu-entry @click.native="addCouchdbWorkspace">
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
<span>Add a <b>CouchDB</b> backed workspace</span> <span>Add a <b>CouchDB</b> workspace</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGithubWorkspace"> <menu-entry @click.native="addGithubWorkspace">
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
<span>Add a <b>GitHub</b> backed workspace</span> <span>Add a <b>GitHub</b> workspace</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGitlabWorkspace"> <menu-entry @click.native="addGitlabWorkspace">
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
<span>Add a <b>GitLab</b> backed workspace</span> <span>Add a <b>GitLab</b> workspace</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="addGoogleDriveWorkspace"> <menu-entry @click.native="addGoogleDriveWorkspace">
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider> <icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
<span>Add a <b>Google Drive</b> backed workspace</span> <span>Add a <b>Google Drive</b> workspace</span>
</menu-entry> </menu-entry>
<menu-entry @click.native="manageWorkspaces"> <menu-entry @click.native="manageWorkspaces">
<icon-database slot="icon"></icon-database> <icon-database slot="icon"></icon-database>

View File

@ -2,7 +2,7 @@
<modal-inner class="modal__inner-1--about-modal" aria-label="About"> <modal-inner class="modal__inner-1--about-modal" aria-label="About">
<div class="modal__content"> <div class="modal__content">
<div class="logo-background"></div> <div class="logo-background"></div>
<small>© 2013-2018 Dock5 Software Ltd.<br>v{{version}}</small> <small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
<hr> <hr>
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a> StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
<br> <br>

View File

@ -0,0 +1,267 @@
<template>
<modal-inner class="modal__inner-1--account-management" aria-label="Manage external accounts">
<div class="modal__content">
<div class="modal__image">
<icon-key></icon-key>
</div>
<p v-if="entries.length">StackEdit has access to the following external accounts:</p>
<p v-else>StackEdit has no access to any external account yet.</p>
<div>
<div class="account-entry flex flex--column" v-for="entry in entries" :key="entry.token.sub">
<div class="account-entry__header flex flex--row flex--align-center">
<div class="account-entry__icon flex flex--column flex--center">
<icon-provider :provider-id="entry.providerId"></icon-provider>
</div>
<div class="account-entry__description">
{{entry.name}}
</div>
<div class="account-entry__buttons flex flex--row flex--center">
<button class="account-entry__button button" @click="remove(entry)" v-title="'Remove access'">
<icon-delete></icon-delete>
</button>
</div>
</div>
<div class="account-entry__row">
<span v-if="entry.userId">
<b>User ID:</b>
{{entry.userId}}
</span>
<span v-if="entry.url">
<b>URL:</b>
{{entry.url}}
</span>
<span v-if="entry.scopes">
<b>Scopes:</b>
{{entry.scopes.join(', ')}}
</span>
</div>
</div>
</div>
<menu-entry @click.native="addBloggerAccount">
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
<span>Add Blogger account</span>
</menu-entry>
<menu-entry @click.native="addDropboxAccount">
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
<span>Add Dropbox account</span>
</menu-entry>
<menu-entry @click.native="addGithubAccount">
<icon-provider slot="icon" provider-id="github"></icon-provider>
<span>Add GitHub account</span>
</menu-entry>
<menu-entry @click.native="addGitlabAccount">
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
<span>Add GitLab account</span>
</menu-entry>
<menu-entry @click.native="addGoogleDriveAccount">
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
<span>Add Google Drive account</span>
</menu-entry>
<menu-entry @click.native="addGooglePhotosAccount">
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
<span>Add Google Photos account</span>
</menu-entry>
<menu-entry @click.native="addWordpressAccount">
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
<span>Add WordPress account</span>
</menu-entry>
<menu-entry @click.native="addZendeskAccount">
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
<span>Add Zendesk account</span>
</menu-entry>
</div>
<div class="modal__button-bar">
<button class="button button--resolve" @click="config.resolve()">Close</button>
</div>
</modal-inner>
</template>
<script>
import { mapGetters } from 'vuex';
import ModalInner from './common/ModalInner';
import MenuEntry from '../menus/common/MenuEntry';
import store from '../../store';
import utils from '../../services/utils';
import googleHelper from '../../services/providers/helpers/googleHelper';
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
import githubHelper from '../../services/providers/helpers/githubHelper';
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
export default {
components: {
ModalInner,
MenuEntry,
},
computed: {
...mapGetters('modal', [
'config',
]),
entries() {
return [
...Object.values(store.getters['data/googleTokensBySub']).map(token => ({
token,
providerId: 'google',
userId: token.sub,
name: token.name,
scopes: ['openid', 'profile', ...token.scopes
.map(scope => scope.replace(/^https:\/\/www.googleapis.com\/auth\//, ''))],
})),
...Object.values(store.getters['data/couchdbTokensBySub']).map(token => ({
token,
providerId: 'couchdb',
url: token.dbUrl,
name: token.name,
})),
...Object.values(store.getters['data/dropboxTokensBySub']).map(token => ({
token,
providerId: 'dropbox',
userId: token.sub,
name: token.name,
})),
...Object.values(store.getters['data/githubTokensBySub']).map(token => ({
token,
providerId: 'github',
userId: token.sub,
name: token.name,
scopes: token.scopes,
})),
...Object.values(store.getters['data/gitlabTokensBySub']).map(token => ({
token,
providerId: 'gitlab',
url: token.serverUrl,
userId: token.sub,
name: token.name,
scopes: ['api'],
})),
...Object.values(store.getters['data/wordpressTokensBySub']).map(token => ({
token,
providerId: 'wordpress',
userId: token.sub,
name: token.name,
scopes: ['global'],
})),
...Object.values(store.getters['data/zendeskTokensBySub']).map(token => ({
token,
providerId: 'zendesk',
url: `https://${token.subdomain}.zendesk.com/`,
userId: token.sub,
name: token.name,
scopes: ['read', 'hc:write'],
})),
];
},
},
methods: {
async remove(entry) {
const tokensBySub = utils.deepCopy(store.getters[`data/${entry.providerId}TokensBySub`]);
delete tokensBySub[entry.token.sub];
await store.dispatch('data/patchTokensByType', {
[entry.providerId]: tokensBySub,
});
},
async addBloggerAccount() {
try {
await googleHelper.addBloggerAccount();
} catch (e) { /* cancel */ }
},
async addDropboxAccount() {
try {
await store.dispatch('modal/open', { type: 'dropboxAccount' });
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
} catch (e) { /* cancel */ }
},
async addGithubAccount() {
try {
await store.dispatch('modal/open', { type: 'githubAccount' });
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
} catch (e) { /* cancel */ }
},
async addGitlabAccount() {
try {
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
await gitlabHelper.addAccount(serverUrl, applicationId);
} catch (e) { /* cancel */ }
},
async addGoogleDriveAccount() {
try {
await store.dispatch('modal/open', { type: 'googleDriveAccount' });
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
} catch (e) { /* cancel */ }
},
async addGooglePhotosAccount() {
try {
await googleHelper.addPhotosAccount();
} catch (e) { /* cancel */ }
},
async addWordpressAccount() {
try {
await wordpressHelper.addAccount();
} catch (e) { /* cancel */ }
},
async addZendeskAccount() {
try {
const { subdomain, clientId } = await store.dispatch('modal/open', { type: 'zendeskAccount' });
await zendeskHelper.addAccount(subdomain, clientId);
} catch (e) { /* cancel */ }
},
},
};
</script>
<style lang="scss">
@import '../../styles/variables.scss';
.account-entry {
margin: 1.5em 0;
height: auto;
font-size: 17px;
line-height: 1.5;
}
$button-size: 30px;
.account-entry__header {
line-height: $button-size;
}
.account-entry__row {
border-top: 1px solid $hr-color;
opacity: 0.5;
font-size: 0.67em;
}
.account-entry__icon {
height: 22px;
width: 22px;
margin-right: 0.75rem;
flex: none;
}
.account-entry__description {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.account-entry__buttons {
margin-left: 0.75rem;
}
.account-entry__button {
width: $button-size;
height: $button-size;
padding: 4px;
background-color: transparent;
opacity: 0.75;
&:active,
&:focus,
&:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.1);
}
}
</style>

View File

@ -58,8 +58,10 @@ export default modalTemplate({
this.config.reject(); this.config.reject();
callback(null); callback(null);
}, },
addGooglePhotosAccount() { async addGooglePhotosAccount() {
return googleHelper.addPhotosAccount(); try {
await googleHelper.addPhotosAccount();
} catch (e) { /* cancel */ }
}, },
async openGooglePhotos(token) { async openGooglePhotos(token) {
const { callback } = this.config; const { callback } = this.config;

View File

@ -95,9 +95,7 @@ $small-button-size: 22px;
} }
.publish-entry__row { .publish-entry__row {
margin-top: 1px; border-top: 1px solid $hr-color;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size; line-height: $small-button-size;
} }

View File

@ -99,9 +99,7 @@ $small-button-size: 22px;
} }
.sync-entry__row { .sync-entry__row {
margin-top: 1px; border-top: 1px solid $hr-color;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size; line-height: $small-button-size;
} }

View File

@ -51,7 +51,7 @@
</div> </div>
</div> </div>
<div class="modal__info"> <div class="modal__info">
<b>ProTip:</b> A workspace is accessible <b>offline</b> once it has been opened for the first time. <b>ProTip:</b> Workspaces are accessible offline, try it!
</div> </div>
</div> </div>
<div class="modal__button-bar"> <div class="modal__button-bar">
@ -149,9 +149,7 @@ $small-button-size: 22px;
} }
.workspace-entry__row { .workspace-entry__row {
margin-top: 1px; border-top: 1px solid $hr-color;
padding-top: 1px;
border-top: 1px solid rgba(128, 128, 128, 0.15);
line-height: $small-button-size; line-height: $small-button-size;
} }

5
src/icons/Key.vue Normal file
View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path d="M 7,14C 5.9,14 5,13.1 5,12C 5,10.9 5.9,10 7,10C 8.1,10 9,10.9 9,12C 9,13.1 8.1,14 7,14 Z M 12.65,10C 11.83,7.67 9.61,6 7,6C 3.69,6 1,8.69 1,12C 1,15.31 3.69,18 7,18C 9.61,18 11.83,16.33 12.65,14L 17,14L 17,18L 21,18L 21,14L 23,14L 23,10L 12.65,10 Z "/>
</svg>
</template>

View File

@ -63,6 +63,10 @@ export default {
background-image: url(../assets/iconGitlab.svg); background-image: url(../assets/iconGitlab.svg);
} }
.icon-provider--google {
background-image: url(../assets/iconGoogle.svg);
}
.icon-provider--dropbox { .icon-provider--dropbox {
background-image: url(../assets/iconDropbox.svg); background-image: url(../assets/iconDropbox.svg);
} }

View File

@ -51,6 +51,7 @@ import Magnify from './Magnify';
import FormatListChecks from './FormatListChecks'; import FormatListChecks from './FormatListChecks';
import CheckCircle from './CheckCircle'; import CheckCircle from './CheckCircle';
import ContentCopy from './ContentCopy'; import ContentCopy from './ContentCopy';
import Key from './Key';
Vue.component('iconProvider', Provider); Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold); Vue.component('iconFormatBold', FormatBold);
@ -104,3 +105,4 @@ Vue.component('iconMagnify', Magnify);
Vue.component('iconFormatListChecks', FormatListChecks); Vue.component('iconFormatListChecks', FormatListChecks);
Vue.component('iconCheckCircle', CheckCircle); Vue.component('iconCheckCircle', CheckCircle);
Vue.component('iconContentCopy', ContentCopy); Vue.component('iconContentCopy', ContentCopy);
Vue.component('iconKey', Key);

View File

@ -6,7 +6,7 @@ import userSvc from '../../userSvc';
const clientId = GOOGLE_CLIENT_ID; const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
const appsDomain = null; const appsDomain = null;
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h) const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
let googlePlusNotification = true; let googlePlusNotification = true;
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata']; const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
@ -37,20 +37,20 @@ if (utils.queryParams.providerId === 'googleDrive') {
} }
/** /**
* https://developers.google.com/+/web/api/rest/latest/people/get * https://developers.google.com/people/api/rest/v1/people/get
*/ */
const getUser = async (sub, token) => { const getUser = async (sub, token) => {
const { body } = await networkSvc.request(token const { body } = await networkSvc.request(token
? { ? {
method: 'GET', method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${sub}`, url: `https://people.googleapis.com/v1/people/${sub}`,
headers: { headers: {
Authorization: `Bearer ${token.accessToken}`, Authorization: `Bearer ${token.accessToken}`,
}, },
} }
: { : {
method: 'GET', method: 'GET',
url: `https://www.googleapis.com/plus/v1/people/${sub}?key=${apiKey}`, url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`,
}, true); }, true);
return body; return body;
}; };
@ -141,22 +141,25 @@ export default {
} }
// Build token object including scopes and sub // Build token object including scopes and sub
const existingToken = store.getters['data/googleTokensBySub'][body.sub]; const existingToken = store.getters['data/googleTokensBySub'][body.sub] || {
scopes: [],
};
const mergedScopes = [...new Set([...scopes, ...existingToken.scopes])];
const token = { const token = {
scopes, scopes: mergedScopes,
accessToken, accessToken,
expiresOn: Date.now() + (expiresIn * 1000), expiresOn: Date.now() + (expiresIn * 1000),
idToken, idToken,
sub: body.sub, sub: body.sub,
name: (existingToken || {}).name || 'Unknown', name: existingToken.name || 'Unknown',
isLogin: !store.getters['workspace/mainWorkspaceToken'] && isLogin: existingToken.isLogin || (!store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1, mergedScopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1),
isSponsor: false, isSponsor: existingToken.isSponsor || false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 || isDrive: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1, mergedScopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1, isBlogger: mergedScopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1, isPhotos: mergedScopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1, driveFullAccess: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
}; };
// Call the user info endpoint // Call the user info endpoint
@ -173,20 +176,6 @@ export default {
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'), imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
}); });
if (existingToken) {
// We probably retrieved a new token with restricted scopes.
// That's no problem, token will be refreshed later with merged scopes.
// Restore flags
Object.assign(token, {
isLogin: existingToken.isLogin || token.isLogin,
isSponsor: existingToken.isSponsor,
isDrive: existingToken.isDrive || token.isDrive,
isBlogger: existingToken.isBlogger || token.isBlogger,
isPhotos: existingToken.isPhotos || token.isPhotos,
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
});
}
if (token.isLogin) { if (token.isLogin) {
try { try {
token.isSponsor = (await networkSvc.request({ token.isSponsor = (await networkSvc.request({
@ -202,7 +191,7 @@ export default {
} }
// Add token to google tokens // Add token to google tokens
store.dispatch('data/addGoogleToken', token); await store.dispatch('data/addGoogleToken', token);
return token; return token;
}, },
async refreshToken(token, scopes = []) { async refreshToken(token, scopes = []) {

View File

@ -75,7 +75,7 @@ const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
helpers, helpers,
isAdditional: true, isAdditional: true,
}); });
const additionalTemplates = { const defaultTemplates = {
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'), plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate), plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate), styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
@ -153,7 +153,7 @@ export default {
templatesById: getter('templates'), templatesById: getter('templates'),
allTemplatesById: (state, { templatesById }) => ({ allTemplatesById: (state, { templatesById }) => ({
...templatesById, ...templatesById,
...additionalTemplates, ...defaultTemplates,
}), }),
lastCreated: getter('lastCreated'), lastCreated: getter('lastCreated'),
lastOpened: getter('lastOpened'), lastOpened: getter('lastOpened'),
@ -238,7 +238,7 @@ export default {
...templatesById, ...templatesById,
}; };
// We don't store additional templates // We don't store additional templates
Object.keys(additionalTemplates).forEach((id) => { Object.keys(defaultTemplates).forEach((id) => {
delete templatesToCommit[id]; delete templatesToCommit[id];
}); });
commit('setItem', itemTemplate('templates', templatesToCommit)); commit('setItem', itemTemplate('templates', templatesToCommit));

View File

@ -8,7 +8,7 @@ $line-height-title: 1.33;
$font-size-monospace: 0.85em; $font-size-monospace: 0.85em;
$highlighting-color: #ff0; $highlighting-color: #ff0;
$selection-highlighting-color: #ff9632; $selection-highlighting-color: #ff9632;
$info-bg: transparentize($selection-highlighting-color, 0.85); $info-bg: #ffad3326;
$code-border-radius: 3px; $code-border-radius: 3px;
$link-color: #0c93e4; $link-color: #0c93e4;
$error-color: #f31; $error-color: #f31;

View File

@ -450,7 +450,7 @@
</a> </a>
</div> </div>
<a href="app" title="The app">The app</a> <a href="https://community.stackedit.io" target="_blank" title="The app">Community</a><br> <a href="app" title="The app">The app</a> <a href="https://community.stackedit.io" target="_blank" title="The app">Community</a><br>
Copyright 2013-2018 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br> Copyright 2013-2019 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br>
Licensed under an Licensed under an
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a> <a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a>
<a href="privacy_policy.html" target="_blank">Privacy Policy</a> <a href="privacy_policy.html" target="_blank">Privacy Policy</a>