aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/apis/AuthenticationManager.tsx90
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts43
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/server/DashUploadUtils.ts2
-rw-r--r--src/server/RouteStore.ts3
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts79
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts4
-rw-r--r--src/server/index.ts31
8 files changed, 194 insertions, 60 deletions
diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx
new file mode 100644
index 000000000..75a50d8f9
--- /dev/null
+++ b/src/client/apis/AuthenticationManager.tsx
@@ -0,0 +1,90 @@
+import { observable, action, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import * as React from "react";
+import MainViewModal from "../views/MainViewModal";
+import { Opt } from "../../new_fields/Doc";
+import { Identified } from "../Network";
+import { RouteStore } from "../../server/RouteStore";
+
+@observer
+export default class AuthenticationManager extends React.Component<{}> {
+ public static Instance: AuthenticationManager;
+ @observable private openState = false;
+ private authenticationLink: Opt<string> = undefined;
+ @observable private authenticationCode: Opt<string> = undefined;
+ @observable private clickedState = false;
+
+ private get isOpen() {
+ return this.openState;
+ }
+
+ private set isOpen(value: boolean) {
+ runInAction(() => this.openState = value);
+ }
+
+ private get hasBeenClicked() {
+ return this.clickedState;
+ }
+
+ private set hasBeenClicked(value: boolean) {
+ runInAction(() => this.clickedState = value);
+ }
+
+ public executeFullRoutine = async (authenticationLink: string) => {
+ this.authenticationLink = authenticationLink;
+ this.isOpen = true;
+ return new Promise<string>(async resolve => {
+ const disposer = reaction(
+ () => this.authenticationCode,
+ authenticationCode => {
+ if (authenticationCode) {
+ Identified.PostToServer(RouteStore.writeGooglePhotosAccessToken, { authenticationCode }).then(token => {
+ this.isOpen = false;
+ this.hasBeenClicked = false;
+ resolve(token);
+ disposer();
+ });
+ }
+ }
+ );
+ });
+ }
+
+ constructor(props: {}) {
+ super(props);
+ AuthenticationManager.Instance = this;
+ }
+
+ private handleClick = () => {
+ window.open(this.authenticationLink);
+ this.hasBeenClicked = true;
+ }
+
+ private handlePaste = action((e: React.ChangeEvent<HTMLInputElement>) => {
+ this.authenticationCode = e.currentTarget.value;
+ })
+
+ private get renderPrompt() {
+ return (
+ <div style={{ display: "flex", flexDirection: "column" }}>
+ <button onClick={this.handleClick}>Please click here to authorize a Google account...</button>
+ {this.clickedState ? <input
+ onChange={this.handlePaste}
+ placeholder={"Please paste the external authetication code here..."}
+ style={{ marginTop: 15 }}
+ /> : (null)}
+ </div>
+ )
+ }
+
+ render() {
+ return (
+ <MainViewModal
+ isDisplayed={this.openState}
+ interactive={true}
+ contents={this.renderPrompt}
+ />
+ );
+ }
+
+} \ No newline at end of file
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index 29cc042b6..dd1492f51 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -13,14 +13,20 @@ import { Docs, DocumentOptions } from "../../documents/Documents";
import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes";
import { AssertionError } from "assert";
import { DocumentView } from "../../views/nodes/DocumentView";
-import { DocumentManager } from "../../util/DocumentManager";
import { Identified } from "../../Network";
+import AuthenticationManager from "../AuthenticationManager";
+import { List } from "../../../new_fields/List";
export namespace GooglePhotos {
+ const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
+
const endpoint = async () => {
- const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken);
- return new Photos(accessToken);
+ let response = await Identified.FetchFromServer(RouteStore.readGooglePhotosAccessToken);
+ if (new RegExp(AuthenticationUrl).test(response)) {
+ response = await AuthenticationManager.Instance.executeFullRoutine(response);
+ }
+ return new Photos(response);
};
export enum MediaType {
@@ -89,9 +95,14 @@ export namespace GooglePhotos {
}
const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`);
const { id, productUrl } = await Create.Album(resolved);
- const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey);
- if (newMediaItemResults) {
- const mediaItems = newMediaItemResults.map(item => item.mediaItem);
+ const response = await Transactions.UploadImages(images, { id }, descriptionKey);
+ if (response) {
+ const { results, failed } = response;
+ let index: Opt<number>;
+ while ((index = failed.pop()) !== undefined) {
+ Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]);
+ }
+ const mediaItems: MediaItem[] = results.map(item => item.mediaItem);
if (mediaItems.length !== images.length) {
throw new AssertionError({ actual: mediaItems.length, expected: images.length });
}
@@ -99,6 +110,9 @@ export namespace GooglePhotos {
for (let i = 0; i < images.length; i++) {
const image = Doc.GetProto(images[i]);
const mediaItem = mediaItems[i];
+ if (!mediaItem) {
+ continue;
+ }
image.googlePhotosId = mediaItem.id;
image.googlePhotosAlbumUrl = productUrl;
image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl;
@@ -304,17 +318,22 @@ export namespace GooglePhotos {
};
export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => {
- const newMediaItems = await UploadImages(sources, album, descriptionKey);
- if (!newMediaItems) {
+ const response = await UploadImages(sources, album, descriptionKey);
+ if (!response) {
return undefined;
}
- const baseUrls: string[] = await Promise.all(newMediaItems.map(item => {
+ const baseUrls: string[] = await Promise.all(response.results.map(item => {
return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl)));
}));
return baseUrls;
};
- export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<NewMediaItemResult[]>> => {
+ export interface ImageUploadResults {
+ results: NewMediaItemResult[];
+ failed: number[];
+ }
+
+ export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => {
if (album && "title" in album) {
album = await Create.Album(album.title);
}
@@ -331,8 +350,8 @@ export namespace GooglePhotos {
media.push({ url, description });
}
if (media.length) {
- const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
- return uploads;
+ const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+ return results;
}
};
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 3b0457dff..12578e5b8 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -39,6 +39,7 @@ import PDFMenu from './pdf/PDFMenu';
import { PreviewCursor } from './PreviewCursor';
import { FilterBox } from './search/FilterBox';
import { OverlayView } from './OverlayView';
+import AuthenticationManager from '../apis/AuthenticationManager';
@observer
export class MainView extends React.Component {
@@ -677,6 +678,7 @@ export class MainView extends React.Component {
<div id="main-div">
{this.dictationOverlay}
<SharingManager />
+ <AuthenticationManager />
<DocumentDecorations />
{this.mainContent}
<PreviewCursor />
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 4230e9b17..57b46714a 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -22,7 +22,7 @@ export namespace DashUploadUtils {
const gifs = [".gif"];
const pngs = [".png"];
const jpgs = [".jpg", ".jpeg"];
- const imageFormats = [...pngs, ...jpgs, ...gifs];
+ export const imageFormats = [...pngs, ...jpgs, ...gifs];
const videoFormats = [".mov", ".mp4"];
const size = "content-length";
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
index ee9cd8a0e..23fdbc53d 100644
--- a/src/server/RouteStore.ts
+++ b/src/server/RouteStore.ts
@@ -32,7 +32,8 @@ export enum RouteStore {
// APIS
cognitiveServices = "/cognitiveservices",
googleDocs = "/googleDocs",
- googlePhotosAccessToken = "/googlePhotosAccessToken",
+ readGooglePhotosAccessToken = "/readGooglePhotosAccessToken",
+ writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken",
googlePhotosMediaUpload = "/googlePhotosMediaUpload",
googlePhotosMediaDownload = "/googlePhotosMediaDownload",
googleDocsGet = "/googleDocsGet"
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index c899c2ef2..2f29cb95f 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -75,6 +75,42 @@ export namespace GoogleApiServerUtils {
});
};
+ const RetrieveOAuthClient = async (information: CredentialInformation) => {
+ return new Promise<OAuth2Client>((resolve, reject) => {
+ readFile(information.credentialsPath, async (err, credentials) => {
+ if (err) {
+ reject(err);
+ return console.log('Error loading client secret file:', err);
+ }
+ const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed;
+ resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]));
+ });
+ });
+ }
+
+ export const GenerateAuthenticationUrl = async (information: CredentialInformation) => {
+ const client = await RetrieveOAuthClient(information);
+ return client.generateAuthUrl({
+ access_type: 'offline',
+ scope: SCOPES.map(relative => prefix + relative),
+ });
+ };
+
+ export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<TokenResult> => {
+ const oAuth2Client = await RetrieveOAuthClient(information);
+ return new Promise<TokenResult>((resolve, reject) => {
+ oAuth2Client.getToken(authenticationCode, async (err, token) => {
+ if (err || !token) {
+ reject(err);
+ return console.error('Error retrieving access token', err);
+ }
+ oAuth2Client.setCredentials(token);
+ await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token);
+ resolve({ token, client: oAuth2Client });
+ });
+ });
+ }
+
export const RetrieveCredentials = (information: CredentialInformation) => {
return new Promise<TokenResult>((resolve, reject) => {
readFile(information.credentialsPath, async (err, credentials) => {
@@ -107,17 +143,13 @@ export namespace GoogleApiServerUtils {
return new Promise<TokenResult>((resolve, reject) => {
// Attempting to authorize user (${userId})
Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => {
- if (!token) {
- // No token registered, so awaiting input from user
- return getNewToken(oAuth2Client, userId).then(resolve, reject);
- }
- if (token.expiry_date! < new Date().getTime()) {
+ if (token!.expiry_date! < new Date().getTime()) {
// Token has expired, so submitting a request for a refreshed access token
- return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject);
+ return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject);
}
// Authentication successful!
- oAuth2Client.setCredentials(token);
- resolve({ token, client: oAuth2Client });
+ oAuth2Client.setCredentials(token!);
+ resolve({ token: token!, client: oAuth2Client });
});
});
}
@@ -145,35 +177,4 @@ export namespace GoogleApiServerUtils {
});
};
- /**
- * Get and store new token after prompting for user authorization, and then
- * execute the given callback with the authorized OAuth2 client.
- * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for.
- * @param {getEventsCallback} callback The callback for the authorized client.
- */
- function getNewToken(oAuth2Client: OAuth2Client, userId: string) {
- return new Promise<TokenResult>((resolve, reject) => {
- const authUrl = oAuth2Client.generateAuthUrl({
- access_type: 'offline',
- scope: SCOPES.map(relative => prefix + relative),
- });
- console.log('Authorize this app by visiting this url:', authUrl);
- const rl = createInterface({
- input: process.stdin,
- output: process.stdout,
- });
- rl.question('Enter the code from that page here: ', (code) => {
- rl.close();
- oAuth2Client.getToken(code, async (err, token) => {
- if (err || !token) {
- reject(err);
- return console.error('Error retrieving access token', err);
- }
- oAuth2Client.setCredentials(token);
- await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token);
- resolve({ token, client: oAuth2Client });
- });
- });
- });
- }
} \ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 16c4f6c3a..4a67e57cc 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -4,6 +4,7 @@ import * as path from 'path';
import { MediaItemCreationResult } from './SharedTypes';
import { NewMediaItem } from "../../index";
import { BatchedArray, TimeUnit } from 'array-batcher';
+import { DashUploadUtils } from '../../DashUploadUtils';
export namespace GooglePhotosUploadUtils {
@@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils {
};
export const DispatchGooglePhotosUpload = async (url: string) => {
+ if (!DashUploadUtils.imageFormats.includes(path.extname(url))) {
+ return undefined;
+ }
const body = await request(url, { encoding: null });
const parameters = {
method: 'POST',
diff --git a/src/server/index.ts b/src/server/index.ts
index 690836fff..077002894 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -862,7 +862,22 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
-app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token)));
+app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
+ const information = { credentialsPath, userId };
+ if (!token) {
+ return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information));
+ }
+ GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token));
+});
+
+app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => {
+ const userId = req.header("userId")!;
+ const information = { credentialsPath, userId };
+ const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode);
+ res.send(token.access_token);
+});
const tokenError = "Unable to successfully upload bytes for all images!";
const mediaError = "Unable to convert all uploaded bytes to media items!";
@@ -885,16 +900,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
- let failed = 0;
+ let failed: number[] = [];
const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
{ magnitude: 100, unit: TimeUnit.Milliseconds },
async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
const newMediaItems: NewMediaItem[] = [];
- for (let element of batch) {
+ for (let index = 0; index < batch.length; index++) {
+ const element = batch[index];
const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
if (!uploadToken) {
- failed++;
+ failed.push(index);
} else {
newMediaItems.push({
description: element.description,
@@ -906,12 +922,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
}
);
- if (failed) {
- return _error(res, tokenError);
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`)
}
GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
- result => _success(res, result.newMediaItemResults),
+ result => _success(res, { results: result.newMediaItemResults, failed }),
error => _error(res, mediaError, error)
);
});