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 From 70a142b84d01e89b56d027b24e37122fa90fe25b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:16 -0400 Subject: better authentication feedback, cleanup --- src/client/apis/AuthenticationManager.tsx | 90 ---------------- src/client/apis/GoogleAuthenticationManager.scss | 3 + src/client/apis/GoogleAuthenticationManager.tsx | 114 +++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 7 +- src/client/util/SharingManager.tsx | 2 +- src/client/views/MainView.tsx | 4 +- src/server/RouteStore.ts | 4 +- src/server/index.ts | 4 +- 8 files changed, 126 insertions(+), 102 deletions(-) delete mode 100644 src/client/apis/AuthenticationManager.tsx create mode 100644 src/client/apis/GoogleAuthenticationManager.scss create mode 100644 src/client/apis/GoogleAuthenticationManager.tsx (limited to 'src/client/apis') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx deleted file mode 100644 index d8f6b675b..000000000 --- a/src/client/apis/AuthenticationManager.tsx +++ /dev/null @@ -1,90 +0,0 @@ -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"; - -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<{}> { - public static Instance: AuthenticationManager; - @observable private openState = false; - private authenticationLink: Opt = undefined; - @observable private authenticationCode: Opt = undefined; - @observable private clickedState = false; - - private set isOpen(value: boolean) { - runInAction(() => this.openState = value); - } - - private set hasBeenClicked(value: boolean) { - runInAction(() => this.clickedState = value); - } - - 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: {}) { - 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/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss new file mode 100644 index 000000000..5efb3ab3b --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -0,0 +1,3 @@ +.paste-target { + padding: 5px; +} \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx new file mode 100644 index 000000000..1ab6380ef --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -0,0 +1,114 @@ +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"; +import "./GoogleAuthenticationManager.scss"; + +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Paste authorization code here..."; + +@observer +export default class GoogleAuthenticationManager extends React.Component<{}> { + public static Instance: GoogleAuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + @observable private success: Opt = undefined; + @observable private displayLauncher = true; + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public fetchOrGenerateAccessToken = async () => { + let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken); + // 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(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( + token => { + runInAction(() => this.success = true); + setTimeout(() => { + this.isOpen = false; + runInAction(() => this.displayLauncher = false); + setTimeout(() => { + runInAction(() => this.success = undefined); + runInAction(() => this.displayLauncher = true); + this.hasBeenClicked = false; + }, 500); + }, 1000); + disposer(); + resolve(token); + }, + () => { + this.hasBeenClicked = false; + runInAction(() => this.success = false); + } + ); + } + } + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; + } + + constructor(props: {}) { + super(props); + GoogleAuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + setTimeout(() => this.hasBeenClicked = true, 500); + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }); + + private get renderPrompt() { + return ( +
+ {this.displayLauncher ? : (null)} + {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 8e88040db..e93fa6eb4 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -14,14 +14,11 @@ import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/Share import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; import { Identified } from "../../Network"; -import AuthenticationManager from "../AuthenticationManager"; -import { List } from "../../../new_fields/List"; +import GoogleAuthenticationManager from "../GoogleAuthenticationManager"; export namespace GooglePhotos { - const endpoint = async () => { - return new Photos(await AuthenticationManager.Instance.executeFullRoutine("GooglePhotos")); - }; + const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 91c8c572d..1541cd6b2 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -218,7 +218,7 @@ export default class SharingManager extends React.Component<{}> { if (!metadata) { return SharingPermissions.None; } - return StrCast(metadata.permissions, SharingPermissions.None)!; + return StrCast(metadata.permissions, SharingPermissions.None); } private get sharingInterface() { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 12578e5b8..ba4224875 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,7 +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'; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; @observer export class MainView extends React.Component { @@ -678,7 +678,7 @@ export class MainView extends React.Component {
{this.dictationOverlay} - + {this.mainContent} diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 23fdbc53d..391d9dc0c 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,8 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", - writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", + readGoogleAccessToken = "/readGoogleAccessToken", + writeGoogleAccessToken = "/writeGoogleAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/index.ts b/src/server/index.ts index 5da05d9a7..dd44a0ce8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,7 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { +app.get(RouteStore.readGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); const information = { credentialsPath, userId }; @@ -872,7 +872,7 @@ app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); }); -app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { +app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const information = { credentialsPath, userId }; const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); -- cgit v1.2.3-70-g09d2 From 0890639c166df6558c3a5c0fd3d427157b3549de Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:57 -0400 Subject: margin fix --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/apis') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 1ab6380ef..8e8bf8def 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -88,12 +88,12 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { {this.displayLauncher ? : (null)} {this.clickedState ? : (null)}
); -- cgit v1.2.3-70-g09d2 From e5e06155af44f86c7204ed5ce904f705186401db Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 17:16:02 -0400 Subject: quick margin fix --- src/client/apis/GoogleAuthenticationManager.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/client/apis') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index 8e8bf8def..db108ad94 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -88,7 +88,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { {this.displayLauncher ? : (null)} {this.clickedState ? Date: Wed, 9 Oct 2019 23:21:17 -0400 Subject: cleanup --- src/client/apis/GoogleAuthenticationManager.tsx | 37 +++++++++++++++---------- 1 file changed, 23 insertions(+), 14 deletions(-) (limited to 'src/client/apis') diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index db108ad94..d143d8273 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -41,23 +41,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { if (authenticationCode) { Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( token => { - runInAction(() => this.success = true); - setTimeout(() => { - this.isOpen = false; - runInAction(() => this.displayLauncher = false); - setTimeout(() => { - runInAction(() => this.success = undefined); - runInAction(() => this.displayLauncher = true); - this.hasBeenClicked = false; - }, 500); - }, 1000); + this.beginFadeout(); disposer(); resolve(token); }, - () => { + action(() => { this.hasBeenClicked = false; - runInAction(() => this.success = false); - } + this.success = false; + }) ); } } @@ -68,6 +59,19 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { return response; } + beginFadeout = action(() => { + this.success = true; + setTimeout(action(() => { + this.isOpen = false; + this.displayLauncher = false; + setTimeout(action(() => { + this.success = undefined; + this.displayLauncher = true; + this.hasBeenClicked = false; + }), 500); + }), 2000); + }); + constructor(props: {}) { super(props); GoogleAuthenticationManager.Instance = this; @@ -99,6 +103,11 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { ); } + private get dialogueBoxStyle() { + const borderColor = this.success === undefined ? "black" : this.success ? "green" : "red"; + return { borderColor, transition: "0.2s borderColor ease" }; + } + render() { return ( { interactive={true} contents={this.renderPrompt} overlayDisplayedOpacity={0.9} - dialogueBoxStyle={{ borderColor: this.success === undefined ? "black" : this.success ? "green" : "red" }} + dialogueBoxStyle={this.dialogueBoxStyle} /> ); } -- cgit v1.2.3-70-g09d2 From 5b83d8da6c262897dc75ada26f08ed1c46ceb95c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 12 Oct 2019 19:13:25 -0400 Subject: parse google user data and use it to provide customized feedback on login --- src/client/apis/GoogleAuthenticationManager.scss | 20 +++++++++- src/client/apis/GoogleAuthenticationManager.tsx | 29 +++++++++++--- src/server/apis/google/GoogleApiServerUtils.ts | 51 +++++++++++++++++++++--- src/server/database.ts | 5 ++- src/server/index.ts | 3 +- 5 files changed, 91 insertions(+), 17 deletions(-) (limited to 'src/client/apis') diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss index 5efb3ab3b..13bde822d 100644 --- a/src/client/apis/GoogleAuthenticationManager.scss +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -1,3 +1,19 @@ -.paste-target { - padding: 5px; +.authorize-container { + display: flex; + flex-direction: column; + align-items: center; + + .paste-target { + padding: 5px; + width: 100%; + } + + .avatar { + border-radius: 50%; + } + + .welcome { + font-style: italic; + margin-top: 15px; + } } \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx index d143d8273..01dac3996 100644 --- a/src/client/apis/GoogleAuthenticationManager.tsx +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -19,6 +19,8 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { @observable private clickedState = false; @observable private success: Opt = undefined; @observable private displayLauncher = true; + @observable private avatar: Opt = undefined; + @observable private username: Opt = undefined; private set isOpen(value: boolean) { runInAction(() => this.openState = value); @@ -40,10 +42,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { authenticationCode => { if (authenticationCode) { Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( - token => { + ({ access_token, avatar, name }) => { + runInAction(() => { + this.avatar = avatar; + this.username = name; + }); this.beginFadeout(); disposer(); - resolve(token); + resolve(access_token); }, action(() => { this.hasBeenClicked = false; @@ -61,15 +67,18 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { beginFadeout = action(() => { this.success = true; + this.authenticationCode = undefined; + this.displayLauncher = false; + this.hasBeenClicked = false; setTimeout(action(() => { this.isOpen = false; - this.displayLauncher = false; setTimeout(action(() => { this.success = undefined; this.displayLauncher = true; - this.hasBeenClicked = false; + this.avatar = undefined; + this.username = undefined; }), 500); - }), 2000); + }), 3000); }); constructor(props: {}) { @@ -88,7 +97,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> { private get renderPrompt() { return ( -
+
{this.displayLauncher ?
); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 963c7736a..5714c9928 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -25,7 +25,8 @@ export namespace GoogleApiServerUtils { 'drive.file', 'photoslibrary', 'photoslibrary.appendonly', - 'photoslibrary.sharing' + 'photoslibrary.sharing', + 'userinfo.profile' ]; export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); @@ -96,21 +97,61 @@ export namespace GoogleApiServerUtils { }); }; - export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { const oAuth2Client = await RetrieveOAuthClient(information); - return new Promise((resolve, reject) => { + return new Promise((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 }); + const enriched = injectUserInfo(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched); + const { given_name, picture } = enriched.userInfo; + resolve({ + access_token: enriched.access_token!, + avatar: picture, + name: given_name + }); }); }); }; + /** + * It's pretty cool: the credentials id_token is split into thirds by periods. + * The middle third contains a base64-encoded JSON string with all the + * user info contained in the interface below. So, we isolate that middle third, + * base64 decode with atob and parse the JSON. + * @param credentials the client credentials returned from OAuth after the user + * has executed the authentication routine + */ + const injectUserInfo = (credentials: Credentials): EnrichedCredentials => { + const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); + return { ...credentials, userInfo }; + }; + + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + export interface UserInfo { + at_hash: string; + aud: string; + azp: string; + exp: number; + family_name: string; + given_name: string; + iat: number; + iss: string; + locale: string; + name: string; + picture: string; + sub: string; + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { diff --git a/src/server/database.ts b/src/server/database.ts index 990441d5a..db86b472d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,6 +4,7 @@ import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { DashUploadUtils } from './DashUploadUtils'; import { Credentials } from 'google-auth-library'; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; export namespace Database { @@ -259,8 +260,8 @@ export namespace Database { return SanitizedSingletonQuery({ userId }, GoogleAuthentication, removeId); }; - export const Write = async (userId: string, token: any) => { - return Instance.insert({ userId, canAccess: [], ...token }, GoogleAuthentication); + export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); }; export const Update = async (userId: string, access_token: string, expiry_date: number) => { diff --git a/src/server/index.ts b/src/server/index.ts index 86c226a21..010a851bc 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -958,8 +958,7 @@ app.get(RouteStore.readGoogleAccessToken, async (req, res) => { app.post(RouteStore.writeGoogleAccessToken, 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); + res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); }); const tokenError = "Unable to successfully upload bytes for all images!"; -- cgit v1.2.3-70-g09d2