diff options
author | Sam Wilkins <samwilkins333@gmail.com> | 2019-09-03 12:51:59 -0400 |
---|---|---|
committer | Sam Wilkins <samwilkins333@gmail.com> | 2019-09-03 12:51:59 -0400 |
commit | 38176e5ba949b84dc410d29197180121d81e085c (patch) | |
tree | fc16f9dadd97d76c8431640bab27cfa8bb75421f | |
parent | 8b992ef2c152e86299fd3460112124d476393a60 (diff) |
implemented refresh tokens and create, get, list
-rw-r--r-- | src/client/apis/google_docs/GooglePhotosClientUtils.ts | 35 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 10 | ||||
-rw-r--r-- | src/server/RouteStore.ts | 3 | ||||
-rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 90 | ||||
-rw-r--r-- | src/server/apis/google/GooglePhotosServerUtils.ts | 68 | ||||
-rw-r--r-- | src/server/apis/google/GooglePhotosUploadUtils.ts | 42 | ||||
-rw-r--r-- | src/server/apis/google/GooglePhotosUtils.ts | 21 | ||||
-rw-r--r-- | src/server/apis/google/typings/albums.ts | 30 | ||||
-rw-r--r-- | src/server/credentials/google_docs_token.json | 2 | ||||
-rw-r--r-- | src/server/index.ts | 54 |
10 files changed, 252 insertions, 103 deletions
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts new file mode 100644 index 000000000..67a282f48 --- /dev/null +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -0,0 +1,35 @@ +import { Album } from "../../../server/apis/google/typings/albums"; +import { PostToServer } from "../../../Utils"; +import { RouteStore } from "../../../server/RouteStore"; + +export namespace GooglePhotosClientUtils { + + export const Create = async (title: string) => { + let parameters = { + action: Album.Action.Create, + body: { album: { title } } + } as Album.Create; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + + export const List = async (options?: Partial<Album.ListOptions>) => { + let parameters = { + action: Album.Action.List, + parameters: { + pageSize: (options ? options.pageSize : 20) || 20, + pageToken: (options ? options.pageToken : undefined) || undefined, + excludeNonAppCreatedData: (options ? options.excludeNonAppCreatedData : false) || false, + } as Album.ListOptions + } as Album.List; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + + export const Get = async (albumId: string) => { + let parameters = { + action: Album.Action.Get, + albumId + } as Album.Get; + return PostToServer(RouteStore.googlePhotos, parameters); + }; + +}
\ No newline at end of file diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index df0718072..ece475c80 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -42,6 +42,8 @@ import PresModeMenu from './presentationview/PresentationModeMenu'; import { PresBox } from './nodes/PresBox'; import { GoogleApiClientUtils } from '../apis/google_docs/GoogleApiClientUtils'; import { docs_v1 } from 'googleapis'; +import { Album } from '../../server/apis/google/typings/albums'; +import { GooglePhotosClientUtils } from '../apis/google_docs/GooglePhotosClientUtils'; @observer export class MainView extends React.Component { @@ -128,7 +130,7 @@ export class MainView extends React.Component { window.removeEventListener("keydown", KeyManager.Instance.handle); window.addEventListener("keydown", KeyManager.Instance.handle); - PostToServer('/googleDocs/Photos/Test', {}); + this.executeGooglePhotosAlbumTestRoutine(); reaction(() => { let workspaces = CurrentUserUtils.UserDocument.workspaces; @@ -147,6 +149,12 @@ export class MainView extends React.Component { }, { fireImmediately: true }); } + executeGooglePhotosAlbumTestRoutine = async () => { + let title = "This is a generically created album!"; + console.log(await GooglePhotosClientUtils.Create(title)); + console.log(await GooglePhotosClientUtils.List({ pageSize: 50 })); + } + componentWillUnMount() { window.removeEventListener("keydown", KeyManager.Instance.handle); //close presentation diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 014906054..fc5511f98 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -31,6 +31,7 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", - googleDocs = "/googleDocs" + googleDocs = "/googleDocs", + googlePhotos = "/googlePhotos" }
\ No newline at end of file diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 00e289b00..048ac4b21 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -1,13 +1,12 @@ -import { google, docs_v1, slides_v1 } from "googleapis"; +import { google } from "googleapis"; import { createInterface } from "readline"; import { readFile, writeFile } from "fs"; import { OAuth2Client, Credentials } from "google-auth-library"; import { Opt } from "../../../new_fields/Doc"; import { GlobalOptions } from "googleapis-common"; import { GaxiosResponse } from "gaxios"; -import { GooglePhotos } from "./GooglePhotosUtils"; -import { Utils } from "../../../Utils"; -import { Album } from "./typings/albums"; +import request = require('request-promise'); +import * as qs from 'query-string'; /** * Server side authentication for Google Api queries. @@ -31,8 +30,7 @@ export namespace GoogleApiServerUtils { export enum Service { Documents = "Documents", - Slides = "Slides", - Photos = "Photos" + Slides = "Slides" } export interface CredentialPaths { @@ -49,38 +47,31 @@ export namespace GoogleApiServerUtils { export type EndpointParameters = GlobalOptions & { version: "v1" }; export const GetEndpoint = async (sector: string, paths: CredentialPaths) => { - return new Promise<Opt<Endpoint>>((resolve, reject) => { + return new Promise<Opt<Endpoint>>(resolve => { + RetrieveAuthenticationInformation(paths).then(authentication => { + let routed: Opt<Endpoint>; + let parameters: EndpointParameters = { auth: authentication.client, version: "v1" }; + switch (sector) { + case Service.Documents: + routed = google.docs(parameters).documents; + break; + case Service.Slides: + routed = google.slides(parameters).presentations; + break; + } + resolve(routed); + }); + }); + }; + + export const RetrieveAuthenticationInformation = async (paths: CredentialPaths) => { + return new Promise<TokenResult>((resolve, reject) => { readFile(paths.credentials, async (err, credentials) => { if (err) { reject(err); return console.log('Error loading client secret file:', err); } - authorize(parseBuffer(credentials), paths.token).then(async result => { - let routed: Opt<Endpoint>; - let parameters: EndpointParameters = { auth: result.client, version: "v1" }; - switch (sector) { - case Service.Documents: - routed = google.docs(parameters).documents; - break; - case Service.Slides: - routed = google.slides(parameters).presentations; - break; - case Service.Photos: - let token = result.token.access_token; - if (token) { - let create: Album.Create = { - action: Album.Action.Create, - body: { - album: { - title: "Sam's Bulk Export", - } - } - }; - console.log(await GooglePhotos.ExecuteQuery(token, create)); - } - } - resolve(routed); - }); + authorize(parseBuffer(credentials), paths.token).then(resolve, reject); }); }); }; @@ -101,13 +92,44 @@ export namespace GoogleApiServerUtils { if (err) { return getNewToken(oAuth2Client, token_path).then(resolve, reject); } - let parsed = parseBuffer(token); + let parsed: Credentials = parseBuffer(token); + if (parsed.expiry_date! < new Date().getTime()) { + return refreshToken(parsed, client_id, client_secret, oAuth2Client, token_path).then(resolve, reject); + } oAuth2Client.setCredentials(parsed); resolve({ token: parsed, client: oAuth2Client }); }); }); } + const refreshEndpoint = "https://oauth2.googleapis.com/token"; + const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, token_path: string) => { + return new Promise<TokenResult>((resolve, reject) => { + let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }; + let queryParameters = { + refreshToken: credentials.refresh_token, + client_id, + client_secret, + grant_type: "refresh_token" + }; + let url = `${refreshEndpoint}?${qs.stringify(queryParameters)}`; + request.post(url, headerParameters).then(response => { + let parsed = JSON.parse(response); + credentials.access_token = parsed.access_token; + credentials.expiry_date = new Date().getTime() + parsed.expires_in; + writeFile(token_path, JSON.stringify(credentials), (err) => { + if (err) { + console.error(err); + reject(err); + } + console.log('Refreshed token stored to', token_path); + oAuth2Client.setCredentials(credentials); + resolve({ token: credentials, client: oAuth2Client }); + }); + }); + }); + }; + /** * Get and store new token after prompting for user authorization, and then * execute the given callback with the authorized OAuth2 client. diff --git a/src/server/apis/google/GooglePhotosServerUtils.ts b/src/server/apis/google/GooglePhotosServerUtils.ts new file mode 100644 index 000000000..cb5464abc --- /dev/null +++ b/src/server/apis/google/GooglePhotosServerUtils.ts @@ -0,0 +1,68 @@ +import request = require('request-promise'); +import { Album } from './typings/albums'; +import * as qs from 'query-string'; + +const apiEndpoint = "https://photoslibrary.googleapis.com/v1/"; + +export interface Authorization { + token: string; +} + +export namespace GooglePhotos { + + export type Query = Album.Query; + export type QueryParameters = { query: GooglePhotos.Query }; + interface DispatchParameters { + required: boolean; + method: "GET" | "POST"; + ignore?: boolean; + } + + export const ExecuteQuery = async (parameters: Authorization & QueryParameters): Promise<any> => { + let action = parameters.query.action; + let dispatch = SuffixMap.get(action)!; + let suffix = Suffix(parameters, dispatch, action); + if (suffix) { + let query: any = parameters.query; + let options: any = { + headers: { 'Content-Type': 'application/json' }, + auth: { 'bearer': parameters.token }, + }; + if (query.body) { + options.body = query.body; + options.json = true; + } + let queryParameters = query.parameters; + if (queryParameters) { + suffix += `?${qs.stringify(queryParameters)}`; + } + let dispatcher = dispatch.method === "POST" ? request.post : request.get; + return dispatcher(apiEndpoint + suffix, options); + } + }; + + const Suffix = (parameters: QueryParameters, dispatch: DispatchParameters, action: Album.Action) => { + let query: any = parameters.query; + let id = query.albumId; + let suffix = 'albums'; + if (dispatch.required) { + if (!id) { + return undefined; + } + suffix += `/${id}${dispatch.ignore ? "" : `:${action}`}`; + } + return suffix; + }; + + const SuffixMap = new Map<Album.Action, DispatchParameters>([ + [Album.Action.AddEnrichment, { required: true, method: "POST" }], + [Album.Action.BatchAddMediaItems, { required: true, method: "POST" }], + [Album.Action.BatchRemoveMediaItems, { required: true, method: "POST" }], + [Album.Action.Create, { required: false, method: "POST" }], + [Album.Action.Get, { required: true, ignore: true, method: "GET" }], + [Album.Action.List, { required: false, method: "GET" }], + [Album.Action.Share, { required: true, method: "POST" }], + [Album.Action.Unshare, { required: true, method: "POST" }] + ]); + +} diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts new file mode 100644 index 000000000..2e1599aaf --- /dev/null +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -0,0 +1,42 @@ +import request = require('request-promise'); +import { Authorization } from './GooglePhotosServerUtils'; + +export namespace GooglePhotosUploadUtils { + + interface UploadInformation { + title: string; + url: URL; + } + + const apiEndpoint = "https://photoslibrary.googleapis.com/v1/uploads"; + + export const SubmitUpload = async (parameters: Authorization & UploadInformation) => { + let MEDIA_BINARY_DATA = binary(parameters.url.href); + + let options = { + headers: { + 'Content-Type': 'application/octet-stream', + Authorization: { 'bearer': parameters.token }, + 'X-Goog-Upload-File-Name': parameters.title, + 'X-Goog-Upload-Protocol': 'raw' + }, + body: { MEDIA_BINARY_DATA }, + json: true + }; + const result = await request.post(apiEndpoint, options); + return result; + }; + + const binary = (source: string) => { + const image = document.createElement("img"); + image.src = source; + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + const ctx = canvas.getContext("2d")!; + ctx.drawImage(image, 0, 0); + const dataUrl = canvas.toDataURL("image/png"); + return dataUrl.replace(/^data:image\/(png|jpg);base64,/, ""); + }; + +}
\ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUtils.ts b/src/server/apis/google/GooglePhotosUtils.ts deleted file mode 100644 index 750630626..000000000 --- a/src/server/apis/google/GooglePhotosUtils.ts +++ /dev/null @@ -1,21 +0,0 @@ -import request = require('request-promise'); -import { Album } from './typings/albums'; - -const apiEndpoint = "https://photoslibrary.googleapis.com"; - -export namespace GooglePhotos { - - export type Query = Album.Query; - - export const ExecuteQuery = async (authToken: string, query: GooglePhotos.Query) => { - let options = { - headers: { 'Content-Type': 'application/json' }, - auth: { 'bearer': authToken }, - body: query.body, - json: true - }; - const result = await request.post(apiEndpoint + '/v1/albums', options); - return result; - }; - -} diff --git a/src/server/apis/google/typings/albums.ts b/src/server/apis/google/typings/albums.ts index 1c9b379fe..f3025567d 100644 --- a/src/server/apis/google/typings/albums.ts +++ b/src/server/apis/google/typings/albums.ts @@ -1,16 +1,16 @@ export namespace Album { - export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare) & { body: any }; + export type Query = (AddEnrichment | BatchAddMediaItems | BatchRemoveMediaItems | Create | Get | List | Share | Unshare); export enum Action { - AddEnrichment, - BatchAddMediaItems, - BatchRemoveMediaItems, - Create, - Get, - List, - Share, - Unshare + AddEnrichment = "addEnrichment", + BatchAddMediaItems = "batchAddMediaItems", + BatchRemoveMediaItems = "batchRemoveMediaItems", + Create = "create", + Get = "get", + List = "list", + Share = "share", + Unshare = "unshare" } export interface AddEnrichment { @@ -52,11 +52,13 @@ export namespace Album { export interface List { action: Action.List; - parameters: { - pageSize: number, - pageToken: string, - excludeNonAppCreatedData: boolean - }; + parameters: ListOptions; + } + + export interface ListOptions { + pageSize: number; + pageToken: string; + excludeNonAppCreatedData: boolean; } export interface Share { diff --git a/src/server/credentials/google_docs_token.json b/src/server/credentials/google_docs_token.json index 61864512c..39e067c86 100644 --- a/src/server/credentials/google_docs_token.json +++ b/src/server/credentials/google_docs_token.json @@ -1 +1 @@ -{"access_token":"ya29.Glt3B8HoVEda7Ab5TQMVrfvjPN2fFp4sFHtGoDs3TsBgFfw4G208q90JiFjkmQqwODjJi3sf4NCZd78VZTVL3aI0By7_ElZF7XaCvA0LJnfcAi2gi1P-2-boyjYO","refresh_token":"1/tJOVDbPZlADzd2B8Q2_j7jqignXlRwHsU7LbZkdbDBc","scope":"https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567374969108}
\ No newline at end of file +{"access_token":"ya29.Glx4B9p8tvFsmsD-AD-D4jygL_YZVCFpPewM9djtfOT3T4S6ROxN5r0WLAKTYVNnXQbUri3Gu_-vIb0NWq9wEy1TdFTLIM8azWD82X5-I5BQq2DSOsYiKugvgVoHLw","refresh_token":"1/kk_pPY7WBwT34JNPzx_HrSVoZlvfzys4EEVNjp7nqzg6aIbRrEKNMRTb0u0wr9GM","scope":"https://www.googleapis.com/auth/drive.file https://www.googleapis.com/auth/documents.readonly https://www.googleapis.com/auth/documents https://www.googleapis.com/auth/photoslibrary.sharing https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/presentations https://www.googleapis.com/auth/presentations.readonly https://www.googleapis.com/auth/photoslibrary","token_type":"Bearer","expiry_date":1567529401142}
\ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index 1f105e9d2..54b954cfb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -42,11 +42,7 @@ var AdmZip = require('adm-zip'); import * as YoutubeApi from "./apis/youtube/youtubeApiSample"; import { Response } from 'express-serve-static-core'; import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -// import { GaxiosResponse } from 'gaxios'; -// import { Opt } from '../new_fields/Doc'; -// import { docs_v1 } from 'googleapis'; -// import { Endpoint } from 'googleapis-common'; -// import { PhotosLibraryQuery } from './apis/google/GooglePhotosUtils'; +import { GooglePhotos } from './apis/google/GooglePhotosServerUtils'; const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); const probe = require("probe-image-size"); @@ -195,32 +191,7 @@ app.get("/version", (req, res) => { // SEARCH const solrURL = "http://localhost:8983/solr/#/dash"; -// GETTERS - -// app.get('/auth/google', passport.authenticate('google', { -// scope: OAuthConfig.scopes, -// failureFlash: true, // Display errors to the user. -// session: true, -// })); - -// app.get("/failed", (req, res) => res.send("DIDN'T WORK!")); - -// app.get( -// '/auth/google/callback', -// passport.authenticate( -// 'google', { failureRedirect: '/failed', failureFlash: true, session: true }), -// (req, res) => { -// // User has logged in. -// console.log('OAUTH: user has logged in 1.'); -// PhotosLibraryQuery(req.user.token, {}); -// console.log('OAUTH: user has logged in 2.'); -// res.redirect('/'); -// }); - -// app.get('/GooglePhotos', (req, res) => { -// console.log("WORKING ON GOOGLE PHOTOS"); -// PhotosLibraryQuery(req.user.token, {}); -// }); +// GETTERSÃ¥ app.get("/search", async (req, res) => { const solrQuery: any = {}; @@ -853,6 +824,27 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); +app.post(RouteStore.googlePhotos, (req, res) => { + GoogleApiServerUtils.RetrieveAuthenticationInformation({ credentials, token }).then(authentication => { + let validated = authentication.token.access_token; + if (!validated) { + res.send("Error: unable to authenticate Google Photos API request."); + return; + } + GooglePhotos.ExecuteQuery({ token: validated, query: req.body }) + .then(response => { + if (response === undefined) { + res.send("Error: unable to build suffix for Google Photos API request"); + return; + } + res.send(response); + }) + .catch(error => { + res.send(`Error: an exception occurred in the execution of this Google Photos API request\n${error}`); + }); + }); +}); + const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { "number": "_n", "string": "_t", |