From 70a142b84d01e89b56d027b24e37122fa90fe25b Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Wed, 9 Oct 2019 16:53:16 -0400
Subject: better authentication feedback, cleanup

---
 src/client/apis/GoogleAuthenticationManager.tsx | 114 ++++++++++++++++++++++++
 1 file changed, 114 insertions(+)
 create mode 100644 src/client/apis/GoogleAuthenticationManager.tsx

(limited to 'src/client/apis/GoogleAuthenticationManager.tsx')

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<string> = undefined;
+    @observable private authenticationCode: Opt<string> = undefined;
+    @observable private clickedState = false;
+    @observable private success: Opt<boolean> = 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<string>(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<HTMLInputElement>) => {
+        this.authenticationCode = e.currentTarget.value;
+    });
+
+    private get renderPrompt() {
+        return (
+            <div style={{ display: "flex", flexDirection: "column" }}>
+                {this.displayLauncher ? <button
+                    className={"dispatch"}
+                    onClick={this.handleClick}
+                >Authorize a Google account...</button> : (null)}
+                {this.clickedState ? <input
+                    className={'paste-target'}
+                    onChange={this.handlePaste}
+                    placeholder={prompt}
+                    style={{ marginTop: 15 }}
+                /> : (null)}
+            </div>
+        );
+    }
+
+    render() {
+        return (
+            <MainViewModal
+                isDisplayed={this.openState}
+                interactive={true}
+                contents={this.renderPrompt}
+                overlayDisplayedOpacity={0.9}
+                dialogueBoxStyle={{ borderColor: this.success === undefined ? "black" : this.success ? "green" : "red" }}
+            />
+        );
+    }
+
+}
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2


From 0890639c166df6558c3a5c0fd3d427157b3549de Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
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/GoogleAuthenticationManager.tsx')

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 ? <button
                     className={"dispatch"}
                     onClick={this.handleClick}
+                    style={{ marginBottom: 15 }}
                 >Authorize a Google account...</button> : (null)}
                 {this.clickedState ? <input
                     className={'paste-target'}
                     onChange={this.handlePaste}
                     placeholder={prompt}
-                    style={{ marginTop: 15 }}
                 /> : (null)}
             </div>
         );
-- 
cgit v1.2.3-70-g09d2


From e5e06155af44f86c7204ed5ce904f705186401db Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
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/GoogleAuthenticationManager.tsx')

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 ? <button
                     className={"dispatch"}
                     onClick={this.handleClick}
-                    style={{ marginBottom: 15 }}
+                    style={{ marginBottom: this.clickedState ? 15 : 0 }}
                 >Authorize a Google account...</button> : (null)}
                 {this.clickedState ? <input
                     className={'paste-target'}
-- 
cgit v1.2.3-70-g09d2


From bd346692a29ea5c0960bbb086c856c4890d7c089 Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
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/GoogleAuthenticationManager.tsx')

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 (
             <MainViewModal
@@ -106,7 +115,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
                 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 <samwilkins333@gmail.com>
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/GoogleAuthenticationManager.tsx')

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<boolean> = undefined;
     @observable private displayLauncher = true;
+    @observable private avatar: Opt<string> = undefined;
+    @observable private username: Opt<string> = 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 (
-            <div style={{ display: "flex", flexDirection: "column" }}>
+            <div className={'authorize-container'}>
                 {this.displayLauncher ? <button
                     className={"dispatch"}
                     onClick={this.handleClick}
@@ -99,6 +108,14 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
                     onChange={this.handlePaste}
                     placeholder={prompt}
                 /> : (null)}
+                {this.avatar ? <img
+                    className={'avatar'}
+                    src={this.avatar}
+                /> : (null)}
+                {this.username ? <span
+                    className={'welcome'}
+                >Welcome to Dash, {this.username}
+                </span> : (null)}
             </div>
         );
     }
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<TokenResult> => {
+    export interface GoogleAuthenticationResult {
+        access_token: string;
+        avatar: string;
+        name: string;
+    }
+    export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<GoogleAuthenticationResult> => {
         const oAuth2Client = await RetrieveOAuthClient(information);
-        return new Promise<TokenResult>((resolve, reject) => {
+        return new Promise<GoogleAuthenticationResult>((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<TokenResult>((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<StoredCredentials>({ 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

