From 0538db262ff611c5273c363470886d2aa42cf760 Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Tue, 1 Oct 2019 20:31:27 -0400 Subject: initial commit --- src/server/RouteStore.ts | 1 + 1 file changed, 1 insertion(+) (limited to 'src/server/RouteStore.ts') diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index ee9cd8a0e..1e1dd6300 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -13,6 +13,7 @@ export enum RouteStore { upload = "/upload", dataUriToImage = "/uploadURI", images = "/images", + inspectImage = "/inspectImage", // USER AND WORKSPACES getCurrUser = "/getCurrentUser", -- cgit v1.2.3-70-g09d2 From c17fba4c07a1cc0e40d08228b4cdc4182615496b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Sat, 5 Oct 2019 15:31:25 -0400 Subject: beginning external file download --- src/client/util/Import & Export/ImageUtils.ts | 33 +++++++++++++++++++++++++-- src/server/RouteStore.ts | 1 + src/server/index.ts | 10 ++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) (limited to 'src/server/RouteStore.ts') diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 33ca55aa9..bf482aea8 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,9 +1,11 @@ -import { Doc } from "../../../new_fields/Doc"; +import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc"; import { ImageField } from "../../../new_fields/URLField"; -import { Cast } from "../../../new_fields/Types"; +import { Cast, StrCast } from "../../../new_fields/Types"; import { RouteStore } from "../../../server/RouteStore"; import { Docs } from "../../documents/Documents"; import { Identified } from "../../Network"; +import { Id } from "../../../new_fields/FieldSymbols"; +import { Utils } from "../../../Utils"; export namespace ImageUtils { @@ -19,4 +21,31 @@ export namespace ImageUtils { return data !== undefined; }; + export type Hierarchy = { [id: string]: string | Hierarchy }; + + export const ExportHierarchyToFileSystem = async (doc: Doc): Promise => { + const hierarchy: Hierarchy = {}; + await HierarchyTraverserRecursive(doc, hierarchy); + const a = document.createElement("a"); + a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${JSON.stringify(hierarchy)}`); + a.download = `Full Export of ${StrCast(doc.title)}`; + a.click(); + }; + + const HierarchyTraverserRecursive = async (collection: Doc, hierarchy: Hierarchy) => { + const children = await DocListCastAsync(collection.data); + if (children) { + const local: Hierarchy = {}; + hierarchy[collection[Id]] = local; + for (const child of children) { + let imageData: Opt; + if (imageData = Cast(child.data, ImageField)) { + local[child[Id]] = imageData.url.href; + } else { + await HierarchyTraverserRecursive(child, local); + } + } + } + }; + } \ No newline at end of file diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 1e1dd6300..8a9a6baae 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -14,6 +14,7 @@ export enum RouteStore { dataUriToImage = "/uploadURI", images = "/images", inspectImage = "/inspectImage", + imageHierarchyExport = "/imageHierarchyExport", // USER AND WORKSPACES getCurrUser = "/getCurrentUser", diff --git a/src/server/index.ts b/src/server/index.ts index 1ca5d6e11..f9ca3de56 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -320,6 +320,16 @@ app.get("/serializeDoc/:docId", async (req, res) => { res.send({ docs, files: Array.from(files) }); }); +app.get(`${RouteStore.imageHierarchyExport}/:hierarchy`, async (req, res) => { + const hierarchy = JSON.parse(req.params.hierarchy); + Object.keys(hierarchy).map(key => { + let value: any; + if (value = hierarchy[key]) { + + } + }); +}); + app.get("/downloadId/:docId", async (req, res) => { res.set('Content-disposition', `attachment;`); res.set('Content-Type', "application/zip"); -- cgit v1.2.3-70-g09d2 From 45b97084b3d49d521ff39963f250e9cd9efe3f8e Mon Sep 17 00:00:00 2001 From: Sam Wilkins <35748010+samwilkins333@users.noreply.github.com> Date: Wed, 9 Oct 2019 05:00:23 -0400 Subject: client side google api authentication UI --- src/client/apis/AuthenticationManager.tsx | 90 ++++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 43 ++++++++--- src/client/views/MainView.tsx | 2 + src/server/DashUploadUtils.ts | 2 +- src/server/RouteStore.ts | 3 +- src/server/apis/google/GoogleApiServerUtils.ts | 79 +++++++++---------- src/server/apis/google/GooglePhotosUploadUtils.ts | 4 + src/server/index.ts | 31 ++++++-- 8 files changed, 194 insertions(+), 60 deletions(-) create mode 100644 src/client/apis/AuthenticationManager.tsx (limited to 'src/server/RouteStore.ts') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx new file mode 100644 index 000000000..75a50d8f9 --- /dev/null +++ b/src/client/apis/AuthenticationManager.tsx @@ -0,0 +1,90 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; + +@observer +export default class AuthenticationManager extends React.Component<{}> { + public static Instance: AuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + + private get isOpen() { + return this.openState; + } + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private get hasBeenClicked() { + return this.clickedState; + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public executeFullRoutine = async (authenticationLink: string) => { + this.authenticationLink = authenticationLink; + this.isOpen = true; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGooglePhotosAccessToken, { authenticationCode }).then(token => { + this.isOpen = false; + this.hasBeenClicked = false; + resolve(token); + disposer(); + }); + } + } + ); + }); + } + + constructor(props: {}) { + super(props); + AuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + this.hasBeenClicked = true; + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }) + + private get renderPrompt() { + return ( +
+ + {this.clickedState ? : (null)} +
+ ) + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 29cc042b6..dd1492f51 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -13,14 +13,20 @@ import { Docs, DocumentOptions } from "../../documents/Documents"; import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/SharedTypes"; import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; import { Identified } from "../../Network"; +import AuthenticationManager from "../AuthenticationManager"; +import { List } from "../../../new_fields/List"; export namespace GooglePhotos { + const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; + const endpoint = async () => { - const accessToken = await Identified.FetchFromServer(RouteStore.googlePhotosAccessToken); - return new Photos(accessToken); + let response = await Identified.FetchFromServer(RouteStore.readGooglePhotosAccessToken); + if (new RegExp(AuthenticationUrl).test(response)) { + response = await AuthenticationManager.Instance.executeFullRoutine(response); + } + return new Photos(response); }; export enum MediaType { @@ -89,9 +95,14 @@ export namespace GooglePhotos { } const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); const { id, productUrl } = await Create.Album(resolved); - const newMediaItemResults = await Transactions.UploadImages(images, { id }, descriptionKey); - if (newMediaItemResults) { - const mediaItems = newMediaItemResults.map(item => item.mediaItem); + const response = await Transactions.UploadImages(images, { id }, descriptionKey); + if (response) { + const { results, failed } = response; + let index: Opt; + while ((index = failed.pop()) !== undefined) { + Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]); + } + const mediaItems: MediaItem[] = results.map(item => item.mediaItem); if (mediaItems.length !== images.length) { throw new AssertionError({ actual: mediaItems.length, expected: images.length }); } @@ -99,6 +110,9 @@ export namespace GooglePhotos { for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; + if (!mediaItem) { + continue; + } image.googlePhotosId = mediaItem.id; image.googlePhotosAlbumUrl = productUrl; image.googlePhotosUrl = mediaItem.productUrl || mediaItem.baseUrl; @@ -304,17 +318,22 @@ export namespace GooglePhotos { }; export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { - const newMediaItems = await UploadImages(sources, album, descriptionKey); - if (!newMediaItems) { + const response = await UploadImages(sources, album, descriptionKey); + if (!response) { return undefined; } - const baseUrls: string[] = await Promise.all(newMediaItems.map(item => { + const baseUrls: string[] = await Promise.all(response.results.map(item => { return new Promise(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); })); return baseUrls; }; - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { + export interface ImageUploadResults { + results: NewMediaItemResult[]; + failed: number[]; + } + + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise> => { if (album && "title" in album) { album = await Create.Album(album.title); } @@ -331,8 +350,8 @@ export namespace GooglePhotos { media.push({ url, description }); } if (media.length) { - const uploads: NewMediaItemResult[] = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); - return uploads; + const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album }); + return results; } }; diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3b0457dff..12578e5b8 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,6 +39,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; +import AuthenticationManager from '../apis/AuthenticationManager'; @observer export class MainView extends React.Component { @@ -677,6 +678,7 @@ export class MainView extends React.Component {
{this.dictationOverlay} + {this.mainContent} diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 4230e9b17..57b46714a 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -22,7 +22,7 @@ export namespace DashUploadUtils { const gifs = [".gif"]; const pngs = [".png"]; const jpgs = [".jpg", ".jpeg"]; - const imageFormats = [...pngs, ...jpgs, ...gifs]; + export const imageFormats = [...pngs, ...jpgs, ...gifs]; const videoFormats = [".mov", ".mp4"]; const size = "content-length"; diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index ee9cd8a0e..23fdbc53d 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,7 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - googlePhotosAccessToken = "/googlePhotosAccessToken", + readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", + writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index c899c2ef2..2f29cb95f 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -75,6 +75,42 @@ export namespace GoogleApiServerUtils { }); }; + const RetrieveOAuthClient = async (information: CredentialInformation) => { + return new Promise((resolve, reject) => { + readFile(information.credentialsPath, async (err, credentials) => { + if (err) { + reject(err); + return console.log('Error loading client secret file:', err); + } + const { client_secret, client_id, redirect_uris } = parseBuffer(credentials).installed; + resolve(new google.auth.OAuth2(client_id, client_secret, redirect_uris[0])); + }); + }); + } + + export const GenerateAuthenticationUrl = async (information: CredentialInformation) => { + const client = await RetrieveOAuthClient(information); + return client.generateAuthUrl({ + access_type: 'offline', + scope: SCOPES.map(relative => prefix + relative), + }); + }; + + export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise => { + const oAuth2Client = await RetrieveOAuthClient(information); + return new Promise((resolve, reject) => { + oAuth2Client.getToken(authenticationCode, async (err, token) => { + if (err || !token) { + reject(err); + return console.error('Error retrieving access token', err); + } + oAuth2Client.setCredentials(token); + await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, token); + resolve({ token, client: oAuth2Client }); + }); + }); + } + export const RetrieveCredentials = (information: CredentialInformation) => { return new Promise((resolve, reject) => { readFile(information.credentialsPath, async (err, credentials) => { @@ -107,17 +143,13 @@ export namespace GoogleApiServerUtils { return new Promise((resolve, reject) => { // Attempting to authorize user (${userId}) Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId).then(token => { - if (!token) { - // No token registered, so awaiting input from user - return getNewToken(oAuth2Client, userId).then(resolve, reject); - } - if (token.expiry_date! < new Date().getTime()) { + if (token!.expiry_date! < new Date().getTime()) { // Token has expired, so submitting a request for a refreshed access token - return refreshToken(token, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); + return refreshToken(token!, client_id, client_secret, oAuth2Client, userId).then(resolve, reject); } // Authentication successful! - oAuth2Client.setCredentials(token); - resolve({ token, client: oAuth2Client }); + oAuth2Client.setCredentials(token!); + resolve({ token: token!, client: oAuth2Client }); }); }); } @@ -145,35 +177,4 @@ export namespace GoogleApiServerUtils { }); }; - /** - * Get and store new token after prompting for user authorization, and then - * execute the given callback with the authorized OAuth2 client. - * @param {google.auth.OAuth2} oAuth2Client The OAuth2 client to get token for. - * @param {getEventsCallback} callback The callback for the authorized client. - */ - function getNewToken(oAuth2Client: OAuth2Client, userId: string) { - return new Promise((resolve, reject) => { - const authUrl = oAuth2Client.generateAuthUrl({ - access_type: 'offline', - scope: SCOPES.map(relative => prefix + relative), - }); - console.log('Authorize this app by visiting this url:', authUrl); - const rl = createInterface({ - input: process.stdin, - output: process.stdout, - }); - rl.question('Enter the code from that page here: ', (code) => { - rl.close(); - oAuth2Client.getToken(code, async (err, token) => { - if (err || !token) { - reject(err); - return console.error('Error retrieving access token', err); - } - oAuth2Client.setCredentials(token); - await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, token); - resolve({ token, client: oAuth2Client }); - }); - }); - }); - } } \ No newline at end of file diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts index 16c4f6c3a..4a67e57cc 100644 --- a/src/server/apis/google/GooglePhotosUploadUtils.ts +++ b/src/server/apis/google/GooglePhotosUploadUtils.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { MediaItemCreationResult } from './SharedTypes'; import { NewMediaItem } from "../../index"; import { BatchedArray, TimeUnit } from 'array-batcher'; +import { DashUploadUtils } from '../../DashUploadUtils'; export namespace GooglePhotosUploadUtils { @@ -32,6 +33,9 @@ export namespace GooglePhotosUploadUtils { }; export const DispatchGooglePhotosUpload = async (url: string) => { + if (!DashUploadUtils.imageFormats.includes(path.extname(url))) { + return undefined; + } const body = await request(url, { encoding: null }); const parameters = { method: 'POST', diff --git a/src/server/index.ts b/src/server/index.ts index 690836fff..077002894 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,22 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.googlePhotosAccessToken, (req, res) => GoogleApiServerUtils.RetrieveAccessToken({ credentialsPath, userId: req.header("userId")! }).then(token => res.send(token))); +app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + const information = { credentialsPath, userId }; + if (!token) { + return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information)); + } + GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); +}); + +app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { + const userId = req.header("userId")!; + const information = { credentialsPath, userId }; + const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); + res.send(token.access_token); +}); const tokenError = "Unable to successfully upload bytes for all images!"; const mediaError = "Unable to convert all uploaded bytes to media items!"; @@ -885,16 +900,17 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { await GooglePhotosUploadUtils.initialize({ credentialsPath, userId }); - let failed = 0; + let failed: number[] = []; const newMediaItems = await BatchedArray.from(media, { batchSize: 25 }).batchedMapPatientInterval( { magnitude: 100, unit: TimeUnit.Milliseconds }, async (batch: GooglePhotosUploadUtils.MediaInput[]) => { const newMediaItems: NewMediaItem[] = []; - for (let element of batch) { + for (let index = 0; index < batch.length; index++) { + const element = batch[index]; const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url); if (!uploadToken) { - failed++; + failed.push(index); } else { newMediaItems.push({ description: element.description, @@ -906,12 +922,13 @@ app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => { } ); - if (failed) { - return _error(res, tokenError); + const failedCount = failed.length; + if (failedCount) { + console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`) } GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then( - result => _success(res, result.newMediaItemResults), + result => _success(res, { results: result.newMediaItemResults, failed }), error => _error(res, mediaError, error) ); }); -- cgit v1.2.3-70-g09d2 From 70a142b84d01e89b56d027b24e37122fa90fe25b Mon Sep 17 00:00:00 2001 From: Sam Wilkins Date: Wed, 9 Oct 2019 16:53:16 -0400 Subject: better authentication feedback, cleanup --- src/client/apis/AuthenticationManager.tsx | 90 ---------------- src/client/apis/GoogleAuthenticationManager.scss | 3 + src/client/apis/GoogleAuthenticationManager.tsx | 114 +++++++++++++++++++++ .../apis/google_docs/GooglePhotosClientUtils.ts | 7 +- src/client/util/SharingManager.tsx | 2 +- src/client/views/MainView.tsx | 4 +- src/server/RouteStore.ts | 4 +- src/server/index.ts | 4 +- 8 files changed, 126 insertions(+), 102 deletions(-) delete mode 100644 src/client/apis/AuthenticationManager.tsx create mode 100644 src/client/apis/GoogleAuthenticationManager.scss create mode 100644 src/client/apis/GoogleAuthenticationManager.tsx (limited to 'src/server/RouteStore.ts') diff --git a/src/client/apis/AuthenticationManager.tsx b/src/client/apis/AuthenticationManager.tsx deleted file mode 100644 index d8f6b675b..000000000 --- a/src/client/apis/AuthenticationManager.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { observable, action, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import MainViewModal from "../views/MainViewModal"; -import { Opt } from "../../new_fields/Doc"; -import { Identified } from "../Network"; - -const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; -const prompt = "Please paste the external authetication code here..."; - -@observer -export default class AuthenticationManager extends React.Component<{}> { - public static Instance: AuthenticationManager; - @observable private openState = false; - private authenticationLink: Opt = undefined; - @observable private authenticationCode: Opt = undefined; - @observable private clickedState = false; - - private set isOpen(value: boolean) { - runInAction(() => this.openState = value); - } - - private set hasBeenClicked(value: boolean) { - runInAction(() => this.clickedState = value); - } - - public executeFullRoutine = async (service: string) => { - let response = await Identified.FetchFromServer(`/read${service}AccessToken`); - // if this is an authentication url, activate the UI to register the new access token - if (new RegExp(AuthenticationUrl).test(response)) { - this.isOpen = true; - this.authenticationLink = response; - return new Promise(async resolve => { - const disposer = reaction( - () => this.authenticationCode, - authenticationCode => { - if (authenticationCode) { - Identified.PostToServer(`/write${service}AccessToken`, { authenticationCode }).then(token => { - this.isOpen = false; - this.hasBeenClicked = false; - resolve(token); - disposer(); - }); - } - } - ); - }); - } - // otherwise, we already have a valid, stored access token - return response; - } - - constructor(props: {}) { - super(props); - AuthenticationManager.Instance = this; - } - - private handleClick = () => { - window.open(this.authenticationLink); - this.hasBeenClicked = true; - } - - private handlePaste = action((e: React.ChangeEvent) => { - this.authenticationCode = e.currentTarget.value; - }); - - private get renderPrompt() { - return ( -
- - {this.clickedState ? : (null)} -
- ); - } - - render() { - return ( - - ); - } - -} \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.scss b/src/client/apis/GoogleAuthenticationManager.scss new file mode 100644 index 000000000..5efb3ab3b --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.scss @@ -0,0 +1,3 @@ +.paste-target { + padding: 5px; +} \ No newline at end of file diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx new file mode 100644 index 000000000..1ab6380ef --- /dev/null +++ b/src/client/apis/GoogleAuthenticationManager.tsx @@ -0,0 +1,114 @@ +import { observable, action, reaction, runInAction } from "mobx"; +import { observer } from "mobx-react"; +import * as React from "react"; +import MainViewModal from "../views/MainViewModal"; +import { Opt } from "../../new_fields/Doc"; +import { Identified } from "../Network"; +import { RouteStore } from "../../server/RouteStore"; +import "./GoogleAuthenticationManager.scss"; + +const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth"; +const prompt = "Paste authorization code here..."; + +@observer +export default class GoogleAuthenticationManager extends React.Component<{}> { + public static Instance: GoogleAuthenticationManager; + @observable private openState = false; + private authenticationLink: Opt = undefined; + @observable private authenticationCode: Opt = undefined; + @observable private clickedState = false; + @observable private success: Opt = undefined; + @observable private displayLauncher = true; + + private set isOpen(value: boolean) { + runInAction(() => this.openState = value); + } + + private set hasBeenClicked(value: boolean) { + runInAction(() => this.clickedState = value); + } + + public fetchOrGenerateAccessToken = async () => { + let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken); + // if this is an authentication url, activate the UI to register the new access token + if (new RegExp(AuthenticationUrl).test(response)) { + this.isOpen = true; + this.authenticationLink = response; + return new Promise(async resolve => { + const disposer = reaction( + () => this.authenticationCode, + authenticationCode => { + if (authenticationCode) { + Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then( + token => { + runInAction(() => this.success = true); + setTimeout(() => { + this.isOpen = false; + runInAction(() => this.displayLauncher = false); + setTimeout(() => { + runInAction(() => this.success = undefined); + runInAction(() => this.displayLauncher = true); + this.hasBeenClicked = false; + }, 500); + }, 1000); + disposer(); + resolve(token); + }, + () => { + this.hasBeenClicked = false; + runInAction(() => this.success = false); + } + ); + } + } + ); + }); + } + // otherwise, we already have a valid, stored access token + return response; + } + + constructor(props: {}) { + super(props); + GoogleAuthenticationManager.Instance = this; + } + + private handleClick = () => { + window.open(this.authenticationLink); + setTimeout(() => this.hasBeenClicked = true, 500); + } + + private handlePaste = action((e: React.ChangeEvent) => { + this.authenticationCode = e.currentTarget.value; + }); + + private get renderPrompt() { + return ( +
+ {this.displayLauncher ? : (null)} + {this.clickedState ? : (null)} +
+ ); + } + + render() { + return ( + + ); + } + +} \ No newline at end of file diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index 8e88040db..e93fa6eb4 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -14,14 +14,11 @@ import { NewMediaItemResult, MediaItem } from "../../../server/apis/google/Share import { AssertionError } from "assert"; import { DocumentView } from "../../views/nodes/DocumentView"; import { Identified } from "../../Network"; -import AuthenticationManager from "../AuthenticationManager"; -import { List } from "../../../new_fields/List"; +import GoogleAuthenticationManager from "../GoogleAuthenticationManager"; export namespace GooglePhotos { - const endpoint = async () => { - return new Photos(await AuthenticationManager.Instance.executeFullRoutine("GooglePhotos")); - }; + const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index 91c8c572d..1541cd6b2 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -218,7 +218,7 @@ export default class SharingManager extends React.Component<{}> { if (!metadata) { return SharingPermissions.None; } - return StrCast(metadata.permissions, SharingPermissions.None)!; + return StrCast(metadata.permissions, SharingPermissions.None); } private get sharingInterface() { diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 12578e5b8..ba4224875 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -39,7 +39,7 @@ import PDFMenu from './pdf/PDFMenu'; import { PreviewCursor } from './PreviewCursor'; import { FilterBox } from './search/FilterBox'; import { OverlayView } from './OverlayView'; -import AuthenticationManager from '../apis/AuthenticationManager'; +import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager'; @observer export class MainView extends React.Component { @@ -678,7 +678,7 @@ export class MainView extends React.Component {
{this.dictationOverlay} - + {this.mainContent} diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts index 23fdbc53d..391d9dc0c 100644 --- a/src/server/RouteStore.ts +++ b/src/server/RouteStore.ts @@ -32,8 +32,8 @@ export enum RouteStore { // APIS cognitiveServices = "/cognitiveservices", googleDocs = "/googleDocs", - readGooglePhotosAccessToken = "/readGooglePhotosAccessToken", - writeGooglePhotosAccessToken = "/writeGooglePhotosAccessToken", + readGoogleAccessToken = "/readGoogleAccessToken", + writeGoogleAccessToken = "/writeGoogleAccessToken", googlePhotosMediaUpload = "/googlePhotosMediaUpload", googlePhotosMediaDownload = "/googlePhotosMediaDownload", googleDocsGet = "/googleDocsGet" diff --git a/src/server/index.ts b/src/server/index.ts index 5da05d9a7..dd44a0ce8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -862,7 +862,7 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => { }); }); -app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { +app.get(RouteStore.readGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); const information = { credentialsPath, userId }; @@ -872,7 +872,7 @@ app.get(RouteStore.readGooglePhotosAccessToken, async (req, res) => { GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token)); }); -app.post(RouteStore.writeGooglePhotosAccessToken, async (req, res) => { +app.post(RouteStore.writeGoogleAccessToken, async (req, res) => { const userId = req.header("userId")!; const information = { credentialsPath, userId }; const { token } = await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode); -- cgit v1.2.3-70-g09d2