From 45b97084b3d49d521ff39963f250e9cd9efe3f8e Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 05:00:23 -0400 Subject: client side google api authentication UI --- src/client/apis/AuthenticationManager.tsx | 90 ++++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 43 ++++++++--- 2 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 src/client/apis/AuthenticationManager.tsx (limited to 'src/client/apis') 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 = undefined; + @observable private authenticationCode: Opt = 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(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) => { + this.authenticationCode = e.currentTarget.value; + }) + + private get renderPrompt() { + return ( +
+ + {this.clickedState ? : (null)} +
+ ) + } + + render() { + return ( + + ); + } + +} \ 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; + 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(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); })); return baseUrls; }; - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + export interface ImageUploadResults { + results: NewMediaItemResult[]; + failed: number[]; + } + + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { 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; } }; -- cgit v1.2.3-70-g09d2 From 0d1d884ae24c74cc92b3be7a429873bde3d4370c Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 05:38:16 -0400 Subject: clean up and extensibility for authentication manager --- src/client/apis/AuthenticationManager.tsx | 42 +++++++++++++--------- .../apis/google_docs/GooglePhotosClientUtils.ts | 8 +---- 2 files changed, 26 insertions(+), 24 deletions(-) (limited to 'src/client/apis') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx index 75a50d8f9..7d8d4f534 100644 --- a/src/client/apis/AuthenticationManager.tsx +++ b/src/client/apis/AuthenticationManager.tsx @@ -6,6 +6,8 @@ import { Opt } from "../../new_fields/Doc"; import { Identified } from "../Network"; import { RouteStore } from "../../server/RouteStore"; +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + @observer export default class AuthenticationManager extends React.Component<{}> { public static Instance: AuthenticationManager; @@ -30,24 +32,30 @@ export default class AuthenticationManager extends React.Component<{}> { runInAction(() => this.clickedState = value); } - public executeFullRoutine = async (authenticationLink: string) => { - this.authenticationLink = authenticationLink; - this.isOpen = true; - return new Promise(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(); - }); + public executeFullRoutine = async (service: string) => { + let response = await Identified.FetchFromServer(`/read${service}AccessToken`); + // if this is an authentication url, activate the UI to register the new access token + if (new RegExp(AuthenticationUrl).test(response)) { + this.isOpen = true; + this.authenticationLink = response; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(`/write${service}AccessToken`, { authenticationCode }).then(token => { + this.isOpen = false; + this.hasBeenClicked = false; + resolve(token); + disposer(); + }); + } } - } - ); - }); + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; } constructor(props: {}) { diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index dd1492f51..8e88040db 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -19,14 +19,8 @@ import { List } from "../../../new_fields/List"; export namespace GooglePhotos { - const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; - const endpoint = async () => { - let response = await Identified.FetchFromServer(RouteStore.readGooglePhotosAccessToken); - if (new RegExp(AuthenticationUrl).test(response)) { - response = await AuthenticationManager.Instance.executeFullRoutine(response); - } - return new Photos(response); + return new Photos(await AuthenticationManager.Instance.executeFullRoutine("GooglePhotos")); }; export enum MediaType { -- cgit v1.2.3-70-g09d2 From 8f935493d8db08d4c1173f0dafb1fe7f2d414f54 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 05:41:11 -0400 Subject: semicolons --- src/client/apis/AuthenticationManager.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src/client/apis') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx index 7d8d4f534..360554b8e 100644 --- a/src/client/apis/AuthenticationManager.tsx +++ b/src/client/apis/AuthenticationManager.tsx @@ -70,7 +70,7 @@ export default class AuthenticationManager extends React.Component<{}> { private handlePaste = action((e: React.ChangeEvent) => { this.authenticationCode = e.currentTarget.value; - }) + }); private get renderPrompt() { return ( @@ -82,7 +82,7 @@ export default class AuthenticationManager extends React.Component<{}> { style={{ marginTop: 15 }} /> : (null)} - ) + ); } render() { -- cgit v1.2.3-70-g09d2 From 33841ef7ca23348dd03017768504e536cf567177 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 05:42:15 -0400 Subject: cleanup --- src/client/apis/AuthenticationManager.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) (limited to 'src/client/apis') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx index 360554b8e..d8f6b675b 100644 --- a/src/client/apis/AuthenticationManager.tsx +++ b/src/client/apis/AuthenticationManager.tsx @@ -4,9 +4,9 @@ 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"; const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Please paste the external authetication code here..."; @observer export default class AuthenticationManager extends React.Component<{}> { @@ -16,18 +16,10 @@ export default class AuthenticationManager extends React.Component<{}> { @observable private authenticationCode: Opt = 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); } @@ -78,7 +70,7 @@ export default class AuthenticationManager extends React.Component<{}> { {this.clickedState ? : (null)} -- cgit v1.2.3-70-g09d2