From feec691275ec83e4ddd8fd8ea803f004a371cf11 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 27 Oct 2019 18:46:57 -0400 Subject: refactoring oauth2 client use in google api serverside --- src/server/apis/google/GoogleApiServerUtils.ts | 87 +++++++++++--------------- src/server/index.ts | 10 ++- 2 files changed, 40 insertions(+), 57 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 6093197f1..c0824cfb7 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -1,7 +1,6 @@ import { google } from "googleapis"; -import { createInterface } from "readline"; -import { readFile, writeFile } from "fs"; -import { OAuth2Client, Credentials } from "google-auth-library"; +import { readFile } from "fs"; +import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; @@ -31,6 +30,8 @@ export namespace GoogleApiServerUtils { 'userinfo.profile' ]; + const ClientMapping = new Map(); + export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); export enum Service { @@ -51,11 +52,11 @@ export namespace GoogleApiServerUtils { export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler }; export type EndpointParameters = GlobalOptions & { version: "v1" }; - export const GetEndpoint = (sector: string, paths: CredentialInformation) => { + export const GetEndpoint = (sector: string, userId: string) => { return new Promise>(resolve => { - RetrieveCredentials(paths).then(authentication => { + authorize(userId).then(({ client: auth }) => { let routed: Opt; - let parameters: EndpointParameters = { auth: authentication.client, version: "v1" }; + let parameters: EndpointParameters = { auth, version: "v1" }; switch (sector) { case Service.Documents: routed = google.docs(parameters).documents; @@ -69,16 +70,17 @@ export namespace GoogleApiServerUtils { }); }; - export const RetrieveAccessToken = (information: CredentialInformation) => { + export const RetrieveAccessToken = (userId: string): Promise => { return new Promise((resolve, reject) => { - RetrieveCredentials(information).then( - credentials => resolve(credentials.token.access_token!), + authorize(userId).then( + ({ token: { access_token } }) => resolve(access_token!), error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) ); }); }; - let AuthorizationManager: OAuth2Client; + let installed: OAuth2ClientOptions; + let worker: OAuth2Client; export const LoadOAuthClient = async () => { return new Promise((resolve, reject) => { @@ -87,15 +89,17 @@ export namespace GoogleApiServerUtils { reject(err); return console.log('Error loading client secret file:', err); } - const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed; - AuthorizationManager = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); + installed = parseBuffer(credentials).installed; + worker = new google.auth.OAuth2(installed); resolve(); }); }); }; + const generateClient = () => new google.auth.OAuth2(installed); + export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { - return AuthorizationManager.generateAuthUrl({ + return worker.generateAuthUrl({ access_type: 'offline', scope: SCOPES.map(relative => prefix + relative), }); @@ -106,16 +110,15 @@ export namespace GoogleApiServerUtils { avatar: string; name: string; } - export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + export const ProcessClientSideCode = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { - AuthorizationManager.getToken(authenticationCode, async (err, token) => { + worker.getToken(authenticationCode, async (err, token) => { if (err || !token) { reject(err); return console.error('Error retrieving access token', err); } - AuthorizationManager.setCredentials(token); const enriched = injectUserInfo(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched); + await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched); const { given_name, picture } = enriched.userInfo; resolve({ access_token: enriched.access_token!, @@ -155,57 +158,39 @@ export namespace GoogleApiServerUtils { sub: string; } - export const RetrieveCredentials = (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); + export const authorize = async (userId: string): Promise => { + return Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { + return new Promise((resolve, reject) => { + const client = generateClient(); + if (token!.expiry_date! < new Date().getTime()) { + // Token has expired, so submitting a request for a refreshed access token + return refreshToken(token!, client, userId).then(resolve, reject); } - authorize(parseBuffer(credentials), information.userId).then(resolve, reject); + // Authentication successful! + client.setCredentials(token!); + resolve({ token: token!, client }); }); }); }; - export const RetrievePhotosEndpoint = (paths: CredentialInformation) => { + export const RetrievePhotosEndpoint = (userId: string) => { return new Promise((resolve, reject) => { - RetrieveAccessToken(paths).then( + RetrieveAccessToken(userId).then( token => resolve(new Photos(token)), reject ); }); }; - type TokenResult = { token: Credentials, client: OAuth2Client }; - /** - * Create an OAuth2 client with the given credentials, and returns the promise resolving to the authenticated client - * @param {Object} credentials The authorization client credentials. - */ - export function authorize(credentials: any, userId: string): Promise { - const { client_secret, client_id, redirect_uris } = credentials.installed; - const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uris[0]); - return new Promise((resolve, reject) => { - // Attempting to authorize user (${userId}) - Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - 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); - } - // Authentication successful! - oAuth2Client.setCredentials(token!); - resolve({ token: token!, client: oAuth2Client }); - }); - }); - } + type AuthenticationResult = { token: Credentials, client: OAuth2Client }; const refreshEndpoint = "https://oauth2.googleapis.com/token"; - const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, userId: string) => { - return new Promise(resolve => { + const refreshToken = (credentials: Credentials, oAuth2Client: OAuth2Client, userId: string) => { + return new Promise(resolve => { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; let queryParameters = { refreshToken: credentials.refresh_token, - client_id, - client_secret, + ...installed, grant_type: "refresh_token" }; let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`; diff --git a/src/server/index.ts b/src/server/index.ts index 384800f23..3220a9533 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -564,10 +564,10 @@ function routeSetter(router: RouteManager) { router.addSupervisedRoute({ method: Method.POST, subscription: new RouteSubscriber(RouteStore.googleDocs).add("sector", "action"), - onValidation: ({ req, res }) => { + onValidation: ({ req, res, user }) => { let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service; let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], { credentialsPath, userId: req.headers.userId as string }).then(endpoint => { + GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( @@ -592,7 +592,7 @@ function routeSetter(router: RouteManager) { if (!token) { return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); } - GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); + GoogleApiServerUtils.RetrieveAccessToken(userId).then(token => res.send(token)); } }); @@ -600,9 +600,7 @@ function routeSetter(router: RouteManager) { method: Method.POST, subscription: RouteStore.writeGoogleAccessToken, onValidation: async ({ user, req, res }) => { - const userId = user.id; - const information = { credentialsPath, userId }; - res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode)); + res.send(await GoogleApiServerUtils.ProcessClientSideCode(user.id, req.body.authenticationCode)); } }); -- cgit v1.2.3-70-g09d2 From f0f3dddbe1d3ac54d3754bb913b8ecd9eb6fcc63 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 27 Oct 2019 21:05:01 -0400 Subject: further cleanup for oauth, separated access token function from client function --- src/server/apis/google/GoogleApiServerUtils.ts | 65 ++++++++++++----------- src/server/apis/google/GooglePhotosUploadUtils.ts | 19 +++---- src/server/database.ts | 2 +- src/server/index.ts | 24 +++------ 4 files changed, 47 insertions(+), 63 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c0824cfb7..88f0f3377 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -15,7 +15,6 @@ const path = require("path"); */ export namespace GoogleApiServerUtils { - // If modifying these scopes, delete token.json. const prefix = 'https://www.googleapis.com/auth/'; const SCOPES = [ 'documents.readonly', @@ -54,7 +53,7 @@ export namespace GoogleApiServerUtils { export const GetEndpoint = (sector: string, userId: string) => { return new Promise>(resolve => { - authorize(userId).then(({ client: auth }) => { + retrieveOAuthClient(userId).then(auth => { let routed: Opt; let parameters: EndpointParameters = { auth, version: "v1" }; switch (sector) { @@ -70,10 +69,23 @@ export namespace GoogleApiServerUtils { }); }; - export const RetrieveAccessToken = (userId: string): Promise => { + export const retrieveAccessToken = (userId: string): Promise => { return new Promise((resolve, reject) => { - authorize(userId).then( - ({ token: { access_token } }) => resolve(access_token!), + retrieveCredentials(userId).then( + ({ access_token }) => resolve(access_token!), + error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) + ); + }); + }; + + export const retrieveOAuthClient = (userId: string): Promise => { + return new Promise((resolve, reject) => { + retrieveCredentials(userId).then( + credentials => { + const client = generateClient(); + client.setCredentials(credentials); + resolve(client); + }, error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) ); }); @@ -82,7 +94,7 @@ export namespace GoogleApiServerUtils { let installed: OAuth2ClientOptions; let worker: OAuth2Client; - export const LoadOAuthClient = async () => { + export const loadClientSecret = async () => { return new Promise((resolve, reject) => { readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, credentials) => { if (err) { @@ -90,7 +102,7 @@ export namespace GoogleApiServerUtils { return console.log('Error loading client secret file:', err); } installed = parseBuffer(credentials).installed; - worker = new google.auth.OAuth2(installed); + worker = generateClient(); resolve(); }); }); @@ -98,7 +110,7 @@ export namespace GoogleApiServerUtils { const generateClient = () => new google.auth.OAuth2(installed); - export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { + export const generateAuthenticationUrl = async () => { return worker.generateAuthUrl({ access_type: 'offline', scope: SCOPES.map(relative => prefix + relative), @@ -110,7 +122,7 @@ export namespace GoogleApiServerUtils { avatar: string; name: string; } - export const ProcessClientSideCode = async (userId: string, authenticationCode: string): Promise => { + export const processNewUser = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { worker.getToken(authenticationCode, async (err, token) => { if (err || !token) { @@ -158,35 +170,25 @@ export namespace GoogleApiServerUtils { sub: string; } - export const authorize = async (userId: string): Promise => { - return Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - return new Promise((resolve, reject) => { - const client = generateClient(); - if (token!.expiry_date! < new Date().getTime()) { + const retrieveCredentials = async (userId: string): Promise => { + return new Promise((resolve, reject) => { + Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(credentials => { + if (!credentials) { + return reject(); + } + if (credentials!.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshToken(token!, client, userId).then(resolve, reject); + return refreshAccessToken(credentials!, userId).then(resolve, reject); } // Authentication successful! - client.setCredentials(token!); - resolve({ token: token!, client }); + resolve(credentials); }); }); }; - export const RetrievePhotosEndpoint = (userId: string) => { - return new Promise((resolve, reject) => { - RetrieveAccessToken(userId).then( - token => resolve(new Photos(token)), - reject - ); - }); - }; - - type AuthenticationResult = { token: Credentials, client: OAuth2Client }; - const refreshEndpoint = "https://oauth2.googleapis.com/token"; - const refreshToken = (credentials: Credentials, oAuth2Client: OAuth2Client, userId: string) => { - return new Promise(resolve => { + const refreshAccessToken = (credentials: Credentials, userId: string) => { + return new Promise(resolve => { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; let queryParameters = { refreshToken: credentials.refresh_token, @@ -200,8 +202,7 @@ export namespace GoogleApiServerUtils { await Database.Auxiliary.GoogleAuthenticationToken.Update(userId, access_token, expiry_date); credentials.access_token = access_token; credentials.expiry_date = expiry_date; - oAuth2Client.setCredentials(credentials); - resolve({ token: credentials, client: oAuth2Client }); + resolve(credentials); }); }); }; diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 4a67e57cc..d704faa71 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -20,19 +20,12 @@ export namespace GooglePhotosUploadUtils { } const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; - const headers = (type: string) => ({ + const headers = (type: string, token: string) => ({ 'Content-Type': `application/${type}`, - 'Authorization': Bearer, + 'Authorization': token, }); - let Bearer: string; - - export const initialize = async (information: GoogleApiServerUtils.CredentialInformation) => { - const token = await GoogleApiServerUtils.RetrieveAccessToken(information); - Bearer = `Bearer ${token}`; - }; - - export const DispatchGooglePhotosUpload = async (url: string) => { + export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string) => { if (!DashUploadUtils.imageFormats.includes(path.extname(url))) { return undefined; } @@ -40,7 +33,7 @@ export namespace GooglePhotosUploadUtils { const parameters = { method: 'POST', headers: { - ...headers('octet-stream'), + ...headers('octet-stream', bearerToken), 'X-Goog-Upload-File-Name': path.basename(url), 'X-Goog-Upload-Protocol': 'raw' }, @@ -56,13 +49,13 @@ export namespace GooglePhotosUploadUtils { })); }; - export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise => { + export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise => { const newMediaItemResults = await BatchedArray.from(newMediaItems, { batchSize: 50 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: NewMediaItem[]) => { const parameters = { method: 'POST', - headers: headers('json'), + headers: headers('json', bearerToken), uri: prepend('mediaItems:batchCreate'), body: { newMediaItems: batch } as any, json: true diff --git a/src/server/database.ts b/src/server/database.ts index 12626e594..79dd26b7d 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -298,7 +298,7 @@ export namespace Database { export type StoredCredentials = Credentials & { _id: string }; - export const Fetch = async (userId: string, removeId = true) => { + export const Fetch = async (userId: string, removeId = true): Promise> => { return SanitizedSingletonQuery({ userId }, GoogleAuthentication, removeId); }; diff --git a/src/server/index.ts b/src/server/index.ts index 3220a9533..24866a5e5 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -65,7 +65,7 @@ async function PreliminaryFunctions() { resolve(); }); }); - await GoogleApiServerUtils.LoadOAuthClient(); + await GoogleApiServerUtils.loadClientSecret(); await DashUploadUtils.createIfNotExists(pdfDirectory); await Database.tryInitializeConnection(); } @@ -553,8 +553,6 @@ function routeSetter(router: RouteManager) { } }); - const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json"); - const EndpointHandlerMap = new Map([ ["create", (api, params) => api.create(params)], ["retrieve", (api, params) => api.get(params)], @@ -588,11 +586,10 @@ function routeSetter(router: RouteManager) { onValidation: async ({ user, res }) => { const userId = user.id; const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); - const information = { credentialsPath, userId }; if (!token) { - return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + return res.send(await GoogleApiServerUtils.generateAuthenticationUrl()); } - GoogleApiServerUtils.RetrieveAccessToken(userId).then(token => res.send(token)); + GoogleApiServerUtils.retrieveAccessToken(userId).then(token => res.send(token)); } }); @@ -600,35 +597,28 @@ function routeSetter(router: RouteManager) { method: Method.POST, subscription: RouteStore.writeGoogleAccessToken, onValidation: async ({ user, req, res }) => { - res.send(await GoogleApiServerUtils.ProcessClientSideCode(user.id, req.body.authenticationCode)); + res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode)); } }); const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; - const userIdError = "Unable to parse the identification of the user!"; router.addSupervisedRoute({ method: Method.POST, subscription: RouteStore.googlePhotosMediaUpload, onValidation: async ({ user, req, res }) => { const { media } = req.body; - const userId = user.id; - if (!userId) { - return _error(res, userIdError); - } - - await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); let failed: number[] = []; - + const token = await GoogleApiServerUtils.retrieveAccessToken(user.id); const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: GooglePhotosUploadUtils.MediaInput[]) => { const newMediaItems: NewMediaItem[] = []; for (let index = 0; index < batch.length; index++) { const element = batch[index]; - const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); + const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, element.url); if (!uploadToken) { failed.push(index); } else { @@ -647,7 +637,7 @@ function routeSetter(router: RouteManager) { console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } - GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( + GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then( result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); -- cgit v1.2.3-70-g09d2 From c4e832aa5c384c9d5f018ed1148cc003e988a45e Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sun, 27 Oct 2019 21:09:53 -0400 Subject: cleanup --- src/server/apis/google/GoogleApiServerUtils.ts | 115 +++++++++++++------------ 1 file changed, 58 insertions(+), 57 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 88f0f3377..5a6aa7abe 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -6,28 +6,68 @@ import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); import * as qs from 'query-string'; -import Photos = require('googlephotos'); import { Database } from "../../database"; const path = require("path"); +const prefix = 'https://www.googleapis.com/auth/'; +const refreshEndpoint = "https://oauth2.googleapis.com/token"; +const SCOPES = [ + 'documents.readonly', + 'documents', + 'presentations', + 'presentations.readonly', + 'drive', + 'drive.file', + 'photoslibrary', + 'photoslibrary.appendonly', + 'photoslibrary.sharing', + 'userinfo.profile' +]; + /** * Server side authentication for Google Api queries. */ export namespace GoogleApiServerUtils { - const prefix = 'https://www.googleapis.com/auth/'; - const SCOPES = [ - 'documents.readonly', - 'documents', - 'presentations', - 'presentations.readonly', - 'drive', - 'drive.file', - 'photoslibrary', - 'photoslibrary.appendonly', - 'photoslibrary.sharing', - 'userinfo.profile' - ]; + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + + 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; + } + + let installed: OAuth2ClientOptions; + let worker: OAuth2Client; + + export const loadClientSecret = async () => { + return new Promise((resolve, reject) => { + readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + installed = parseBuffer(credentials).installed; + worker = generateClient(); + resolve(); + }); + }); + }; const ClientMapping = new Map(); @@ -71,7 +111,7 @@ export namespace GoogleApiServerUtils { export const retrieveAccessToken = (userId: string): Promise => { return new Promise((resolve, reject) => { - retrieveCredentials(userId).then( + retrieveCurrentCredentials(userId).then( ({ access_token }) => resolve(access_token!), error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) ); @@ -80,34 +120,17 @@ export namespace GoogleApiServerUtils { export const retrieveOAuthClient = (userId: string): Promise => { return new Promise((resolve, reject) => { - retrieveCredentials(userId).then( + retrieveCurrentCredentials(userId).then( credentials => { const client = generateClient(); client.setCredentials(credentials); resolve(client); }, - error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) + error => reject(`Error: unable to instantiate and certify a new OAuth2 client.\n${error}`) ); }); }; - let installed: OAuth2ClientOptions; - let worker: OAuth2Client; - - export const loadClientSecret = async () => { - return new Promise((resolve, reject) => { - readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, credentials) => { - if (err) { - reject(err); - return console.log('Error loading client secret file:', err); - } - installed = parseBuffer(credentials).installed; - worker = generateClient(); - resolve(); - }); - }); - }; - const generateClient = () => new google.auth.OAuth2(installed); export const generateAuthenticationUrl = async () => { @@ -117,11 +140,6 @@ export namespace GoogleApiServerUtils { }); }; - export interface GoogleAuthenticationResult { - access_token: string; - avatar: string; - name: string; - } export const processNewUser = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { worker.getToken(authenticationCode, async (err, token) => { @@ -154,23 +172,7 @@ export namespace GoogleApiServerUtils { 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; - } - - const retrieveCredentials = async (userId: string): Promise => { + const retrieveCurrentCredentials = async (userId: string): Promise => { return new Promise((resolve, reject) => { Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(credentials => { if (!credentials) { @@ -186,7 +188,6 @@ export namespace GoogleApiServerUtils { }); }; - const refreshEndpoint = "https://oauth2.googleapis.com/token"; const refreshAccessToken = (credentials: Credentials, userId: string) => { return new Promise(resolve => { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; -- cgit v1.2.3-70-g09d2 From b217bd842356deace1e6620625b8f1841a9bce7b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 02:10:58 -0400 Subject: using client mapping --- src/server/apis/google/GoogleApiServerUtils.ts | 43 ++++++++++++++++++-------- 1 file changed, 30 insertions(+), 13 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 5a6aa7abe..ad7540e5d 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -37,6 +37,11 @@ export namespace GoogleApiServerUtils { name: string; } + export interface CredentialsResult { + credentials: Credentials; + refreshed: boolean; + } + export interface UserInfo { at_hash: string; aud: string; @@ -69,7 +74,7 @@ export namespace GoogleApiServerUtils { }); }; - const ClientMapping = new Map(); + const authenticationClients = new Map(); export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); @@ -111,8 +116,8 @@ export namespace GoogleApiServerUtils { export const retrieveAccessToken = (userId: string): Promise => { return new Promise((resolve, reject) => { - retrieveCurrentCredentials(userId).then( - ({ access_token }) => resolve(access_token!), + retrieveCredentials(userId).then( + ({ credentials }) => resolve(credentials.access_token!), error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`) ); }); @@ -120,10 +125,14 @@ export namespace GoogleApiServerUtils { export const retrieveOAuthClient = (userId: string): Promise => { return new Promise((resolve, reject) => { - retrieveCurrentCredentials(userId).then( - credentials => { - const client = generateClient(); - client.setCredentials(credentials); + retrieveCredentials(userId).then( + ({ credentials, refreshed }) => { + let client = authenticationClients.get(userId); + if (!client) { + authenticationClients.set(userId, client = generateClientWith(credentials)); + } else if (refreshed) { + client.setCredentials(credentials); + } resolve(client); }, error => reject(`Error: unable to instantiate and certify a new OAuth2 client.\n${error}`) @@ -131,7 +140,15 @@ export namespace GoogleApiServerUtils { }); }; - const generateClient = () => new google.auth.OAuth2(installed); + function generateClient() { + return new google.auth.OAuth2(installed); + } + + function generateClientWith(credentials: Credentials) { + const client = new google.auth.OAuth2(installed); + client.setCredentials(credentials); + return client; + } export const generateAuthenticationUrl = async () => { return worker.generateAuthUrl({ @@ -172,8 +189,8 @@ export namespace GoogleApiServerUtils { return { ...credentials, userInfo }; }; - const retrieveCurrentCredentials = async (userId: string): Promise => { - return new Promise((resolve, reject) => { + const retrieveCredentials = async (userId: string): Promise => { + return new Promise((resolve, reject) => { Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(credentials => { if (!credentials) { return reject(); @@ -183,13 +200,13 @@ export namespace GoogleApiServerUtils { return refreshAccessToken(credentials!, userId).then(resolve, reject); } // Authentication successful! - resolve(credentials); + resolve({ credentials, refreshed: false }); }); }); }; const refreshAccessToken = (credentials: Credentials, userId: string) => { - return new Promise(resolve => { + return new Promise(resolve => { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; let queryParameters = { refreshToken: credentials.refresh_token, @@ -203,7 +220,7 @@ export namespace GoogleApiServerUtils { await Database.Auxiliary.GoogleAuthenticationToken.Update(userId, access_token, expiry_date); credentials.access_token = access_token; credentials.expiry_date = expiry_date; - resolve(credentials); + resolve({ credentials, refreshed: true }); }); }); }; -- cgit v1.2.3-70-g09d2 From 1f6e1d7e063f9ce1c08486f8c0c11b6c2c4198dc Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 04:11:53 -0400 Subject: repaired google photos routine, no route handlers can have dangling promises --- src/server/ApiManagers/ExportManager.ts | 4 +- src/server/ApiManagers/UtilManager.ts | 36 ++-- src/server/RouteManager.ts | 1 + src/server/apis/google/GoogleApiServerUtils.ts | 9 +- src/server/apis/google/GooglePhotosUploadUtils.ts | 2 +- src/server/index.ts | 202 +++++++++++----------- 6 files changed, 137 insertions(+), 117 deletions(-) (limited to 'src') diff --git a/src/server/ApiManagers/ExportManager.ts b/src/server/ApiManagers/ExportManager.ts index 261acbbe0..14ac7dd5b 100644 --- a/src/server/ApiManagers/ExportManager.ts +++ b/src/server/ApiManagers/ExportManager.ts @@ -26,7 +26,7 @@ export default class ExportManager extends ApiManager { const id = req.params.docId; const hierarchy: Hierarchy = {}; await buildHierarchyRecursive(id, hierarchy); - BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy)); + return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy)); } }); } @@ -48,7 +48,7 @@ export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMut const zip = Archiver('zip'); zip.pipe(res); await mutator(zip); - zip.finalize(); + return zip.finalize(); } /** diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts index a3f802b20..61cda2e9b 100644 --- a/src/server/ApiManagers/UtilManager.ts +++ b/src/server/ApiManagers/UtilManager.ts @@ -10,13 +10,16 @@ export default class UtilManager extends ApiManager { register({ method: Method.GET, subscription: "/pull", - onValidation: ({ res }) => { - exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', err => { - if (err) { - res.send(err.message); - return; - } - res.redirect("/"); + onValidation: async ({ res }) => { + return new Promise(resolve => { + exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', err => { + if (err) { + res.send(err.message); + return; + } + res.redirect("/"); + resolve(); + }); }); } }); @@ -24,14 +27,14 @@ export default class UtilManager extends ApiManager { register({ method: Method.GET, subscription: "/buxton", - onValidation: ({ res }) => { + onValidation: async ({ res }) => { let cwd = '../scraping/buxton'; let onResolved = (stdout: string) => { console.log(stdout); res.redirect("/"); }; let onRejected = (err: any) => { console.error(err.message); res.send(err); }; let tryPython3 = () => command_line('python3 scraper.py', cwd).then(onResolved, onRejected); - command_line('python scraper.py', cwd).then(onResolved, tryPython3); + return command_line('python scraper.py', cwd).then(onResolved, tryPython3); }, }); @@ -39,12 +42,15 @@ export default class UtilManager extends ApiManager { method: Method.GET, subscription: "/version", onValidation: ({ res }) => { - exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout) => { - if (err) { - res.send(err.message); - return; - } - res.send(stdout); + return new Promise(resolve => { + exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout) => { + if (err) { + res.send(err.message); + return; + } + res.send(stdout); + }); + resolve(); }); } }); diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index ef083a88a..21ce9c9e4 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -75,6 +75,7 @@ export default class RouteManager { } setTimeout(() => { if (!res.headersSent) { + console.log("Initiating fallback for ", target); const warning = `request to ${target} fell through - this is a fallback response`; res.send({ warning }); } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index ad7540e5d..1cca07036 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -62,12 +62,17 @@ export namespace GoogleApiServerUtils { export const loadClientSecret = async () => { return new Promise((resolve, reject) => { - readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, credentials) => { + readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, projectCredentials) => { if (err) { reject(err); return console.log('Error loading client secret file:', err); } - installed = parseBuffer(credentials).installed; + const { client_secret, client_id, redirect_uris } = parseBuffer(projectCredentials).installed; + installed = { + clientId: client_id, + clientSecret: client_secret, + redirectUri: redirect_uris[0] + }; worker = generateClient(); resolve(); }); diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index d704faa71..172fa8d46 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -22,7 +22,7 @@ export namespace GooglePhotosUploadUtils { const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`; const headers = (type: string, token: string) => ({ 'Content-Type': `application/${type}`, - 'Authorization': token, + 'Authorization': `Bearer ${token}`, }); export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string) => { diff --git a/src/server/index.ts b/src/server/index.ts index 24866a5e5..eb19c71a9 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -225,55 +225,58 @@ function routeSetter(router: RouteManager) { } } }; - form.parse(req, async (err, fields, files) => { - remap = fields.remap !== "false"; - let id: string = ""; - try { - for (const name in files) { - const path_2 = files[name].path; - const zip = new AdmZip(path_2); - zip.getEntries().forEach((entry: any) => { - if (!entry.entryName.startsWith("files/")) return; - let dirname = path.dirname(entry.entryName) + "/"; - let extname = path.extname(entry.entryName); - let basename = path.basename(entry.entryName).split(".")[0]; - // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false); - // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false); - // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false); - // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false); + return new Promise(resolve => { + form.parse(req, async (_err, fields, files) => { + remap = fields.remap !== "false"; + let id: string = ""; + try { + for (const name in files) { + const path_2 = files[name].path; + const zip = new AdmZip(path_2); + zip.getEntries().forEach((entry: any) => { + if (!entry.entryName.startsWith("files/")) return; + let dirname = path.dirname(entry.entryName) + "/"; + let extname = path.extname(entry.entryName); + let basename = path.basename(entry.entryName).split(".")[0]; + // zip.extractEntryTo(dirname + basename + "_o" + extname, __dirname + RouteStore.public, true, false); + // zip.extractEntryTo(dirname + basename + "_s" + extname, __dirname + RouteStore.public, true, false); + // zip.extractEntryTo(dirname + basename + "_m" + extname, __dirname + RouteStore.public, true, false); + // zip.extractEntryTo(dirname + basename + "_l" + extname, __dirname + RouteStore.public, true, false); + try { + zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false); + dirname = "/" + dirname; + + fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname)); + fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname)); + fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname)); + fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname)); + } catch (e) { + console.log(e); + } + }); + const json = zip.getEntry("doc.json"); + let docs: any; try { - zip.extractEntryTo(entry.entryName, __dirname + RouteStore.public, true, false); - dirname = "/" + dirname; - - fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_o" + extname)); - fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_s" + extname)); - fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_m" + extname)); - fs.createReadStream(__dirname + RouteStore.public + dirname + basename + extname).pipe(fs.createWriteStream(__dirname + RouteStore.public + dirname + basename + "_l" + extname)); - } catch (e) { - console.log(e); - } - }); - const json = zip.getEntry("doc.json"); - let docs: any; - try { - let data = JSON.parse(json.getData().toString("utf8")); - docs = data.docs; - id = data.id; - docs = Object.keys(docs).map(key => docs[key]); - docs.forEach(mapFn); - await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { - err && console.log(err); - res(); - }, true, "newDocuments")))); - } catch (e) { console.log(e); } - fs.unlink(path_2, () => { }); - } - if (id) { - res.send(JSON.stringify(getId(id))); - } else { - res.send(JSON.stringify("error")); - } - } catch (e) { console.log(e); } + let data = JSON.parse(json.getData().toString("utf8")); + docs = data.docs; + id = data.id; + docs = Object.keys(docs).map(key => docs[key]); + docs.forEach(mapFn); + await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { + err && console.log(err); + res(); + }, true, "newDocuments")))); + } catch (e) { console.log(e); } + fs.unlink(path_2, () => { }); + } + if (id) { + res.send(JSON.stringify(getId(id))); + } else { + res.send(JSON.stringify("error")); + } + } catch (e) { console.log(e); } + resolve(); + }); }); } }); @@ -285,22 +288,25 @@ function routeSetter(router: RouteManager) { let filename = req.params.filename; let noExt = filename.substring(0, filename.length - ".png".length); let pagenumber = parseInt(noExt.split('-')[1]); - fs.exists(uploadDirectory + filename, (exists: boolean) => { - console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`); - if (exists) { - let input = fs.createReadStream(uploadDirectory + filename); - probe(input, (err: any, result: any) => { - if (err) { - console.log(err); - console.log(`error on ${filename}`); - return; - } - res.send({ path: "/files/" + filename, width: result.width, height: result.height }); - }); - } - else { - LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); - } + return new Promise(resolve => { + fs.exists(uploadDirectory + filename, (exists: boolean) => { + console.log(`${uploadDirectory + filename} ${exists ? "exists" : "does not exist"}`); + if (exists) { + let input = fs.createReadStream(uploadDirectory + filename); + probe(input, (err: any, result: any) => { + if (err) { + console.log(err); + console.log(`error on ${filename}`); + return; + } + res.send({ path: "/files/" + filename, width: result.width, height: result.height }); + }); + } + else { + LoadPage(uploadDirectory + filename.substring(0, filename.length - noExt.split('-')[1].length - ".PNG".length - 1) + ".pdf", pagenumber, res); + } + resolve(); + }); }); } }); @@ -414,8 +420,8 @@ function routeSetter(router: RouteManager) { var canvas = createCanvas(width, height); var context = canvas.getContext('2d'); return { - canvas: canvas, - context: context, + canvas, + context }; } @@ -442,37 +448,39 @@ function routeSetter(router: RouteManager) { router.addSupervisedRoute({ method: Method.POST, subscription: RouteStore.upload, - onValidation: ({ req, res }) => { + onValidation: async ({ req, res }) => { let form = new formidable.IncomingForm(); form.uploadDir = uploadDirectory; form.keepExtensions = true; - form.parse(req, async (_err, _fields, files) => { - let results: ImageFileResponse[] = []; - for (const key in files) { - const { type, path: location, name } = files[key]; - const filename = path.basename(location); - let uploadInformation: Opt; - if (filename.endsWith(".pdf")) { - let dataBuffer = fs.readFileSync(uploadDirectory + filename); - const result: ParsedPDF = await pdf(dataBuffer); - await new Promise(resolve => { - const path = pdfDirectory + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt"; - fs.createWriteStream(path).write(result.text, error => { - if (!error) { - resolve(); - } else { - reject(error); - } + return new Promise(resolve => { + form.parse(req, async (_err, _fields, files) => { + let results: ImageFileResponse[] = []; + for (const key in files) { + const { type, path: location, name } = files[key]; + const filename = path.basename(location); + let uploadInformation: Opt; + if (filename.endsWith(".pdf")) { + let dataBuffer = fs.readFileSync(uploadDirectory + filename); + const result: ParsedPDF = await pdf(dataBuffer); + await new Promise(resolve => { + const path = pdfDirectory + "/" + filename.substring(0, filename.length - ".pdf".length) + ".txt"; + fs.createWriteStream(path).write(result.text, error => { + if (!error) { + resolve(); + } else { + reject(error); + } + }); }); - }); - } else { - uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename); + } else { + uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename); + } + const exif = uploadInformation ? uploadInformation.exifData : undefined; + results.push({ name, type, path: `/files/${filename}`, exif }); } - const exif = uploadInformation ? uploadInformation.exifData : undefined; - results.push({ name, type, path: `/files/${filename}`, exif }); - - } - _success(res, results); + _success(res, results); + resolve(); + }); }); } }); @@ -500,7 +508,7 @@ function routeSetter(router: RouteManager) { res.status(401).send("incorrect parameters specified"); return; } - imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => { + return imageDataUri.outputFile(uri, uploadDirectory + filename).then((savedName: string) => { const ext = path.extname(savedName); let resizers = [ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" }, @@ -562,10 +570,10 @@ function routeSetter(router: RouteManager) { router.addSupervisedRoute({ method: Method.POST, subscription: new RouteSubscriber(RouteStore.googleDocs).add("sector", "action"), - onValidation: ({ req, res, user }) => { + onValidation: async ({ req, res, user }) => { let sector: GoogleApiServerUtils.Service = req.params.sector as GoogleApiServerUtils.Service; let action: GoogleApiServerUtils.Action = req.params.action as GoogleApiServerUtils.Action; - GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id).then(endpoint => { + return GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id).then(endpoint => { let handler = EndpointHandlerMap.get(action); if (endpoint && handler) { let execute = handler(endpoint, req.body).then( @@ -589,7 +597,7 @@ function routeSetter(router: RouteManager) { if (!token) { return res.send(await GoogleApiServerUtils.generateAuthenticationUrl()); } - GoogleApiServerUtils.retrieveAccessToken(userId).then(token => res.send(token)); + return GoogleApiServerUtils.retrieveAccessToken(userId).then(token => res.send(token)); } }); @@ -637,7 +645,7 @@ function routeSetter(router: RouteManager) { console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`); } - GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then( + return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then( result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); -- cgit v1.2.3-70-g09d2 From acea9d7aa984fe8b1eeac0546833d3dca3c844e3 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 04:14:37 -0400 Subject: removed one-line functions --- src/server/apis/google/GoogleApiServerUtils.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 1cca07036..92bb8d072 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -67,13 +67,12 @@ export namespace GoogleApiServerUtils { reject(err); return console.log('Error loading client secret file:', err); } - const { client_secret, client_id, redirect_uris } = parseBuffer(projectCredentials).installed; - installed = { + const { client_secret, client_id, redirect_uris } = JSON.parse(projectCredentials.toString()).installed; + worker = new google.auth.OAuth2({ clientId: client_id, clientSecret: client_secret, redirectUri: redirect_uris[0] - }; - worker = generateClient(); + }); resolve(); }); }); @@ -81,8 +80,6 @@ export namespace GoogleApiServerUtils { const authenticationClients = new Map(); - export const parseBuffer = (data: Buffer) => JSON.parse(data.toString()); - export enum Service { Documents = "Documents", Slides = "Slides" @@ -145,10 +142,6 @@ export namespace GoogleApiServerUtils { }); }; - function generateClient() { - return new google.auth.OAuth2(installed); - } - function generateClientWith(credentials: Credentials) { const client = new google.auth.OAuth2(installed); client.setCredentials(credentials); -- cgit v1.2.3-70-g09d2 From b259472385b03099380f22c7c19ae135b2adf30c Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 04:17:06 -0400 Subject: rename --- src/server/apis/google/GoogleApiServerUtils.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 92bb8d072..4e5175a2b 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -157,12 +157,12 @@ export namespace GoogleApiServerUtils { export const processNewUser = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { - worker.getToken(authenticationCode, async (err, token) => { - if (err || !token) { + worker.getToken(authenticationCode, async (err, credentials) => { + if (err || !credentials) { reject(err); return console.error('Error retrieving access token', err); } - const enriched = injectUserInfo(token); + const enriched = injectUserInfo(credentials); await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched); const { given_name, picture } = enriched.userInfo; resolve({ -- cgit v1.2.3-70-g09d2 From c56b602e892707dbc7e22be2edba75f49a465ec7 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 15:48:54 -0400 Subject: server utils beginning commenting --- src/server/apis/google/GoogleApiServerUtils.ts | 168 ++++++++++++++++++++----- 1 file changed, 139 insertions(+), 29 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 4e5175a2b..9071b0485 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -7,10 +7,21 @@ import { GaxiosResponse } from "gaxios"; import request = require('request-promise'); import * as qs from 'query-string'; import { Database } from "../../database"; -const path = require("path"); +import path from "path"; +/** + * + */ const prefix = 'https://www.googleapis.com/auth/'; + +/** + * + */ const refreshEndpoint = "https://oauth2.googleapis.com/token"; + +/** + * + */ const SCOPES = [ 'documents.readonly', 'documents', @@ -25,23 +36,31 @@ const SCOPES = [ ]; /** - * Server side authentication for Google Api queries. + * This namespace manages server side authentication for Google API queries, either + * from the standard v1 APIs or the Google Photos REST API. */ export namespace GoogleApiServerUtils { - export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; - + /** + * + */ export interface GoogleAuthenticationResult { access_token: string; avatar: string; name: string; } + /** + * + */ export interface CredentialsResult { credentials: Credentials; refreshed: boolean; } + /** + * + */ export interface UserInfo { at_hash: string; aud: string; @@ -57,9 +76,74 @@ export namespace GoogleApiServerUtils { sub: string; } + /** + * + */ + export enum Service { + Documents = "Documents", + Slides = "Slides" + } + + /** + * + */ + export interface CredentialInformation { + credentialsPath: string; + userId: string; + } + + /** + * + */ let installed: OAuth2ClientOptions; + + /** + * This is a global authorization client that is never + * passed around, and whose credentials are never set. + * Its job is purely to generate new authentication urls + * (users will follow to get to Google's permissions GUI) + * and to use the codes returned from that process to generate the + * initial credentials. + */ let worker: OAuth2Client; + /** + * + */ + export type ApiResponse = Promise; + + /** + * + */ + export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; + + /** + * + */ + export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse; + + /** + * + */ + export type Action = "create" | "retrieve" | "update"; + + /** + * + */ + export interface Endpoint { + get: ApiHandler; + create: ApiHandler; + batchUpdate: ApiHandler; + } + + /** + * + */ + export type EndpointParameters = GlobalOptions & { version: "v1" }; + + /** + * + */ export const loadClientSecret = async () => { return new Promise((resolve, reject) => { readFile(path.join(__dirname, "../../credentials/google_docs_credentials.json"), async (err, projectCredentials) => { @@ -68,36 +152,28 @@ export namespace GoogleApiServerUtils { return console.log('Error loading client secret file:', err); } const { client_secret, client_id, redirect_uris } = JSON.parse(projectCredentials.toString()).installed; - worker = new google.auth.OAuth2({ + // initialize the global authorization client + installed = { clientId: client_id, clientSecret: client_secret, redirectUri: redirect_uris[0] - }); + }; + worker = generateClient(); resolve(); }); }); }; + /** + * + */ const authenticationClients = new Map(); - export enum Service { - Documents = "Documents", - Slides = "Slides" - } - - export interface CredentialInformation { - credentialsPath: string; - userId: string; - } - - export type ApiResponse = Promise; - export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse; - export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse; - export type Action = "create" | "retrieve" | "update"; - - export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler }; - export type EndpointParameters = GlobalOptions & { version: "v1" }; - + /** + * + * @param sector + * @param userId + */ export const GetEndpoint = (sector: string, userId: string) => { return new Promise>(resolve => { retrieveOAuthClient(userId).then(auth => { @@ -116,6 +192,10 @@ export namespace GoogleApiServerUtils { }); }; + /** + * + * @param userId + */ export const retrieveAccessToken = (userId: string): Promise => { return new Promise((resolve, reject) => { retrieveCredentials(userId).then( @@ -125,13 +205,17 @@ export namespace GoogleApiServerUtils { }); }; + /** + * + * @param userId + */ export const retrieveOAuthClient = (userId: string): Promise => { return new Promise((resolve, reject) => { retrieveCredentials(userId).then( ({ credentials, refreshed }) => { let client = authenticationClients.get(userId); if (!client) { - authenticationClients.set(userId, client = generateClientWith(credentials)); + authenticationClients.set(userId, client = generateClient(credentials)); } else if (refreshed) { client.setCredentials(credentials); } @@ -142,12 +226,19 @@ export namespace GoogleApiServerUtils { }); }; - function generateClientWith(credentials: Credentials) { + /** + * + * @param credentials + */ + function generateClient(credentials?: Credentials) { const client = new google.auth.OAuth2(installed); - client.setCredentials(credentials); + credentials && client.setCredentials(credentials); return client; } + /** + * + */ export const generateAuthenticationUrl = async () => { return worker.generateAuthUrl({ access_type: 'offline', @@ -155,6 +246,11 @@ export namespace GoogleApiServerUtils { }); }; + /** + * + * @param userId + * @param authenticationCode + */ export const processNewUser = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { worker.getToken(authenticationCode, async (err, credentials) => { @@ -174,6 +270,11 @@ export namespace GoogleApiServerUtils { }); }; + /** + * + */ + export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; + /** * 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 @@ -187,6 +288,10 @@ export namespace GoogleApiServerUtils { return { ...credentials, userInfo }; }; + /** + * + * @param userId + */ const retrieveCredentials = async (userId: string): Promise => { return new Promise((resolve, reject) => { Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(credentials => { @@ -203,13 +308,18 @@ export namespace GoogleApiServerUtils { }); }; + /** + * + * @param credentials + * @param userId + */ const refreshAccessToken = (credentials: Credentials, userId: string) => { return new Promise(resolve => { let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; let queryParameters = { refreshToken: credentials.refresh_token, - ...installed, - grant_type: "refresh_token" + grant_type: "refresh_token", + ...installed }; let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`; request.post(url, headerParameters).then(async response => { -- cgit v1.2.3-70-g09d2 From c1c919d4d44a40d59f2ec714c143cd8f03ad3481 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Mon, 28 Oct 2019 20:04:51 -0400 Subject: clean up --- src/server/apis/google/GoogleApiServerUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 9071b0485..884487509 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -298,9 +298,9 @@ export namespace GoogleApiServerUtils { if (!credentials) { return reject(); } - if (credentials!.expiry_date! < new Date().getTime()) { + if (credentials.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshAccessToken(credentials!, userId).then(resolve, reject); + return refreshAccessToken(credentials, userId).then(resolve, reject); } // Authentication successful! resolve({ credentials, refreshed: false }); -- cgit v1.2.3-70-g09d2 From ba7568e4fe2e9323a66a91876305f829487bffb9 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 29 Oct 2019 16:00:21 -0400 Subject: beginning commenting --- src/server/apis/google/GoogleApiServerUtils.ts | 57 ++++++++++++++++++-------- 1 file changed, 40 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 884487509..b9984649e 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -41,15 +41,6 @@ const SCOPES = [ */ export namespace GoogleApiServerUtils { - /** - * - */ - export interface GoogleAuthenticationResult { - access_token: string; - avatar: string; - name: string; - } - /** * */ @@ -247,9 +238,30 @@ export namespace GoogleApiServerUtils { }; /** - * - * @param userId - * @param authenticationCode + * This is what we return to the server in processNewUser(), after the + * worker OAuth2Client has used the user-pasted authentication code + * to retrieve an access token and an info token. The avatar is the + * URL to the Google-hosted mono-color, single white letter profile 'image'. + */ + export interface GoogleAuthenticationResult { + access_token: string; + avatar: string; + name: string; + } + + /** + * This method receives the authentication code that the + * user pasted into the overlay in the client side and uses the worker + * and the authentication code to fetch the full set of credentials that + * we'll store in the database for each user. This is called once per + * new account integration. + * @param userId The Dash user id of the user requesting account integration, used to associate the new credentials + * with a Dash user in the googleAuthentication table of the database. + * @param authenticationCode the Google-provided authentication code that the user copied + * from Google's permissions UI and pasted into the overlay. + * @returns the information necessary to authenticate a client side google photos request + * and display basic user information in the overlay on successful authentication. + * This can be expanded as needed by adding properties to the interface GoogleAuthenticationResult. */ export const processNewUser = async (userId: string, authenticationCode: string): Promise => { return new Promise((resolve, reject) => { @@ -271,7 +283,9 @@ export namespace GoogleApiServerUtils { }; /** - * + * This type represents the union of the full set of OAuth2 credentials + * and all of a Google user's publically available information. This is the strucure + * of the JSON object we ultimately store in the googleAuthentication table of the database. */ export type EnrichedCredentials = Credentials & { userInfo: UserInfo }; @@ -282,6 +296,8 @@ export namespace GoogleApiServerUtils { * base64 decode with atob and parse the JSON. * @param credentials the client credentials returned from OAuth after the user * has executed the authentication routine + * @returns the full set of credentials in the structure in which they'll be stored + * in the database. */ const injectUserInfo = (credentials: Credentials): EnrichedCredentials => { const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1])); @@ -289,8 +305,12 @@ export namespace GoogleApiServerUtils { }; /** - * - * @param userId + * Looks in the database for any credentials object with the given user id, + * and returns them. If the credentials are found but expired, the function will + * automatically refresh the credentials and then resolve with the updated values. + * @param userId the id of the Dash user requesting his/her credentials. Eventually + * might have multiple. + * @returns the credentials and whether or not they were updated in the process */ const retrieveCredentials = async (userId: string): Promise => { return new Promise((resolve, reject) => { @@ -309,8 +329,11 @@ export namespace GoogleApiServerUtils { }; /** - * - * @param credentials + * This function submits a request to OAuth with the local refresh token + * to revalidate the credentials for a given Google user associated with + * the Dash user id passed in. In addition to returning the credentials, it + * writes the diff to the database. + * @param credentials the credentials * @param userId */ const refreshAccessToken = (credentials: Credentials, userId: string) => { -- cgit v1.2.3-70-g09d2