aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorSam Wilkins <samwilkins333@gmail.com>2019-10-29 23:26:20 -0400
committerSam Wilkins <samwilkins333@gmail.com>2019-10-29 23:26:20 -0400
commitd4d8c2835c8e1e943f77a14e2b87df05f5848dbd (patch)
treeeeac829fc9ab5a5baaee0e553cae3920093e4b2c /src
parentaf25eaf2a848278a58f0993cba2e68c05da0760c (diff)
finished cleaning and commenting GoogleApiServerUtils
Diffstat (limited to 'src')
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts233
1 files changed, 114 insertions, 119 deletions
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index ff5dc7081..ec7c2cfe1 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -2,7 +2,6 @@ import { google } from "googleapis";
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";
import request = require('request-promise');
import * as qs from 'query-string';
@@ -10,19 +9,12 @@ import { Database } from "../../database";
import * as path from "path";
/**
- *
+ * Scopes give Google users fine granularity of control
+ * over the information they make accessible via the API.
+ * This is the somewhat overkill list of what Dash requests
+ * from the user.
*/
-const prefix = 'https://www.googleapis.com/auth/';
-
-/**
- *
- */
-const refreshEndpoint = "https://oauth2.googleapis.com/token";
-
-/**
- *
- */
-const SCOPES = [
+const scope = [
'documents.readonly',
'documents',
'presentations',
@@ -33,7 +25,7 @@ const SCOPES = [
'photoslibrary.appendonly',
'photoslibrary.sharing',
'userinfo.profile'
-];
+].map(relative => `https://www.googleapis.com/auth/${relative}`);
/**
* This namespace manages server side authentication for Google API queries, either
@@ -42,33 +34,9 @@ const SCOPES = [
export namespace GoogleApiServerUtils {
/**
- *
- */
- export interface CredentialsResult {
- credentials: Opt<Credentials>;
- refreshed: boolean;
- }
-
- /**
- *
- */
- 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;
- }
-
- /**
- *
+ * As we expand out to more Google APIs that are accessible from
+ * the 'googleapis' module imported above, this enum will record
+ * the list and provide a unified string representation of each API.
*/
export enum Service {
Documents = "Documents",
@@ -76,15 +44,10 @@ export namespace GoogleApiServerUtils {
}
/**
- *
- */
- export interface CredentialInformation {
- credentialsPath: string;
- userId: string;
- }
-
- /**
- *
+ * Global credentials read once from a JSON file
+ * before the server is started that
+ * allow us to build OAuth2 clients with Dash's
+ * application specific credentials.
*/
let installed: OAuth2ClientOptions;
@@ -99,27 +62,33 @@ export namespace GoogleApiServerUtils {
let worker: OAuth2Client;
/**
- *
+ * A briefer format for the response from a 'googleapis' API request
*/
export type ApiResponse = Promise<GaxiosResponse>;
/**
- *
+ * A generic form for a handler that executes some request on the endpoint
*/
export type ApiRouter = (endpoint: Endpoint, parameters: any) => ApiResponse;
/**
- *
+ * A generic form for the asynchronous function that actually submits the
+ * request to the API and returns the corresporing response. Helpful when
+ * making an extensible endpoint definition.
*/
export type ApiHandler = (parameters: any, methodOptions?: any) => ApiResponse;
/**
- *
+ * A literal union type indicating the valid actions for these 'googleapis'
+ * requestions
*/
export type Action = "create" | "retrieve" | "update";
/**
- *
+ * An interface defining any entity on which one can invoke
+ * anuy of the following handlers. All 'googleapis' wrappers
+ * such as google.docs().documents and google.slides().presentations
+ * satisfy this interface.
*/
export interface Endpoint {
get: ApiHandler;
@@ -128,12 +97,10 @@ export namespace GoogleApiServerUtils {
}
/**
- *
- */
- export type EndpointParameters = GlobalOptions & { version: "v1" };
-
- /**
- *
+ * This function is called once before the server is started,
+ * reading in Dash's project-specific credentials (client secret
+ * and client id) for later repeated access. It also sets up the
+ * global, intentionally unauthenticated worker OAuth2 client instance.
*/
export async function loadClientSecret(): Promise<void> {
return new Promise((resolve, reject) => {
@@ -156,75 +123,83 @@ export namespace GoogleApiServerUtils {
}
/**
- *
+ * Maps the Dash user id of a given user to their single
+ * associated OAuth2 client, mitigating the creation
+ * of needless duplicate clients that would arise from
+ * making one new client instance per request.
*/
const authenticationClients = new Map<String, OAuth2Client>();
/**
- *
- * @param sector
- * @param userId
+ * This function receives the target sector ("which G-Suite app's API am I interested in?")
+ * and the id of the Dash user making the request to the API. With this information, it generates
+ * an authenticated OAuth2 client and passes it into the relevant 'googleapis' wrapper.
+ * @param sector the particular desired G-Suite 'googleapis' API (docs, slides, etc.)
+ * @param userId the id of the Dash user making the request to the API
+ * @returns the relevant 'googleapis' wrapper, if any
*/
export async function GetEndpoint(sector: string, userId: string): Promise<Opt<Endpoint>> {
- return new Promise(resolve => {
- retrieveOAuthClient(userId).then(auth => {
- if (!auth) {
- return resolve();
- }
- let routed: Opt<Endpoint>;
- let parameters: EndpointParameters = { auth, version: "v1" };
- switch (sector) {
- case Service.Documents:
- routed = google.docs(parameters).documents;
- break;
- case Service.Slides:
- routed = google.slides(parameters).presentations;
- break;
- }
- resolve(routed);
- });
+ return new Promise(async resolve => {
+ const auth = await retrieveOAuthClient(userId);
+ if (!auth) {
+ return resolve();
+ }
+ let routed: Opt<Endpoint>;
+ let parameters: any = { auth, version: "v1" };
+ switch (sector) {
+ case Service.Documents:
+ routed = google.docs(parameters).documents;
+ break;
+ case Service.Slides:
+ routed = google.slides(parameters).presentations;
+ break;
+ }
+ resolve(routed);
});
}
/**
- *
- * @param userId
+ * Returns the lengthy string or access token that can be passed into
+ * the headers of an API request or into the constructor of the Photos
+ * client API wrapper.
+ * @param userId the Dash user id of the user requesting his/her associated
+ * access_token
+ * @returns the current access_token associated with the requesting
+ * Dash user. The access_token is valid for only an hour, and
+ * is then refreshed.
*/
export async function retrieveAccessToken(userId: string): Promise<string> {
- return new Promise(resolve => {
- retrieveCredentials(userId).then(
- ({ credentials }) => {
- if (credentials) {
- return resolve(credentials.access_token!);
- }
- resolve();
- }
- );
+ return new Promise(async resolve => {
+ const { credentials } = await retrieveCredentials(userId);
+ if (!credentials) {
+ return resolve();
+ }
+ resolve(credentials.access_token!);
});
}
/**
- * Returns an initialized OAuth2 client instance, likely to be passed into Google's
+ * Manipulates a mapping such that, in the limit, each Dash user has
+ * an associated authenticated OAuth2 client at their disposal. This
+ * function ensures that the client's credentials always remain up to date
+ * @param userId the Dash user id of the user requesting account integration
+ * @returns returns an initialized OAuth2 client instance, likely to be passed into Google's
* npm-installed API wrappers that use authenticated client instances rather than access codes for
* security.
- * @param userId the Dash user id of the user requesting account integration
*/
export async function retrieveOAuthClient(userId: string): Promise<OAuth2Client> {
- return new Promise((resolve, reject) => {
- retrieveCredentials(userId).then(
- ({ credentials, refreshed }) => {
- if (!credentials) {
- return resolve();
- }
- let client = authenticationClients.get(userId);
- if (!client) {
- authenticationClients.set(userId, client = generateClient(credentials));
- } else if (refreshed) {
- client.setCredentials(credentials);
- }
- resolve(client);
- }
- );
+ return new Promise(async resolve => {
+ const { credentials, refreshed } = await retrieveCredentials(userId);
+ if (!credentials) {
+ return resolve();
+ }
+ let client = authenticationClients.get(userId);
+ if (!client) {
+ authenticationClients.set(userId, client = generateClient(credentials));
+ } else if (refreshed) {
+ client.setCredentials(credentials);
+ }
+ resolve(client);
});
}
@@ -233,6 +208,7 @@ export namespace GoogleApiServerUtils {
* the specific credentials on the client
* @param credentials if you have access to the credentials that you'll eventually set on
* the client, just pass them in at initialization
+ * @returns the newly created, potentially certified, OAuth2 client instance
*/
function generateClient(credentials?: Credentials): OAuth2Client {
const client = new google.auth.OAuth2(installed);
@@ -244,12 +220,10 @@ export namespace GoogleApiServerUtils {
* Calls on the worker (which does not have and does not need
* any credentials) to produce a url to which the user can
* navigate to give Dash the necessary Google permissions.
+ * @returns the newly generated url to the authentication landing page
*/
export function generateAuthenticationUrl(): string {
- return worker.generateAuthUrl({
- access_type: 'offline',
- scope: SCOPES.map(relative => prefix + relative),
- });
+ return worker.generateAuthUrl({ scope, access_type: 'offline' });
}
/**
@@ -306,6 +280,26 @@ export namespace GoogleApiServerUtils {
export type EnrichedCredentials = Credentials & { userInfo: UserInfo };
/**
+ * This interface defines all of the information we
+ * receive from parsing the base64 encoded info-token
+ * for a Google user.
+ */
+ 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;
+ }
+
+ /**
* 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,
@@ -316,7 +310,7 @@ export namespace GoogleApiServerUtils {
* in the database.
*/
function injectUserInfo(credentials: Credentials): EnrichedCredentials {
- const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
+ const userInfo: UserInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
return { ...credentials, userInfo };
}
@@ -326,15 +320,16 @@ export namespace GoogleApiServerUtils {
* automatically refresh the credentials and then resolve with the updated values.
* @param userId the id of the Dash user requesting his/her credentials. Eventually, each user might
* be associated with multiple different sets of Google credentials.
- * @returns the credentials and a flag indicating whether or not they were refreshed during retrieval
+ * @returns the credentials, or undefined if the user has no stored associated credentials,
+ * and a flag indicating whether or not they were refreshed during retrieval
*/
- async function retrieveCredentials(userId: string): Promise<CredentialsResult> {
+ async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> {
let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
let refreshed = false;
if (!credentials) {
return { credentials: undefined, refreshed };
}
- // if the token has expired, submit a request for a refreshed access token
+ // check for token expiry
if (credentials.expiry_date! <= new Date().getTime()) {
credentials = await refreshAccessToken(credentials, userId);
}
@@ -353,7 +348,7 @@ export namespace GoogleApiServerUtils {
*/
async function refreshAccessToken(credentials: Credentials, userId: string): Promise<Credentials> {
let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
- let url = `${refreshEndpoint}?${qs.stringify({
+ let url = `https://oauth2.googleapis.com/token?${qs.stringify({
refreshToken: credentials.refresh_token,
grant_type: "refresh_token",
...installed