mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Extending Google Drive integration scope
Summary: New environmental variable GOOGLE_DRIVE_SCOPE that modifies the scope requested for Google Drive integration. For prod it has value https://www.googleapis.com/auth/drive.file which leaves current behavior (Grist is allowed only to access public files and for private files - it fallbacks to Picker). For staging it has value https://www.googleapis.com/auth/drive.readonly which allows Grist to access all private files, and fallbacks to Picker only when the file is neither public nor private). Default value is https://www.googleapis.com/auth/drive.file Test Plan: manual and existing tests Reviewers: dsagal Reviewed By: dsagal Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3038
This commit is contained in:
@@ -1,30 +1,18 @@
|
||||
import {get as getBrowserGlobals} from 'app/client/lib/browserGlobals';
|
||||
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';
|
||||
|
||||
import type {DocPageModel} from 'app/client/models/DocPageModel';
|
||||
import type {Document} from 'app/common/UserAPI';
|
||||
import { getGoogleCodeForSending } from "app/client/ui/googleAuth";
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
function getGoogleAuthEndpoint(scope?: string) {
|
||||
return new URL(`auth/google?scope=${scope || ''}`, window.location.origin).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) {
|
||||
export async 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
|
||||
@@ -38,68 +26,11 @@ export function sendToDrive(doc: Document, pageModel: DocPageModel) {
|
||||
.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 = '';
|
||||
try {
|
||||
const token = await getGoogleCodeForSending(gristDoc);
|
||||
const {url} = await send(token);
|
||||
G.window.location.assign(url);
|
||||
} catch (err) {
|
||||
reportError(err);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user