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/server/apis/google/GoogleApiServerUtils.ts | 79 ++++++++++++----------- src/server/apis/google/GooglePhotosUploadUtils.ts | 4 ++ 2 files changed, 44 insertions(+), 39 deletions(-) (limited to 'src/server/apis') 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((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 => { + const oAuth2Client = await RetrieveOAuthClient(information); + 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 }); + }); + }); + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { @@ -107,17 +143,13 @@ export namespace GoogleApiServerUtils { return new Promise((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((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', -- cgit v1.2.3-70-g09d2 From 7763ddefcd14986573f9a0010c7691fa4715b94e Mon Sep 17 00:00:00 2001 From: Bob Zeleznik Date: Wed, 9 Oct 2019 15:13:28 -0400 Subject: semicolons --- src/client/util/TooltipTextMenu.tsx | 4 ++-- src/server/apis/google/GoogleApiServerUtils.ts | 4 ++-- src/server/index.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'src/server/apis') diff --git a/src/client/util/TooltipTextMenu.tsx b/src/client/util/TooltipTextMenu.tsx index e162ad475..c82d3bc63 100644 --- a/src/client/util/TooltipTextMenu.tsx +++ b/src/client/util/TooltipTextMenu.tsx @@ -841,11 +841,11 @@ export class TooltipTextMenu { } } - update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps) } + update(view: EditorView, lastState: EditorState | undefined) { this.updateFromDash(view, lastState, this.editorProps); } //updates the tooltip menu when the selection changes public updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) { if (!view) { - console.log("no editor? why?") + console.log("no editor? why?"); return; } this.view = view; diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 2f29cb95f..963c7736a 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -86,7 +86,7 @@ export namespace GoogleApiServerUtils { resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); }); }); - } + }; export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { const client = await RetrieveOAuthClient(information); @@ -109,7 +109,7 @@ export namespace GoogleApiServerUtils { resolve({ token, client: oAuth2Client }); }); }); - } + }; export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { diff --git a/src/server/index.ts b/src/server/index.ts index 077002894..5da05d9a7 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -924,7 +924,7 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { const failedCount = failed.length; if (failedCount) { - console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`) + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( -- 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/server/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