You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gristlabs_grist-core/app/client/ui/sendToDrive.ts

107 lines
4.5 KiB

import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
import {getHomeUrl} from 'app/client/models/AppModel';
import {reportError} from 'app/client/models/errors';
import {spinnerModal} from 'app/client/ui2018/modals';
import type { DocPageModel } from 'app/client/models/DocPageModel';
import type { Document } from 'app/common/UserAPI';
import type { Disposable } from 'grainjs';
const G = getBrowserGlobals('window');
/**
* Generates Google Auth endpoint (exposed by Grist) url. For example:
* https://docs.getgrist.com/auth/google
* @param scope Requested access scope for Google Services
* https://developers.google.com/identity/protocols/oauth2/scopes
*/
function getGoogleAuthEndpoint(scope?: string) {
return new URL(`auth/google?scope=${scope || ''}`, getHomeUrl()).href;
}
/**
* Sends xlsx file to Google Drive. It first authenticates with Google to get encrypted access token,
* then it calls "send-to-drive" api endpoint to upload xlsx file to drive and finally it redirects
* to the created spreadsheet.
* Code that is received from Google contains encrypted access token, server is able to decrypt it
* using GOOGLE_CLIENT_SECRET key.
*/
export function sendToDrive(doc: Document, pageModel: DocPageModel) {
// Get current document - it will be used to remove popup listener.
const gristDoc = pageModel.gristDoc.get();
// Sanity check - gristDoc should be always present
if (!gristDoc) { throw new Error("Grist document is not present in Page Model"); }
// Create send to google drive handler (it will return a spreadsheet url).
const send = (code: string) =>
// Decorate it with a spinner
spinnerModal('Sending file to Google Drive',
pageModel.appModel.api.getDocAPI(doc.id)
.sendToDrive(code, pageModel.currentDocTitle.get())
);
// Compute google auth server endpoint (grist endpoint for server side google authentication).
// This endpoint will redirect user to Google Consent screen and after Google sends a response,
// it will render a page (/static/message.html) that will post a message containing message
// from Google. Message will be an object { code, error }. We will use the code to invoke
// "send-to-drive" api endpoint - that will actually send the xlsx file to Google Drive.
const authLink = getGoogleAuthEndpoint();
const authWindow = openPopup(authLink);
attachListener(gristDoc, authWindow, async (event: MessageEvent) => {
// For the first message (we expect only a single message) close the window.
authWindow.close();
// Check response from the popup
const response = (event.data || {}) as { code?: string, error?: string };
// - when user declined, do nothing,
if (response.error === "access_denied") { return; }
// - when there is no response code or error code is different from what we expected - report to user.
if (!response.code) { reportError(response.error || "Unrecognized or empty error code"); return; }
// Send file to Google Drive.
try {
const { url } = await send(response.code);
G.window.location.assign(url);
} catch (err) {
reportError(err);
}
});
}
// Helper function that attaches a handler to message event from a popup window.
function attachListener(owner: Disposable, popup: Window, listener: (e: MessageEvent) => any) {
const wrapped = (e: MessageEvent) => {
// Listen to events only from our window.
if (e.source !== popup) { return; }
// In case when Grist was reloaded or user navigated away - do nothing.
if (owner.isDisposed()) { return; }
listener(e);
};
G.window.addEventListener('message', wrapped);
owner.onDispose(() => {
G.window.removeEventListener('message', wrapped);
});
}
function openPopup(url: string): Window {
// Center window on desktop
// https://stackoverflow.com/questions/16363474/window-open-on-a-multi-monitor-dual-monitor-system-where-does-window-pop-up
const width = 600;
const height = 650;
const left = window.screenX + (screen.width - width) / 2;
const top = (screen.height - height) / 4;
let windowFeatures = `top=${top},left=${left},menubar=no,location=no,` +
`resizable=yes,scrollbars=yes,status=yes,height=${height},width=${width}`;
// If window will be too large (for example on mobile) - open as a new tab
if (screen.width <= width || screen.height <= height) {
windowFeatures = '';
}
const authWindow = G.window.open(url, "GoogleAuthPopup", windowFeatures);
if (!authWindow) {
// This method should be invoked by an user action.
throw new Error("This method should be invoked synchronously");
}
return authWindow;
}