aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSam Wilkins <35748010+samwilkins333@users.noreply.github.com>2019-12-02 12:21:09 -0500
committerGitHub <noreply@github.com>2019-12-02 12:21:09 -0500
commit0595f93dde717b7b6990e9a81c5b43a73a3808d5 (patch)
treeaf03ac257a5584f9913f120c44d4d39bb13916f2
parent68f49ef5daf3bf5c47d1d21c8f1cd2097947d071 (diff)
parentae76fd39a6530ac055948bb7b98537d38b592ef6 (diff)
Merge pull request #316 from browngraphicslab/server_refactor
Server refactor
-rw-r--r--.gitignore1
-rw-r--r--client_secret.json1
-rw-r--r--package.json2
-rw-r--r--src/Utils.ts10
-rw-r--r--src/client/Network.ts12
-rw-r--r--src/client/apis/GoogleAuthenticationManager.tsx40
-rw-r--r--src/client/apis/google_docs/GoogleApiClientUtils.ts15
-rw-r--r--src/client/apis/google_docs/GooglePhotosClientUtils.ts11
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts29
-rw-r--r--src/client/documents/Documents.ts1
-rw-r--r--src/client/util/ClientDiagnostics.ts34
-rw-r--r--src/client/util/History.ts3
-rw-r--r--src/client/util/Import & Export/DirectoryImportBox.tsx33
-rw-r--r--src/client/util/Import & Export/ImageUtils.ts9
-rw-r--r--src/client/util/SharingManager.tsx3
-rw-r--r--src/client/views/Main.tsx2
-rw-r--r--src/client/views/MainView.tsx8
-rw-r--r--src/client/views/collections/CollectionSubView.tsx26
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx15
-rw-r--r--src/client/views/nodes/AudioBox.tsx3
-rw-r--r--src/client/views/nodes/ImageBox.tsx7
-rw-r--r--src/client/views/nodes/VideoBox.tsx5
-rw-r--r--src/client/views/pdf/PDFViewer.tsx3
-rw-r--r--src/client/views/search/SearchBox.tsx3
-rw-r--r--src/mobile/ImageUpload.tsx3
-rw-r--r--src/new_fields/RichTextUtils.ts5
-rw-r--r--src/server/ActionUtilities.ts86
-rw-r--r--src/server/ApiManagers/ApiManager.ts11
-rw-r--r--src/server/ApiManagers/DeleteManager.ts63
-rw-r--r--src/server/ApiManagers/DiagnosticManager.ts30
-rw-r--r--src/server/ApiManagers/DownloadManager.ts267
-rw-r--r--src/server/ApiManagers/GeneralGoogleManager.ts58
-rw-r--r--src/server/ApiManagers/GooglePhotosManager.ts115
-rw-r--r--src/server/ApiManagers/PDFManager.ts111
-rw-r--r--src/server/ApiManagers/SearchManager.ts49
-rw-r--r--src/server/ApiManagers/UploadManager.ts222
-rw-r--r--src/server/ApiManagers/UserManager.ts71
-rw-r--r--src/server/ApiManagers/UtilManager.ts67
-rw-r--r--src/server/DashUploadUtils.ts184
-rw-r--r--src/server/Initialization.ts139
-rw-r--r--src/server/RouteManager.ts151
-rw-r--r--src/server/RouteStore.ts43
-rw-r--r--src/server/RouteSubscriber.ts2
-rw-r--r--src/server/SharedMediaTypes.ts8
-rw-r--r--src/server/Websocket/Websocket.ts217
-rw-r--r--src/server/apis/google/GoogleApiServerUtils.ts463
-rw-r--r--src/server/apis/google/GooglePhotosUploadUtils.ts127
-rw-r--r--src/server/authentication/config/passport.ts5
-rw-r--r--src/server/authentication/controllers/user_controller.ts35
-rw-r--r--src/server/authentication/models/current_user_utils.ts13
-rw-r--r--src/server/authentication/models/user_model.ts14
-rw-r--r--src/server/credentials/CredentialsLoader.ts29
-rw-r--r--src/server/credentials/google_project_credentials.json (renamed from src/server/credentials/google_docs_credentials.json)0
-rw-r--r--src/server/database.ts56
-rw-r--r--src/server/index.ts1315
-rw-r--r--src/server/public/files/.gitignore2
-rw-r--r--views/login.pug2
-rw-r--r--views/stylesheets/authentication.css81
-rw-r--r--views/user_activity.pug19
59 files changed, 2631 insertions, 1708 deletions
diff --git a/.gitignore b/.gitignore
index 5161268ac..e5048cfc4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@ dist/
.env
ClientUtils.ts
solr-8.1.1/server/
+src/server/public/files/ \ No newline at end of file
diff --git a/client_secret.json b/client_secret.json
deleted file mode 100644
index a9c698421..000000000
--- a/client_secret.json
+++ /dev/null
@@ -1 +0,0 @@
-{"installed":{"client_id":"1005546247619-kqpnvh42mpa803tem8556b87umi4j9r0.apps.googleusercontent.com","project_id":"brown-dash","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"WshLb5TH9SdFVGGbQcnYj7IU","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file
diff --git a/package.json b/package.json
index 393df8574..3725d76eb 100644
--- a/package.json
+++ b/package.json
@@ -105,6 +105,7 @@
"@types/react-table": "^6.7.22",
"@types/request": "^2.48.1",
"@types/request-promise": "^4.1.42",
+ "@types/rimraf": "^2.0.3",
"@types/sharp": "^0.22.2",
"@types/shelljs": "^0.8.5",
"@types/socket.io": "^2.1.2",
@@ -211,6 +212,7 @@
"readline": "^1.3.0",
"request": "^2.88.0",
"request-promise": "^4.2.4",
+ "rimraf": "^3.0.0",
"serializr": "^1.5.1",
"sharp": "^0.22.1",
"shelljs": "^0.8.3",
diff --git a/src/Utils.ts b/src/Utils.ts
index 37b509370..2543743a4 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -2,7 +2,6 @@ import v4 = require('uuid/v4');
import v5 = require("uuid/v5");
import { Socket } from 'socket.io';
import { Message } from './server/Message';
-import { RouteStore } from './server/RouteStore';
export namespace Utils {
@@ -46,7 +45,12 @@ export namespace Utils {
}
export function CorsProxy(url: string): string {
- return prepend(RouteStore.corsProxy + "/") + encodeURIComponent(url);
+ return prepend("/corsProxy/") + encodeURIComponent(url);
+ }
+
+ export async function getApiKey(target: string): Promise<string> {
+ const response = await fetch(prepend(`environment/${target.toUpperCase()}`));
+ return response.text();
}
export function CopyText(text: string) {
@@ -255,7 +259,7 @@ export namespace Utils {
}
let idString = (message.id || "").padStart(36, ' ');
prefix = prefix.padEnd(16, ' ');
- console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)}`);
+ console.log(`${prefix}: ${idString}, ${receiving ? 'receiving' : 'sending'} ${messageName} with data ${JSON.stringify(message)} `);
}
function loggingCallback(prefix: string, func: (args: any) => any, messageName: string) {
diff --git a/src/client/Network.ts b/src/client/Network.ts
index 75ccb5e99..f9ef27267 100644
--- a/src/client/Network.ts
+++ b/src/client/Network.ts
@@ -1,18 +1,16 @@
import { Utils } from "../Utils";
-import { CurrentUserUtils } from "../server/authentication/models/current_user_utils";
import requestPromise = require('request-promise');
-export namespace Identified {
+export namespace Networking {
export async function FetchFromServer(relativeRoute: string) {
- return (await fetch(relativeRoute, { headers: { userId: CurrentUserUtils.id } })).text();
+ return (await fetch(relativeRoute)).text();
}
export async function PostToServer(relativeRoute: string, body?: any) {
let options = {
uri: Utils.prepend(relativeRoute),
method: "POST",
- headers: { userId: CurrentUserUtils.id },
body,
json: true
};
@@ -22,12 +20,10 @@ export namespace Identified {
export async function PostFormDataToServer(relativeRoute: string, formData: FormData) {
const parameters = {
method: 'POST',
- headers: { userId: CurrentUserUtils.id },
- body: formData,
+ body: formData
};
const response = await fetch(relativeRoute, parameters);
- const text = await response.json();
- return text;
+ return response.json();
}
} \ No newline at end of file
diff --git a/src/client/apis/GoogleAuthenticationManager.tsx b/src/client/apis/GoogleAuthenticationManager.tsx
index 01dac3996..ae77c4b7b 100644
--- a/src/client/apis/GoogleAuthenticationManager.tsx
+++ b/src/client/apis/GoogleAuthenticationManager.tsx
@@ -3,8 +3,7 @@ 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 { Networking } from "../Network";
import "./GoogleAuthenticationManager.scss";
const AuthenticationUrl = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -31,7 +30,7 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
}
public fetchOrGenerateAccessToken = async () => {
- let response = await Identified.FetchFromServer(RouteStore.readGoogleAccessToken);
+ let response = await Networking.FetchFromServer("/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;
@@ -39,24 +38,25 @@ export default class GoogleAuthenticationManager extends React.Component<{}> {
return new Promise<string>(async resolve => {
const disposer = reaction(
() => this.authenticationCode,
- authenticationCode => {
- if (authenticationCode) {
- Identified.PostToServer(RouteStore.writeGoogleAccessToken, { authenticationCode }).then(
- ({ access_token, avatar, name }) => {
- runInAction(() => {
- this.avatar = avatar;
- this.username = name;
- });
- this.beginFadeout();
- disposer();
- resolve(access_token);
- },
- action(() => {
- this.hasBeenClicked = false;
- this.success = false;
- })
- );
+ async authenticationCode => {
+ if (!authenticationCode) {
+ return;
}
+ const { access_token, avatar, name } = await Networking.PostToServer(
+ "/writeGoogleAccessToken",
+ { authenticationCode }
+ );
+ runInAction(() => {
+ this.avatar = avatar;
+ this.username = name;
+ });
+ this.beginFadeout();
+ disposer();
+ resolve(access_token);
+ action(() => {
+ this.hasBeenClicked = false;
+ this.success = false;
+ });
}
);
});
diff --git a/src/client/apis/google_docs/GoogleApiClientUtils.ts b/src/client/apis/google_docs/GoogleApiClientUtils.ts
index 1cf01fc3d..26c7f8d2e 100644
--- a/src/client/apis/google_docs/GoogleApiClientUtils.ts
+++ b/src/client/apis/google_docs/GoogleApiClientUtils.ts
@@ -1,9 +1,8 @@
import { docs_v1, slides_v1 } from "googleapis";
-import { RouteStore } from "../../../server/RouteStore";
import { Opt } from "../../../new_fields/Doc";
import { isArray } from "util";
import { EditorState } from "prosemirror-state";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
export const Pulls = "googleDocsPullCount";
export const Pushes = "googleDocsPushCount";
@@ -77,14 +76,14 @@ export namespace GoogleApiClientUtils {
* @returns the documentId of the newly generated document, or undefined if the creation process fails.
*/
export const create = async (options: CreateOptions): Promise<CreationResult> => {
- const path = `${RouteStore.googleDocs}/Documents/${Actions.Create}`;
+ const path = `/googleDocs/Documents/${Actions.Create}`;
const parameters = {
requestBody: {
title: options.title || `Dash Export (${new Date().toDateString()})`
}
};
try {
- const schema: docs_v1.Schema$Document = await Identified.PostToServer(path, parameters);
+ const schema: docs_v1.Schema$Document = await Networking.PostToServer(path, parameters);
return schema.documentId;
} catch {
return undefined;
@@ -154,10 +153,10 @@ export namespace GoogleApiClientUtils {
}
export const retrieve = async (options: RetrieveOptions): Promise<RetrievalResult> => {
- const path = `${RouteStore.googleDocs}/Documents/${Actions.Retrieve}`;
+ const path = `/googleDocs/Documents/${Actions.Retrieve}`;
try {
const parameters = { documentId: options.documentId };
- const schema: RetrievalResult = await Identified.PostToServer(path, parameters);
+ const schema: RetrievalResult = await Networking.PostToServer(path, parameters);
return schema;
} catch {
return undefined;
@@ -165,7 +164,7 @@ export namespace GoogleApiClientUtils {
};
export const update = async (options: UpdateOptions): Promise<UpdateResult> => {
- const path = `${RouteStore.googleDocs}/Documents/${Actions.Update}`;
+ const path = `/googleDocs/Documents/${Actions.Update}`;
const parameters = {
documentId: options.documentId,
requestBody: {
@@ -173,7 +172,7 @@ export namespace GoogleApiClientUtils {
}
};
try {
- const replies: UpdateResult = await Identified.PostToServer(path, parameters);
+ const replies: UpdateResult = await Networking.PostToServer(path, parameters);
return replies;
} catch {
return undefined;
diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
index e93fa6eb4..bf8897061 100644
--- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts
+++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts
@@ -1,5 +1,4 @@
import { Utils } from "../../../Utils";
-import { RouteStore } from "../../../server/RouteStore";
import { ImageField } from "../../../new_fields/URLField";
import { Cast, StrCast } from "../../../new_fields/Types";
import { Doc, Opt, DocListCastAsync } from "../../../new_fields/Doc";
@@ -13,7 +12,7 @@ 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 { Identified } from "../../Network";
+import { Networking } from "../../Network";
import GoogleAuthenticationManager from "../GoogleAuthenticationManager";
export namespace GooglePhotos {
@@ -78,6 +77,7 @@ export namespace GooglePhotos {
}
export const CollectionToAlbum = async (options: AlbumCreationOptions): Promise<Opt<AlbumCreationResult>> => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const { collection, title, descriptionKey, tag } = options;
const dataDocument = Doc.GetProto(collection);
const images = ((await DocListCastAsync(dataDocument.data)) || []).filter(doc => Cast(doc.data, ImageField));
@@ -127,6 +127,7 @@ export namespace GooglePhotos {
export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc;
export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
let response = await Query.ContentSearch(requested);
let uploads = await Transactions.WriteMediaItemsToServer(response);
const children = uploads.map((upload: Transactions.UploadInformation) => {
@@ -147,6 +148,7 @@ export namespace GooglePhotos {
const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0);
export const TagChildImages = async (collection: Doc) => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
const idMapping = await Cast(collection.googlePhotosIdMapping, Doc);
if (!idMapping) {
throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!");
@@ -304,7 +306,7 @@ export namespace GooglePhotos {
};
export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => {
- const uploads = await Identified.PostToServer(RouteStore.googlePhotosMediaDownload, body);
+ const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", body);
return uploads;
};
@@ -325,6 +327,7 @@ export namespace GooglePhotos {
}
export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => {
+ await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken();
if (album && "title" in album) {
album = await Create.Album(album.title);
}
@@ -341,7 +344,7 @@ export namespace GooglePhotos {
media.push({ url, description });
}
if (media.length) {
- const results = await Identified.PostToServer(RouteStore.googlePhotosMediaUpload, { media, album });
+ const results = await Networking.PostToServer("/googlePhotosMediaUpload", { media, album });
return results;
}
};
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 08fcb4883..5a7f5e991 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -2,7 +2,6 @@ import * as request from "request-promise";
import { Doc, Field, Opt } from "../../new_fields/Doc";
import { Cast } from "../../new_fields/Types";
import { Docs } from "../documents/Documents";
-import { RouteStore } from "../../server/RouteStore";
import { Utils } from "../../Utils";
import { InkData } from "../../new_fields/InkField";
import { UndoManager } from "../util/UndoManager";
@@ -39,21 +38,19 @@ export enum Confidence {
export namespace CognitiveServices {
const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => {
- return fetch(Utils.prepend(`${RouteStore.cognitiveServices}/${service}`)).then(async response => {
- let apiKey = await response.text();
- if (!apiKey) {
- console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory`);
- return undefined;
- }
-
- let results: any;
- try {
- results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
- } catch {
- results = undefined;
- }
- return results;
- });
+ const apiKey = await Utils.getApiKey(service);
+ if (!apiKey) {
+ console.log(`No API key found for ${service}: ensure index.ts has access to a .env file in your root directory.`);
+ return undefined;
+ }
+
+ let results: any;
+ try {
+ results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json));
+ } catch {
+ results = undefined;
+ }
+ return results;
};
export namespace Image {
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index 0befec1da..d1e3ea708 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -377,7 +377,6 @@ export namespace Docs {
let extension = path.extname(target);
target = `${target.substring(0, target.length - extension.length)}_o${extension}`;
}
- // if (target !== "http://www.cs.brown.edu/") {
requestImageSize(target)
.then((size: any) => {
let aspect = size.height / size.width;
diff --git a/src/client/util/ClientDiagnostics.ts b/src/client/util/ClientDiagnostics.ts
new file mode 100644
index 000000000..0a213aa1c
--- /dev/null
+++ b/src/client/util/ClientDiagnostics.ts
@@ -0,0 +1,34 @@
+export namespace ClientDiagnostics {
+
+ export async function start() {
+
+ let serverPolls = 0;
+ const serverHandle = setInterval(async () => {
+ if (++serverPolls === 20) {
+ alert("Your connection to the server has been terminated.");
+ clearInterval(serverHandle);
+ }
+ await fetch("/serverHeartbeat");
+ serverPolls--;
+ }, 1000 * 15);
+
+ let executed = false;
+ let solrHandle: NodeJS.Timeout | undefined;
+ const handler = async () => {
+ const response = await fetch("/solrHeartbeat");
+ if (!(await response.json()).running) {
+ if (!executed) {
+ alert("Looks like SOLR is not running on your machine.");
+ executed = true;
+ solrHandle && clearInterval(solrHandle);
+ }
+ }
+ };
+ await handler();
+ if (!executed) {
+ solrHandle = setInterval(handler, 1000 * 15);
+ }
+
+ }
+
+} \ No newline at end of file
diff --git a/src/client/util/History.ts b/src/client/util/History.ts
index 899abbe40..1c51236cb 100644
--- a/src/client/util/History.ts
+++ b/src/client/util/History.ts
@@ -1,6 +1,5 @@
import { Doc, Opt, Field } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
-import { RouteStore } from "../../server/RouteStore";
import { MainView } from "../views/MainView";
import * as qs from 'query-string';
import { Utils, OmitKeys } from "../../Utils";
@@ -26,7 +25,7 @@ export namespace HistoryUtil {
// const handlers: ((state: ParsedUrl | null) => void)[] = [];
function onHistory(e: PopStateEvent) {
- if (window.location.pathname !== RouteStore.home) {
+ if (window.location.pathname !== "/home") {
const url = e.state as ParsedUrl || parseUrl(window.location);
if (url) {
switch (url.type) {
diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx
index 5904088fc..104d9e099 100644
--- a/src/client/util/Import & Export/DirectoryImportBox.tsx
+++ b/src/client/util/Import & Export/DirectoryImportBox.tsx
@@ -1,7 +1,6 @@
import "fs";
import React = require("react");
import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
-import { RouteStore } from "../../../server/RouteStore";
import { action, observable, autorun, runInAction, computed, reaction, IReactionDisposer } from "mobx";
import { FieldViewProps, FieldView } from "../../views/nodes/FieldView";
import Measure, { ContentRect } from "react-measure";
@@ -20,19 +19,13 @@ import { listSpec } from "../../../new_fields/Schema";
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
import "./DirectoryImportBox.scss";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
import { BatchedArray } from "array-batcher";
-import { ExifData } from "exif";
+import * as path from 'path';
+import { AcceptibleMedia } from "../../../server/SharedMediaTypes";
const unsupported = ["text/html", "text/plain"];
-interface ImageUploadResponse {
- name: string;
- path: string;
- type: string;
- exif: any;
-}
-
@observer
export default class DirectoryImportBox extends React.Component<FieldViewProps> {
private selector = React.createRef<HTMLInputElement>();
@@ -95,7 +88,12 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
let validated: File[] = [];
for (let i = 0; i < files.length; i++) {
let file = files.item(i);
- file && !unsupported.includes(file.type) && validated.push(file);
+ if (file && !unsupported.includes(file.type)) {
+ const ext = path.extname(file.name).toLowerCase();
+ if (AcceptibleMedia.imageFormats.includes(ext)) {
+ validated.push(file);
+ }
+ }
}
runInAction(() => {
@@ -109,7 +107,7 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
runInAction(() => this.phase = `Internal: uploading ${this.quota - this.completed} files to Dash...`);
const batched = BatchedArray.from(validated, { batchSize: 15 });
- const uploads = await batched.batchedMapAsync<ImageUploadResponse>(async (batch, collector) => {
+ const uploads = await batched.batchedMapAsync<any>(async (batch, collector) => {
const formData = new FormData();
batch.forEach(file => {
@@ -118,20 +116,19 @@ export default class DirectoryImportBox extends React.Component<FieldViewProps>
formData.append(Utils.GenerateGuid(), file);
});
- collector.push(...(await Identified.PostFormDataToServer(RouteStore.upload, formData)));
+ collector.push(...(await Networking.PostFormDataToServer("/upload", formData)));
runInAction(() => this.completed += batch.length);
});
- await Promise.all(uploads.map(async upload => {
- const type = upload.type;
- const path = Utils.prepend(upload.path);
+ await Promise.all(uploads.map(async ({ name, type, clientAccessPath, exifData }) => {
+ const path = Utils.prepend(clientAccessPath);
const options = {
nativeWidth: 300,
width: 300,
- title: upload.name
+ title: name
};
const document = await Docs.Get.DocumentFromType(type, path, options);
- const { data, error } = upload.exif;
+ const { data, error } = exifData;
if (document) {
Doc.GetProto(document).exif = error || Docs.Get.DocumentHierarchyFromJson(data);
docs.push(document);
diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts
index c9abf38fa..6a9486f83 100644
--- a/src/client/util/Import & Export/ImageUtils.ts
+++ b/src/client/util/Import & Export/ImageUtils.ts
@@ -1,9 +1,8 @@
-import { Doc, DocListCast, DocListCastAsync, Opt } from "../../../new_fields/Doc";
+import { Doc } from "../../../new_fields/Doc";
import { ImageField } from "../../../new_fields/URLField";
import { Cast, StrCast } from "../../../new_fields/Types";
-import { RouteStore } from "../../../server/RouteStore";
import { Docs } from "../../documents/Documents";
-import { Identified } from "../../Network";
+import { Networking } from "../../Network";
import { Id } from "../../../new_fields/FieldSymbols";
import { Utils } from "../../../Utils";
@@ -15,7 +14,7 @@ export namespace ImageUtils {
return false;
}
const source = field.url.href;
- const response = await Identified.PostToServer(RouteStore.inspectImage, { source });
+ const response = await Networking.PostToServer("/inspectImage", { source });
const { error, data } = response.exifData;
document.exif = error || Docs.Get.DocumentHierarchyFromJson(data);
return data !== undefined;
@@ -23,7 +22,7 @@ export namespace ImageUtils {
export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => {
const a = document.createElement("a");
- a.href = Utils.prepend(`${RouteStore.imageHierarchyExport}/${collection[Id]}`);
+ a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`);
a.download = `Dash Export [${StrCast(collection.title)}].zip`;
a.click();
};
diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx
index 2082d6324..cc1d628b1 100644
--- a/src/client/util/SharingManager.tsx
+++ b/src/client/util/SharingManager.tsx
@@ -4,7 +4,6 @@ import MainViewModal from "../views/MainViewModal";
import { Doc, Opt, DocCastAsync } from "../../new_fields/Doc";
import { DocServer } from "../DocServer";
import { Cast, StrCast } from "../../new_fields/Types";
-import { RouteStore } from "../../server/RouteStore";
import * as RequestPromise from "request-promise";
import { Utils } from "../../Utils";
import "./SharingManager.scss";
@@ -104,7 +103,7 @@ export default class SharingManager extends React.Component<{}> {
}
populateUsers = async () => {
- let userList = await RequestPromise.get(Utils.prepend(RouteStore.getUsers));
+ let userList = await RequestPromise.get(Utils.prepend("/getUsers"));
const raw = JSON.parse(userList) as User[];
const evaluating = raw.map(async user => {
let isCandidate = user.email !== Doc.CurrentUserEmail;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b21eb9c8f..9e699978f 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -5,10 +5,12 @@ import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { DocServer } from "../DocServer";
import { AssignAllExtensions } from "../../extensions/General/Extensions";
+import { ClientDiagnostics } from "../util/ClientDiagnostics";
AssignAllExtensions();
(async () => {
+ await ClientDiagnostics.start();
const info = await CurrentUserUtils.loadCurrentUser();
DocServer.init(window.location.protocol, window.location.hostname, 4321, info.email);
await Docs.Prototypes.initialize();
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f3a1e799c..291781da1 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -15,7 +15,6 @@ import { List } from '../../new_fields/List';
import { listSpec } from '../../new_fields/Schema';
import { Cast, FieldValue, StrCast } from '../../new_fields/Types';
import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
-import { RouteStore } from '../../server/RouteStore';
import { emptyFunction, returnEmptyString, returnFalse, returnOne, returnTrue, Utils } from '../../Utils';
import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
import { DocServer } from '../DocServer';
@@ -82,7 +81,7 @@ export class MainView extends React.Component {
this._urlState = HistoryUtil.parseUrl(window.location) || {} as any;
// causes errors to be generated when modifying an observable outside of an action
configure({ enforceActions: "observed" });
- if (window.location.pathname !== RouteStore.home) {
+ if (window.location.pathname !== "/home") {
let pathname = window.location.pathname.substr(1).split("/");
if (pathname.length > 1) {
let type = pathname[0];
@@ -411,10 +410,11 @@ export class MainView extends React.Component {
zoomToScale={emptyFunction}
getScale={returnOne}>
</DocumentView>
- <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend(RouteStore.logout))}>
+ <button className="mainView-logout" key="logout" onClick={() => window.location.assign(Utils.prepend("/logout"))}>
{CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
</button>
- </div></div>;
+ </div>
+ </div>;
}
@computed get mainContent() {
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 368e988d4..e80825825 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -6,9 +6,8 @@ import { Id } from "../../../new_fields/FieldSymbols";
import { List } from "../../../new_fields/List";
import { listSpec } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
-import { Cast, StrCast } from "../../../new_fields/Types";
+import { Cast } from "../../../new_fields/Types";
import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
-import { RouteStore } from "../../../server/RouteStore";
import { Utils } from "../../../Utils";
import { DocServer } from "../../DocServer";
import { DocumentType } from "../../documents/DocumentTypes";
@@ -23,6 +22,7 @@ import React = require("react");
var path = require('path');
import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
import { ImageUtils } from "../../util/Import & Export/ImageUtils";
+import { Networking } from "../../Network";
export interface CollectionViewProps extends FieldViewProps {
addDocument: (document: Doc) => boolean;
@@ -253,7 +253,6 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let promises: Promise<void>[] = [];
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < e.dataTransfer.items.length; i++) {
- const upload = window.location.origin + RouteStore.upload;
let item = e.dataTransfer.items[i];
if (item.kind === "string" && item.type.indexOf("uri") !== -1) {
let str: string;
@@ -273,28 +272,25 @@ export function CollectionSubView<T>(schemaCtor: (doc: Doc) => T) {
let file = item.getAsFile();
let formData = new FormData();
- if (file) {
- formData.append('file', file);
+ if (!file || !file.type) {
+ continue;
}
- let dropFileName = file ? file.name : "-empty-";
- let prom = fetch(upload, {
- method: 'POST',
- body: formData
- }).then(async (res: Response) => {
- (await res.json()).map(action((file: any) => {
+ formData.append('file', file);
+ let dropFileName = file ? file.name : "-empty-";
+ promises.push(Networking.PostFormDataToServer("/upload", formData).then(results => {
+ results.map(action(({ clientAccessPath }: any) => {
let full = { ...options, nativeWidth: type.indexOf("video") !== -1 ? 600 : 300, width: 300, title: dropFileName };
- let pathname = Utils.prepend(file.path);
+ let pathname = Utils.prepend(clientAccessPath);
Docs.Get.DocumentFromType(type, pathname, full).then(doc => {
doc && (Doc.GetProto(doc).fileUpload = path.basename(pathname).replace("upload_", "").replace(/\.[a-z0-9]*$/, ""));
doc && this.props.addDocument(doc);
});
}));
- });
- promises.push(prom);
+ }));
}
}
- if (text) {
+ if (text && !text.includes("https://")) {
this.props.addDocument(Docs.Create.TextDocument({ ...options, documentText: "@@@" + text, width: 400, height: 315 }));
return;
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
index e1d23ddcb..4a32c1647 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx
@@ -1,5 +1,5 @@
import { Doc, Field, FieldResult } from "../../../../new_fields/Doc";
-import { NumCast, StrCast, Cast } from "../../../../new_fields/Types";
+import { NumCast, StrCast, Cast, DateCast } from "../../../../new_fields/Types";
import { ScriptBox } from "../../ScriptBox";
import { CompileScript } from "../../../util/Scripting";
import { ScriptField } from "../../../../new_fields/ScriptField";
@@ -8,6 +8,7 @@ import { emptyFunction } from "../../../../Utils";
import React = require("react");
import { ObservableMap, runInAction } from "mobx";
import { Id } from "../../../../new_fields/FieldSymbols";
+import { DateField } from "../../../../new_fields/DateField";
interface PivotData {
type: string;
@@ -33,6 +34,16 @@ export interface ViewDefResult {
bounds?: ViewDefBounds;
}
+function toLabel(target: FieldResult<Field>) {
+ if (target instanceof DateField) {
+ const date = DateCast(target).date;
+ if (date) {
+ return `${date.toDateString()} ${date.toTimeString()}`;
+ }
+ }
+ return String(target);
+}
+
export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDoc: Doc, childDocs: Doc[], childPairs: { layout: Doc, data?: Doc }[], viewDefsToJSX: (views: any) => ViewDefResult[]) {
const pivotAxisWidth = NumCast(pivotDoc.pivotWidth, 200);
const pivotColumnGroups = new Map<FieldResult<Field>, Doc[]>();
@@ -58,7 +69,7 @@ export function computePivotLayout(poolData: ObservableMap<string, any>, pivotDo
let xCount = 0;
groupNames.push({
type: "text",
- text: String(key),
+ text: toLabel(key),
x,
y: pivotAxisWidth + 50,
width: pivotAxisWidth * expander * numCols,
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 86bd23b67..77b10e395 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -8,7 +8,6 @@ import { DocExtendableComponent } from "../DocComponent";
import { makeInterface, createSchema } from "../../../new_fields/Schema";
import { documentSchema } from "../../../new_fields/documentSchemas";
import { Utils, returnTrue, emptyFunction, returnOne, returnTransparent } from "../../../Utils";
-import { RouteStore } from "../../../server/RouteStore";
import { runInAction, observable, reaction, IReactionDisposer, computed, action } from "mobx";
import { DateField } from "../../../new_fields/DateField";
import { SelectionManager } from "../../util/SelectionManager";
@@ -140,7 +139,7 @@ export class AudioBox extends DocExtendableComponent<FieldViewProps, AudioDocume
self._recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(Utils.prepend(RouteStore.upload), {
+ const res = await fetch(Utils.prepend("/upload"), {
method: 'POST',
body: formData
});
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 14523b2b4..c283e4f21 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -8,10 +8,9 @@ import { Doc, DocListCast, HeightSym, WidthSym } from '../../../new_fields/Doc';
import { List } from '../../../new_fields/List';
import { createSchema, listSpec, makeInterface } from '../../../new_fields/Schema';
import { ComputedField } from '../../../new_fields/ScriptField';
-import { BoolCast, Cast, FieldValue, NumCast, StrCast } from '../../../new_fields/Types';
+import { Cast, NumCast } from '../../../new_fields/Types';
import { AudioField, ImageField } from '../../../new_fields/URLField';
-import { RouteStore } from '../../../server/RouteStore';
-import { Utils, returnOne, emptyFunction, OmitKeys } from '../../../Utils';
+import { Utils, returnOne, emptyFunction } from '../../../Utils';
import { CognitiveServices, Confidence, Service, Tag } from '../../cognitive_services/CognitiveServices';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
@@ -99,7 +98,7 @@ export class ImageBox extends DocAnnotatableComponent<FieldViewProps, ImageDocum
recorder.ondataavailable = async function (e: any) {
const formData = new FormData();
formData.append("file", e.data);
- const res = await fetch(Utils.prepend(RouteStore.upload), {
+ const res = await fetch(Utils.prepend("/upload"), {
method: 'POST',
body: formData
});
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 7842ecd57..741dcada0 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -11,8 +11,7 @@ import { createSchema, makeInterface } from "../../../new_fields/Schema";
import { ScriptField } from "../../../new_fields/ScriptField";
import { Cast, StrCast, NumCast } from "../../../new_fields/Types";
import { VideoField } from "../../../new_fields/URLField";
-import { RouteStore } from "../../../server/RouteStore";
-import { emptyFunction, returnOne, Utils } from "../../../Utils";
+import { Utils, emptyFunction, returnOne } from "../../../Utils";
import { Docs, DocUtils } from "../../documents/Documents";
import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView";
import { ContextMenu } from "../ContextMenu";
@@ -182,7 +181,7 @@ export class VideoBox extends DocAnnotatableComponent<FieldViewProps, VideoDocum
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = Utils.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend("/uploadURI");
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index b737ce221..c075a4f99 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -125,7 +125,8 @@ export class PDFViewer extends DocAnnotatableComponent<IViewerProps, PdfDocument
!this.props.Document.lockedTransform && (this.props.Document.lockedTransform = true);
// change the address to be the file address of the PNG version of each page
// file address of the pdf
- this._coverPath = JSON.parse(await rp.get(Utils.prepend(`/thumbnail${this.props.url.substring("files/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.PNG`)));
+ const path = Utils.prepend(`/thumbnail${this.props.url.substring("files/pdfs/".length, this.props.url.length - ".pdf".length)}-${(this.Document.curPage || 1)}.png`);
+ this._coverPath = JSON.parse(await rp.get(path));
runInAction(() => this._showWaiting = this._showCover = true);
this.props.startupLive && this.setupPdfJsViewer();
this._searchReactionDisposer = reaction(() => this.Document.search_string, searchString => {
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 5c1bd8ef9..ff35542ed 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -8,7 +8,6 @@ import * as rp from 'request-promise';
import { Doc } from '../../../new_fields/Doc';
import { Id } from '../../../new_fields/FieldSymbols';
import { Cast, NumCast } from '../../../new_fields/Types';
-import { RouteStore } from '../../../server/RouteStore';
import { Utils } from '../../../Utils';
import { Docs } from '../../documents/Documents';
import { SetupDrag } from '../../util/DragManager';
@@ -90,7 +89,7 @@ export class SearchBox extends React.Component {
public static async convertDataUri(imageUri: string, returnedFilename: string) {
try {
- let posting = Utils.prepend(RouteStore.dataUriToImage);
+ let posting = Utils.prepend("/uploadURI");
const returnedUri = await rp.post(posting, {
body: {
uri: imageUri,
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 33a615cbf..9fdaac66e 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -1,7 +1,6 @@
import * as ReactDOM from 'react-dom';
import * as rp from 'request-promise';
import { Docs } from '../client/documents/Documents';
-import { RouteStore } from '../server/RouteStore';
import "./ImageUpload.scss";
import React = require('react');
import { DocServer } from '../client/DocServer';
@@ -58,7 +57,7 @@ class Uploader extends React.Component {
this.status = "getting user document";
- const res = await rp.get(Utils.prepend(RouteStore.getUserDocumentId));
+ const res = await rp.get(Utils.prepend("/getUserDocumentId"));
if (!res) {
throw new Error("No user id returned");
}
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index c2cca859c..dc5574782 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -8,7 +8,6 @@ import { Opt, Doc } from "./Doc";
import Color = require('color');
import { sinkListItem } from "prosemirror-schema-list";
import { Utils } from "../Utils";
-import { RouteStore } from "../server/RouteStore";
import { Docs } from "../client/documents/Documents";
import { schema } from "../client/util/RichTextSchema";
import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
@@ -17,7 +16,7 @@ import { Cast, StrCast } from "./Types";
import { Id } from "./FieldSymbols";
import { DocumentView } from "../client/views/nodes/DocumentView";
import { AssertionError } from "assert";
-import { Identified } from "../client/Network";
+import { Networking } from "../client/Network";
export namespace RichTextUtils {
@@ -129,7 +128,7 @@ export namespace RichTextUtils {
return { baseUrl, filename };
});
- const uploads = await Identified.PostToServer(RouteStore.googlePhotosMediaDownload, { mediaItems });
+ const uploads = await Networking.PostToServer("/googlePhotosMediaDownload", { mediaItems });
if (uploads.length !== mediaItems.length) {
throw new AssertionError({ expected: mediaItems.length, actual: uploads.length, message: "Error with internally uploading inlineObjects!" });
diff --git a/src/server/ActionUtilities.ts b/src/server/ActionUtilities.ts
new file mode 100644
index 000000000..c9fc86fea
--- /dev/null
+++ b/src/server/ActionUtilities.ts
@@ -0,0 +1,86 @@
+import * as fs from 'fs';
+import { ExecOptions } from 'shelljs';
+import { exec } from 'child_process';
+import * as path from 'path';
+import * as rimraf from "rimraf";
+
+export const command_line = (command: string, fromDirectory?: string) => {
+ return new Promise<string>((resolve, reject) => {
+ let options: ExecOptions = {};
+ if (fromDirectory) {
+ options.cwd = path.join(__dirname, fromDirectory);
+ }
+ exec(command, options, (err, stdout) => err ? reject(err) : resolve(stdout));
+ });
+};
+
+export const read_text_file = (relativePath: string) => {
+ let target = path.join(__dirname, relativePath);
+ return new Promise<string>((resolve, reject) => {
+ fs.readFile(target, (err, data) => err ? reject(err) : resolve(data.toString()));
+ });
+};
+
+export const write_text_file = (relativePath: string, contents: any) => {
+ let target = path.join(__dirname, relativePath);
+ return new Promise<void>((resolve, reject) => {
+ fs.writeFile(target, contents, (err) => err ? reject(err) : resolve());
+ });
+};
+
+export interface LogData {
+ startMessage: string;
+ endMessage: string;
+ action: () => void | Promise<void>;
+}
+
+let current = Math.ceil(Math.random() * 20);
+export async function log_execution({ startMessage, endMessage, action }: LogData) {
+ const color = `\x1b[${31 + current++ % 6}m%s\x1b[0m`;
+ console.log(color, `${startMessage}...`);
+ await action();
+ console.log(color, endMessage);
+}
+
+export enum ConsoleColors {
+ Black = `\x1b[30m%s\x1b[0m`,
+ Red = `\x1b[31m%s\x1b[0m`,
+ Green = `\x1b[32m%s\x1b[0m`,
+ Yellow = `\x1b[33m%s\x1b[0m`,
+ Blue = `\x1b[34m%s\x1b[0m`,
+ Magenta = `\x1b[35m%s\x1b[0m`,
+ Cyan = `\x1b[36m%s\x1b[0m`,
+ White = `\x1b[37m%s\x1b[0m`
+}
+
+export function logPort(listener: string, port: number) {
+ process.stdout.write(`${listener} listening on port `);
+ console.log(ConsoleColors.Yellow, port);
+}
+
+export function msToTime(duration: number) {
+ let milliseconds = Math.floor((duration % 1000) / 100),
+ seconds = Math.floor((duration / 1000) % 60),
+ minutes = Math.floor((duration / (1000 * 60)) % 60),
+ hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
+
+ let hoursS = (hours < 10) ? "0" + hours : hours;
+ let minutesS = (minutes < 10) ? "0" + minutes : minutes;
+ let secondsS = (seconds < 10) ? "0" + seconds : seconds;
+
+ return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
+}
+
+export const createIfNotExists = async (path: string) => {
+ if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
+ return true;
+ }
+ return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
+};
+
+export async function Prune(rootDirectory: string): Promise<boolean> {
+ const error = await new Promise<Error>(resolve => rimraf(rootDirectory, resolve));
+ return error === null;
+}
+
+export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null))); \ No newline at end of file
diff --git a/src/server/ApiManagers/ApiManager.ts b/src/server/ApiManagers/ApiManager.ts
new file mode 100644
index 000000000..e2b01d585
--- /dev/null
+++ b/src/server/ApiManagers/ApiManager.ts
@@ -0,0 +1,11 @@
+import RouteManager, { RouteInitializer } from "../RouteManager";
+
+export type Registration = (initializer: RouteInitializer) => void;
+
+export default abstract class ApiManager {
+ protected abstract initialize(register: Registration): void;
+
+ public register(register: Registration) {
+ this.initialize(register);
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
new file mode 100644
index 000000000..71818c673
--- /dev/null
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -0,0 +1,63 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { WebSocket } from "../Websocket/Websocket";
+import { Database } from "../database";
+
+export default class DeleteManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/delete",
+ onValidation: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await WebSocket.deleteFields();
+ res.redirect("/home");
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/deleteAll",
+ onValidation: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await WebSocket.deleteAll();
+ res.redirect("/home");
+ }
+ });
+
+
+ register({
+ method: Method.GET,
+ subscription: "/deleteWithAux",
+ onValidation: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Auxiliary.DeleteAll();
+ res.redirect("/delete");
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/deleteWithGoogleCredentials",
+ onValidation: async ({ res, isRelease }) => {
+ if (isRelease) {
+ return _permission_denied(res, deletionPermissionError);
+ }
+ await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
+ res.redirect("/delete");
+ }
+ });
+
+ }
+
+}
+
+const deletionPermissionError = "Cannot perform a delete operation outside of the development environment!";
diff --git a/src/server/ApiManagers/DiagnosticManager.ts b/src/server/ApiManagers/DiagnosticManager.ts
new file mode 100644
index 000000000..104985481
--- /dev/null
+++ b/src/server/ApiManagers/DiagnosticManager.ts
@@ -0,0 +1,30 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import request = require('request-promise');
+
+export default class DiagnosticManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/serverHeartbeat",
+ onValidation: ({ res }) => res.send(true)
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/solrHeartbeat",
+ onValidation: async ({ res }) => {
+ try {
+ await request("http://localhost:8983");
+ res.send({ running: true });
+ } catch (e) {
+ res.send({ running: false });
+ }
+ }
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/DownloadManager.ts b/src/server/ApiManagers/DownloadManager.ts
new file mode 100644
index 000000000..5bad46eda
--- /dev/null
+++ b/src/server/ApiManagers/DownloadManager.ts
@@ -0,0 +1,267 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import * as Archiver from 'archiver';
+import * as express from 'express';
+import { Database } from "../database";
+import * as path from "path";
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
+import { publicDirectory } from "..";
+
+export type Hierarchy = { [id: string]: string | Hierarchy };
+export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
+export interface DocumentElements {
+ data: string | any[];
+ title: string;
+}
+
+export default class DownloadManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ /**
+ * Let's say someone's using Dash to organize images in collections.
+ * This lets them export the hierarchy they've built to their
+ * own file system in a useful format.
+ *
+ * This handler starts with a single document id (interesting only
+ * if it's that of a collection). It traverses the database, captures
+ * the nesting of only nested images or collections, writes
+ * that to a zip file and returns it to the client for download.
+ */
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber("imageHierarchyExport").add('docId'),
+ onValidation: async ({ req, res }) => {
+ const id = req.params.docId;
+ const hierarchy: Hierarchy = {};
+ await buildHierarchyRecursive(id, hierarchy);
+ return BuildAndDispatchZip(res, zip => writeHierarchyRecursive(zip, hierarchy));
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber("downloadId").add("docId"),
+ onValidation: async ({ req, res }) => {
+ return BuildAndDispatchZip(res, async zip => {
+ const { id, docs, files } = await getDocs(req.params.docId);
+ const docString = JSON.stringify({ id, docs });
+ zip.append(docString, { name: "doc.json" });
+ files.forEach(val => {
+ zip.file(publicDirectory + val, { name: val.substring(1) });
+ });
+ });
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber("/serializeDoc").add("docId"),
+ onValidation: async ({ req, res }) => {
+ const { docs, files } = await getDocs(req.params.docId);
+ res.send({ docs, files: Array.from(files) });
+ }
+ });
+
+
+ }
+
+}
+
+async function getDocs(id: string) {
+ const files = new Set<string>();
+ const docs: { [id: string]: any } = {};
+ const fn = (doc: any): string[] => {
+ const id = doc.id;
+ if (typeof id === "string" && id.endsWith("Proto")) {
+ //Skip protos
+ return [];
+ }
+ const ids: string[] = [];
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+
+ if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ ids.push(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ ids.push(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ ids.push(...fn(field));
+ } else if (typeof field === "string") {
+ const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field)) !== null) {
+ ids.push(match[1]);
+ }
+ } else if (field.__type === "RichTextField") {
+ const re = /"href"\s*:\s*"(.*?)"/g;
+ let match: string[] | null;
+ while ((match = re.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const split = new URL(urlString).pathname.split("doc/");
+ if (split.length > 1) {
+ ids.push(split[split.length - 1]);
+ }
+ }
+ const re2 = /"src"\s*:\s*"(.*?)"/g;
+ while ((match = re2.exec(field.Data)) !== null) {
+ const urlString = match[1];
+ const pathname = new URL(urlString).pathname;
+ files.add(pathname);
+ }
+ } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
+ const url = new URL(field.url);
+ const pathname = url.pathname;
+ files.add(pathname);
+ }
+ }
+
+ if (doc.id) {
+ docs[doc.id] = doc;
+ }
+ return ids;
+ };
+ await Database.Instance.visit([id], fn);
+ return { id, docs, files };
+}
+
+/**
+ * This utility function factors out the process
+ * of creating a zip file and sending it back to the client
+ * by piping it into a response.
+ *
+ * Learn more about piping and readable / writable streams here!
+ * https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93/
+ *
+ * @param res the writable stream response object that will transfer the generated zip file
+ * @param mutator the callback function used to actually modify and insert information into the zip instance
+ */
+export async function BuildAndDispatchZip(res: express.Response, mutator: ZipMutator): Promise<void> {
+ res.set('Content-disposition', `attachment;`);
+ res.set('Content-Type', "application/zip");
+ const zip = Archiver('zip');
+ zip.pipe(res);
+ await mutator(zip);
+ return zip.finalize();
+}
+
+/**
+ * This function starts with a single document id as a seed,
+ * typically that of a collection, and then descends the entire tree
+ * of image or collection documents that are reachable from that seed.
+ * @param seedId the id of the root of the subtree we're trying to capture, interesting only if it's a collection
+ * @param hierarchy the data structure we're going to use to record the nesting of the collections and images as we descend
+ */
+
+/*
+Below is an example of the JSON hierarchy built from two images contained inside a collection titled 'a nested collection',
+following the general recursive structure shown immediately below
+{
+ "parent folder name":{
+ "first child's fild name":"first child's url"
+ ...
+ "nth child's fild name":"nth child's url"
+ }
+}
+{
+ "a nested collection (865c4734-c036-4d67-a588-c71bb43d1440)":{
+ "an image of a cat (ace99ffd-8ed8-4026-a5d5-a353fff57bdd).jpg":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg",
+ "1*SGJw31T5Q9Zfsk24l2yirg.gif (9321cc9b-9b3e-4cb6-b99c-b7e667340f05).gif":"https://cdn-media-1.freecodecamp.org/images/1*SGJw31T5Q9Zfsk24l2yirg.gif"
+ }
+}
+*/
+async function buildHierarchyRecursive(seedId: string, hierarchy: Hierarchy): Promise<void> {
+ const { title, data } = await getData(seedId);
+ const label = `${title} (${seedId})`;
+ // is the document a collection?
+ if (Array.isArray(data)) {
+ // recurse over all documents in the collection.
+ const local: Hierarchy = {}; // create a child hierarchy for this level, which will get passed in as the parent of the recursive call
+ hierarchy[label] = local; // store it at the index in the parent, so we'll end up with a map of maps of maps
+ await Promise.all(data.map(proxy => buildHierarchyRecursive(proxy.fieldId, local)));
+ } else {
+ // now, data can only be a string, namely the url of the image
+ const filename = label + path.extname(data); // this is the file name under which the output image will be stored
+ hierarchy[filename] = data;
+ }
+}
+
+/**
+ * This is a very specific utility method to help traverse the database
+ * to parse data and titles out of images and collections alone.
+ *
+ * We don't know if the document id given to is corresponds to a view document or a data
+ * document. If it's a data document, the response from the database will have
+ * a data field. If not, call recursively on the proto, and resolve with *its* data
+ *
+ * @param targetId the id of the Dash document whose data is being requests
+ * @returns the data of the document, as well as its title
+ */
+async function getData(targetId: string): Promise<DocumentElements> {
+ return new Promise<DocumentElements>((resolve, reject) => {
+ Database.Instance.getDocument(targetId, async (result: any) => {
+ const { data, proto, title } = result.fields;
+ if (data) {
+ if (data.url) {
+ resolve({ data: data.url, title });
+ } else if (data.fields) {
+ resolve({ data: data.fields, title });
+ } else {
+ reject();
+ }
+ } else if (proto) {
+ getData(proto.fieldId).then(resolve, reject);
+ } else {
+ reject();
+ }
+ });
+ });
+}
+
+/**
+ *
+ * @param file the zip file to which we write the files
+ * @param hierarchy the data structure from which we read, defining the nesting of the documents in the zip
+ * @param prefix lets us create nested folders in the zip file by continually appending to the end
+ * of the prefix with each layer of recursion.
+ *
+ * Function Call #1 => "Dash Export"
+ * Function Call #2 => "Dash Export/a nested collection"
+ * Function Call #3 => "Dash Export/a nested collection/lowest level collection"
+ * ...
+ */
+async function writeHierarchyRecursive(file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> {
+ for (const documentTitle of Object.keys(hierarchy)) {
+ const result = hierarchy[documentTitle];
+ // base case or leaf node, we've hit a url (image)
+ if (typeof result === "string") {
+ let path: string;
+ let matches: RegExpExecArray | null;
+ if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
+ // image already exists on our server
+ path = `${__dirname}/public/files/${matches[1]}`;
+ } else {
+ // the image doesn't already exist on our server (may have been dragged
+ // and dropped in the browser and thus hosted remotely) so we upload it
+ // to our server and point the zip file to it, so it can bundle up the bytes
+ const information = await DashUploadUtils.UploadImage(result);
+ path = information.serverAccessPaths[SizeSuffix.Original];
+ }
+ // write the file specified by the path to the directory in the
+ // zip file given by the prefix.
+ file.file(path, { name: documentTitle, prefix });
+ } else {
+ // we've hit a collection, so we have to recurse
+ await writeHierarchyRecursive(file, result, `${prefix}/${documentTitle}`);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
new file mode 100644
index 000000000..629684e0c
--- /dev/null
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -0,0 +1,58 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _permission_denied } from "../RouteManager";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { Database } from "../database";
+import RouteSubscriber from "../RouteSubscriber";
+
+const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
+
+const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
+ ["create", (api, params) => api.create(params)],
+ ["retrieve", (api, params) => api.get(params)],
+ ["update", (api, params) => api.batchUpdate(params)],
+]);
+
+export default class GeneralGoogleManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/readGoogleAccessToken",
+ onValidation: async ({ user, res }) => {
+ const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
+ if (!token) {
+ return res.send(GoogleApiServerUtils.generateAuthenticationUrl());
+ }
+ return res.send(token);
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/writeGoogleAccessToken",
+ onValidation: async ({ user, req, res }) => {
+ res.send(await GoogleApiServerUtils.processNewUser(user.id, req.body.authenticationCode));
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: new RouteSubscriber("/googleDocs").add("sector", "action"),
+ 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;
+ const endpoint = await GoogleApiServerUtils.GetEndpoint(GoogleApiServerUtils.Service[sector], user.id);
+ let handler = EndpointHandlerMap.get(action);
+ if (endpoint && handler) {
+ handler(endpoint, req.body)
+ .then(response => res.send(response.data))
+ .catch(exception => res.send(exception));
+ return;
+ }
+ res.send(undefined);
+ }
+ });
+
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts
new file mode 100644
index 000000000..4a0c0b936
--- /dev/null
+++ b/src/server/ApiManagers/GooglePhotosManager.ts
@@ -0,0 +1,115 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _error, _success, _invalid } from "../RouteManager";
+import * as path from "path";
+import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils";
+import { BatchedArray, TimeUnit } from "array-batcher";
+import { GooglePhotosUploadUtils } from "../apis/google/GooglePhotosUploadUtils";
+import { Opt } from "../../new_fields/Doc";
+import { DashUploadUtils, InjectSize, SizeSuffix } from "../DashUploadUtils";
+import { Database } from "../database";
+
+const authenticationError = "Unable to authenticate Google credentials before uploading to Google Photos!";
+const mediaError = "Unable to convert all uploaded bytes to media items!";
+const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
+const requestError = "Unable to execute download: the body's media items were malformed.";
+const downloadError = "Encountered an error while executing downloads.";
+interface GooglePhotosUploadFailure {
+ batch: number;
+ index: number;
+ url: string;
+ reason: string;
+}
+interface MediaItem {
+ baseUrl: string;
+ filename: string;
+}
+interface NewMediaItem {
+ description: string;
+ simpleMediaItem: {
+ uploadToken: string;
+ };
+}
+const prefix = "google_photos_";
+
+/**
+ * This manager handles the creation of routes for google photos functionality.
+ */
+export default class GooglePhotosManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.POST,
+ subscription: "/googlePhotosMediaUpload",
+ onValidation: async ({ user, req, res }) => {
+ const { media } = req.body;
+ const token = await GoogleApiServerUtils.retrieveAccessToken(user.id);
+ if (!token) {
+ return _error(res, authenticationError);
+ }
+ let failed: GooglePhotosUploadFailure[] = [];
+ const batched = BatchedArray.from<GooglePhotosUploadUtils.UploadSource>(media, { batchSize: 25 });
+ const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: any, collector: any, { completedBatches }: any) => {
+ for (let index = 0; index < batch.length; index++) {
+ const { url, description } = batch[index];
+ const fail = (reason: string) => failed.push({ reason, batch: completedBatches + 1, index, url });
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(token, InjectSize(url, SizeSuffix.Original)).catch(fail);
+ if (!uploadToken) {
+ fail(`${path.extname(url)} is not an accepted extension`);
+ } else {
+ collector.push({
+ description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
+ }
+ }
+ );
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+ console.log(failed.map(({ reason, batch, index, url }) => `@${batch}.${index}: ${url} failed:\n${reason}`).join('\n\n'));
+ }
+ return GooglePhotosUploadUtils.CreateMediaItems(token, newMediaItems, req.body.album).then(
+ results => _success(res, { results, failed }),
+ error => _error(res, mediaError, error)
+ );
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/googlePhotosMediaDownload",
+ onValidation: async ({ req, res }) => {
+ const contents: { mediaItems: MediaItem[] } = req.body;
+ let failed = 0;
+ if (contents) {
+ const completed: Opt<DashUploadUtils.ImageUploadInformation>[] = [];
+ for (let item of contents.mediaItems) {
+ const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
+ const found: Opt<DashUploadUtils.ImageUploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
+ if (!found) {
+ const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
+ if (upload) {
+ completed.push(upload);
+ await Database.Auxiliary.LogUpload(upload);
+ } else {
+ failed++;
+ }
+ } else {
+ completed.push(found);
+ }
+ }
+ if (failed) {
+ return _error(res, UploadError(failed));
+ }
+ return _success(res, completed);
+ }
+ _invalid(res, requestError);
+ }
+ });
+
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/PDFManager.ts b/src/server/ApiManagers/PDFManager.ts
new file mode 100644
index 000000000..151b48dd9
--- /dev/null
+++ b/src/server/ApiManagers/PDFManager.ts
@@ -0,0 +1,111 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import RouteSubscriber from "../RouteSubscriber";
+import { exists, createReadStream, createWriteStream } from "fs";
+import * as Pdfjs from 'pdfjs-dist';
+import { createCanvas } from "canvas";
+const imageSize = require("probe-image-size");
+import * as express from "express";
+import * as path from "path";
+import { Directory, serverPathToFile, clientPathToFile } from "./UploadManager";
+import { ConsoleColors } from "../ActionUtilities";
+
+export default class PDFManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber("thumbnail").add("filename"),
+ onValidation: ({ req, res }) => getOrCreateThumbnail(req.params.filename, res)
+ });
+
+ }
+
+}
+
+function getOrCreateThumbnail(thumbnailName: string, res: express.Response) {
+ const noExtension = thumbnailName.substring(0, thumbnailName.length - ".png".length);
+ const pageString = noExtension.split('-')[1];
+ const pageNumber = parseInt(pageString);
+ return new Promise<void>(resolve => {
+ const path = serverPathToFile(Directory.pdf_thumbnails, thumbnailName);
+ exists(path, (exists: boolean) => {
+ if (exists) {
+ let existingThumbnail = createReadStream(path);
+ imageSize(existingThumbnail, (err: any, { width, height }: any) => {
+ if (err) {
+ console.log(ConsoleColors.Red, `In PDF thumbnail response, unable to determine dimensions of ${thumbnailName}:`);
+ console.log(err);
+ return;
+ }
+ res.send({
+ path: clientPathToFile(Directory.pdf_thumbnails, thumbnailName),
+ width,
+ height
+ });
+ });
+ } else {
+ const offset = thumbnailName.length - pageString.length - 5;
+ const name = thumbnailName.substring(0, offset) + ".pdf";
+ const path = serverPathToFile(Directory.pdfs, name);
+ CreateThumbnail(path, pageNumber, res);
+ }
+ resolve();
+ });
+ });
+}
+
+async function CreateThumbnail(file: string, pageNumber: number, res: express.Response) {
+ const documentProxy = await Pdfjs.getDocument(file).promise;
+ const factory = new NodeCanvasFactory();
+ const page = await documentProxy.getPage(pageNumber);
+ const viewport = page.getViewport(1 as any);
+ const { canvas, context } = factory.create(viewport.width, viewport.height);
+ const renderContext = {
+ canvasContext: context,
+ canvasFactory: factory,
+ viewport
+ };
+ await page.render(renderContext).promise;
+ const pngStream = canvas.createPNGStream();
+ const filenames = path.basename(file).split(".");
+ const pngFile = serverPathToFile(Directory.pdf_thumbnails, `${filenames[0]}-${pageNumber}.png`);
+ const out = createWriteStream(pngFile);
+ pngStream.pipe(out);
+ out.on("finish", () => {
+ res.send({
+ path: pngFile,
+ width: viewport.width,
+ height: viewport.height
+ });
+ });
+ out.on("error", error => {
+ console.log(ConsoleColors.Red, `In PDF thumbnail creation, encountered the following error when piping ${pngFile}:`);
+ console.log(error);
+ });
+}
+
+class NodeCanvasFactory {
+
+ create = (width: number, height: number) => {
+ var canvas = createCanvas(width, height);
+ var context = canvas.getContext('2d');
+ return {
+ canvas,
+ context
+ };
+ }
+
+ reset = (canvasAndContext: any, width: number, height: number) => {
+ canvasAndContext.canvas.width = width;
+ canvasAndContext.canvas.height = height;
+ }
+
+ destroy = (canvasAndContext: any) => {
+ canvasAndContext.canvas.width = 0;
+ canvasAndContext.canvas.height = 0;
+ canvasAndContext.canvas = null;
+ canvasAndContext.context = null;
+ }
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts
new file mode 100644
index 000000000..d3f8995b0
--- /dev/null
+++ b/src/server/ApiManagers/SearchManager.ts
@@ -0,0 +1,49 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import { Search } from "../Search";
+var findInFiles = require('find-in-files');
+import * as path from 'path';
+import { pathToDirectory, Directory } from "./UploadManager";
+
+export default class SearchManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/textsearch",
+ onValidation: async ({ req, res }) => {
+ let q = req.query.q;
+ if (q === undefined) {
+ res.send([]);
+ return;
+ }
+ let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, pathToDirectory(Directory.text), ".txt$");
+ let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
+ for (var result in results) {
+ resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
+ resObj.lines.push(results[result].line);
+ resObj.numFound++;
+ }
+ res.send(resObj);
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/search",
+ onValidation: async ({ req, res }) => {
+ const solrQuery: any = {};
+ ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
+ if (solrQuery.q === undefined) {
+ res.send([]);
+ return;
+ }
+ let results = await Search.Instance.search(solrQuery);
+ res.send(results);
+ }
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts
new file mode 100644
index 000000000..80ae0ad61
--- /dev/null
+++ b/src/server/ApiManagers/UploadManager.ts
@@ -0,0 +1,222 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method, _success } from "../RouteManager";
+import * as formidable from 'formidable';
+import v4 = require('uuid/v4');
+var AdmZip = require('adm-zip');
+import { extname, basename, dirname } from 'path';
+import { createReadStream, createWriteStream, unlink, readFileSync } from "fs";
+import { publicDirectory, filesDirectory } from "..";
+import { Database } from "../database";
+import { DashUploadUtils, SizeSuffix } from "../DashUploadUtils";
+import * as sharp from 'sharp';
+import { AcceptibleMedia } from "../SharedMediaTypes";
+import { normalize } from "path";
+const imageDataUri = require('image-data-uri');
+
+export enum Directory {
+ parsed_files = "parsed_files",
+ images = "images",
+ videos = "videos",
+ pdfs = "pdfs",
+ text = "text",
+ pdf_thumbnails = "pdf_thumbnails"
+}
+
+export function serverPathToFile(directory: Directory, filename: string) {
+ return normalize(`${filesDirectory}/${directory}/${filename}`);
+}
+
+export function pathToDirectory(directory: Directory) {
+ return normalize(`${filesDirectory}/${directory}`);
+}
+
+export function clientPathToFile(directory: Directory, filename: string) {
+ return `/files/${directory}/${filename}`;
+}
+
+export default class UploadManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.POST,
+ subscription: "/upload",
+ onValidation: async ({ req, res }) => {
+ let form = new formidable.IncomingForm();
+ form.uploadDir = pathToDirectory(Directory.parsed_files);
+ form.keepExtensions = true;
+ return new Promise<void>(resolve => {
+ form.parse(req, async (_err, _fields, files) => {
+ let results: any[] = [];
+ for (const key in files) {
+ const result = await DashUploadUtils.upload(files[key]);
+ result && results.push(result);
+ }
+ _success(res, results);
+ resolve();
+ });
+ });
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/uploadDoc",
+ onValidation: ({ req, res }) => {
+ let form = new formidable.IncomingForm();
+ form.keepExtensions = true;
+ // let path = req.body.path;
+ const ids: { [id: string]: string } = {};
+ let remap = true;
+ const getId = (id: string): string => {
+ if (!remap) return id;
+ if (id.endsWith("Proto")) return id;
+ if (id in ids) {
+ return ids[id];
+ } else {
+ return ids[id] = v4();
+ }
+ };
+ const mapFn = (doc: any) => {
+ if (doc.id) {
+ doc.id = getId(doc.id);
+ }
+ for (const key in doc.fields) {
+ if (!doc.fields.hasOwnProperty(key)) {
+ continue;
+ }
+ const field = doc.fields[key];
+ if (field === undefined || field === null) {
+ continue;
+ }
+
+ if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
+ field.fieldId = getId(field.fieldId);
+ } else if (field.__type === "script" || field.__type === "computed") {
+ if (field.captures) {
+ field.captures.fieldId = getId(field.captures.fieldId);
+ }
+ } else if (field.__type === "list") {
+ mapFn(field);
+ } else if (typeof field === "string") {
+ const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
+ doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ } else if (field.__type === "RichTextField") {
+ const re = /("href"\s*:\s*")(.*?)"/g;
+ field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
+ return `${p1}${getId(p2)}"`;
+ });
+ }
+ }
+ };
+ return new Promise<void>(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 directory = dirname(entry.entryName) + "/";
+ let extension = extname(entry.entryName);
+ let base = basename(entry.entryName).split(".")[0];
+ try {
+ zip.extractEntryTo(entry.entryName, publicDirectory, true, false);
+ directory = "/" + directory;
+
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_o" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_s" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_m" + extension));
+ createReadStream(publicDirectory + directory + base + extension).pipe(createWriteStream(publicDirectory + directory + base + "_l" + extension));
+ } 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); }
+ unlink(path_2, () => { });
+ }
+ if (id) {
+ res.send(JSON.stringify(getId(id)));
+ } else {
+ res.send(JSON.stringify("error"));
+ }
+ } catch (e) { console.log(e); }
+ resolve();
+ });
+ });
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/inspectImage",
+ onValidation: async ({ req, res }) => {
+ const { source } = req.body;
+ if (typeof source === "string") {
+ const { serverAccessPaths } = await DashUploadUtils.UploadImage(source);
+ return res.send(await DashUploadUtils.InspectImage(serverAccessPaths[SizeSuffix.Original]));
+ }
+ res.send({});
+ }
+ });
+
+ register({
+ method: Method.POST,
+ subscription: "/uploadURI",
+ onValidation: ({ req, res }) => {
+ const uri = req.body.uri;
+ const filename = req.body.name;
+ if (!uri || !filename) {
+ res.status(401).send("incorrect parameters specified");
+ return;
+ }
+ return imageDataUri.outputFile(uri, serverPathToFile(Directory.images, filename)).then((savedName: string) => {
+ const ext = extname(savedName).toLowerCase();
+ const { pngs, jpgs } = AcceptibleMedia;
+ let resizers = [
+ { resizer: sharp().resize(100, undefined, { withoutEnlargement: true }), suffix: "_s" },
+ { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
+ { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
+ ];
+ let isImage = false;
+ if (pngs.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.png();
+ });
+ isImage = true;
+ } else if (jpgs.includes(ext)) {
+ resizers.forEach(element => {
+ element.resizer = element.resizer.jpeg();
+ });
+ isImage = true;
+ }
+ if (isImage) {
+ resizers.forEach(resizer => {
+ const path = serverPathToFile(Directory.images, filename + resizer.suffix + ext);
+ createReadStream(savedName).pipe(resizer.resizer).pipe(createWriteStream(path));
+ });
+ }
+ res.send(clientPathToFile(Directory.images, filename + ext));
+ });
+ }
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts
new file mode 100644
index 000000000..0f7d14320
--- /dev/null
+++ b/src/server/ApiManagers/UserManager.ts
@@ -0,0 +1,71 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import { Database } from "../database";
+import { msToTime } from "../ActionUtilities";
+
+export const timeMap: { [id: string]: number } = {};
+interface ActivityUnit {
+ user: string;
+ duration: number;
+}
+
+export default class UserManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: "/getUsers",
+ onValidation: async ({ res }) => {
+ const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
+ const results = await cursor.toArray();
+ res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/getUserDocumentId",
+ onValidation: ({ res, user }) => res.send(user.userDocumentId)
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/getCurrentUser",
+ onValidation: ({ res, user }) => res.send(JSON.stringify(user)),
+ onUnauthenticated: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/activity",
+ onValidation: ({ res }) => {
+ const now = Date.now();
+
+ const activeTimes: ActivityUnit[] = [];
+ const inactiveTimes: ActivityUnit[] = [];
+
+ for (const user in timeMap) {
+ const time = timeMap[user];
+ const duration = now - time;
+ const target = (duration / 1000) < (60 * 5) ? activeTimes : inactiveTimes;
+ target.push({ user, duration });
+ }
+
+ const process = (target: { user: string, duration: number }[]) => {
+ const comparator = (first: ActivityUnit, second: ActivityUnit) => first.duration - second.duration;
+ const sorted = target.sort(comparator);
+ return sorted.map(({ user, duration }) => `${user} (${msToTime(duration)})`);
+ };
+
+ res.render("user_activity.pug", {
+ title: "User Activity",
+ active: process(activeTimes),
+ inactive: process(inactiveTimes)
+ });
+ }
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/ApiManagers/UtilManager.ts b/src/server/ApiManagers/UtilManager.ts
new file mode 100644
index 000000000..c1234be6c
--- /dev/null
+++ b/src/server/ApiManagers/UtilManager.ts
@@ -0,0 +1,67 @@
+import ApiManager, { Registration } from "./ApiManager";
+import { Method } from "../RouteManager";
+import { exec } from 'child_process';
+import { command_line } from "../ActionUtilities";
+import RouteSubscriber from "../RouteSubscriber";
+
+export default class UtilManager extends ApiManager {
+
+ protected initialize(register: Registration): void {
+
+ register({
+ method: Method.GET,
+ subscription: new RouteSubscriber("environment").add("key"),
+ onValidation: ({ req, res }) => res.send(process.env[req.params.key])
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/pull",
+ onValidation: async ({ res }) => {
+ return new Promise<void>(resolve => {
+ exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', err => {
+ if (err) {
+ res.send(err.message);
+ return;
+ }
+ res.redirect("/");
+ resolve();
+ });
+ });
+ }
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/buxton",
+ 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);
+
+ return command_line('python scraper.py', cwd).then(onResolved, tryPython3);
+ },
+ });
+
+ register({
+ method: Method.GET,
+ subscription: "/version",
+ onValidation: ({ res }) => {
+ return new Promise<void>(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();
+ });
+ }
+ });
+
+ }
+
+} \ No newline at end of file
diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts
index 46d897339..9ccc72e35 100644
--- a/src/server/DashUploadUtils.ts
+++ b/src/server/DashUploadUtils.ts
@@ -5,41 +5,104 @@ import * as sharp from 'sharp';
import request = require('request-promise');
import { ExifData, ExifImage } from 'exif';
import { Opt } from '../new_fields/Doc';
+import { AcceptibleMedia } from './SharedMediaTypes';
+import { filesDirectory } from '.';
+import { File } from 'formidable';
+import { basename } from "path";
+import { ConsoleColors, createIfNotExists } from './ActionUtilities';
+import { ParsedPDF } from "../server/PdfTypes";
+const parse = require('pdf-parse');
+import { Directory, serverPathToFile, clientPathToFile } from './ApiManagers/UploadManager';
-const uploadDirectory = path.join(__dirname, './public/files/');
+export enum SizeSuffix {
+ Small = "_s",
+ Medium = "_m",
+ Large = "_l",
+ Original = "_o"
+}
+
+export function InjectSize(filename: string, size: SizeSuffix) {
+ const extension = path.extname(filename).toLowerCase();
+ return filename.substring(0, filename.length - extension.length) + size + extension;
+}
export namespace DashUploadUtils {
export interface Size {
width: number;
- suffix: string;
+ suffix: SizeSuffix;
+ }
+
+ export interface ImageFileResponse {
+ name: string;
+ path: string;
+ type: string;
+ exif: Opt<DashUploadUtils.EnrichedExifData>;
}
export const Sizes: { [size: string]: Size } = {
- SMALL: { width: 100, suffix: "_s" },
- MEDIUM: { width: 400, suffix: "_m" },
- LARGE: { width: 900, suffix: "_l" },
+ SMALL: { width: 100, suffix: SizeSuffix.Small },
+ MEDIUM: { width: 400, suffix: SizeSuffix.Medium },
+ LARGE: { width: 900, suffix: SizeSuffix.Large },
};
- const gifs = [".gif"];
- const pngs = [".png"];
- const jpgs = [".jpg", ".jpeg"];
- export const imageFormats = [...pngs, ...jpgs, ...gifs];
- const videoFormats = [".mov", ".mp4"];
+ export function validateExtension(url: string) {
+ return AcceptibleMedia.imageFormats.includes(path.extname(url).toLowerCase());
+ }
const size = "content-length";
const type = "content-type";
- export interface UploadInformation {
- mediaPaths: string[];
- fileNames: { [key: string]: string };
+ export interface ImageUploadInformation {
+ clientAccessPath: string;
+ serverAccessPaths: { [key: string]: string };
exifData: EnrichedExifData;
contentSize?: number;
contentType?: string;
}
+ const { imageFormats, videoFormats, applicationFormats } = AcceptibleMedia;
+
+ export async function upload(file: File): Promise<any> {
+ const { type, path, name } = file;
+ const types = type.split("/");
+
+ const category = types[0];
+ const format = `.${types[1]}`;
+
+ switch (category) {
+ case "image":
+ if (imageFormats.includes(format)) {
+ const results = await UploadImage(path, basename(path), format);
+ return { ...results, name, type };
+ }
+ case "video":
+ if (videoFormats.includes(format)) {
+ return MoveParsedFile(path, Directory.videos);
+ }
+ case "application":
+ if (applicationFormats.includes(format)) {
+ return UploadPdf(path);
+ }
+ }
+
+ console.log(ConsoleColors.Red, `Ignoring unsupported file (${name}) with upload type (${type}).`);
+ return { clientAccessPath: undefined };
+ }
+
+ async function UploadPdf(absolutePath: string) {
+ let dataBuffer = fs.readFileSync(absolutePath);
+ const result: ParsedPDF = await parse(dataBuffer);
+ const parsedName = basename(absolutePath);
+ await new Promise<void>((resolve, reject) => {
+ const textFilename = `${parsedName.substring(0, parsedName.length - 4)}.txt`;
+ const writeStream = fs.createWriteStream(serverPathToFile(Directory.text, textFilename));
+ writeStream.write(result.text, error => error ? reject(error) : resolve());
+ });
+ return MoveParsedFile(absolutePath, Directory.pdfs);
+ }
+
const generate = (prefix: string, url: string) => `${prefix}upload_${Utils.GenerateGuid()}${sanitizeExtension(url)}`;
- const sanitize = (filename: string) => filename.replace(/\s+/g, "_");
const sanitizeExtension = (source: string) => {
let extension = path.extname(source);
extension = extension.toLowerCase();
@@ -58,15 +121,15 @@ export namespace DashUploadUtils {
* @param {string} prefix is a string prepended to the generated image name in the
* event that @param filename is not specified
*
- * @returns {UploadInformation} This method returns
+ * @returns {ImageUploadInformation} This method returns
* 1) the paths to the uploaded images (plural due to resizing)
* 2) the file name of each of the resized images
* 3) the size of the image, in bytes (4432130)
* 4) the content type of the image, i.e. image/(jpeg | png | ...)
*/
- export const UploadImage = async (source: string, filename?: string, prefix: string = ""): Promise<UploadInformation> => {
+ export const UploadImage = async (source: string, filename?: string, format?: string, prefix: string = ""): Promise<ImageUploadInformation> => {
const metadata = await InspectImage(source);
- return UploadInspectedImage(metadata, filename, prefix);
+ return UploadInspectedImage(metadata, filename, format, prefix);
};
export interface InspectionResults {
@@ -83,6 +146,11 @@ export namespace DashUploadUtils {
error?: string;
}
+ export async function buildFileDirectories() {
+ const pending = Object.keys(Directory).map(sub => createIfNotExists(`${filesDirectory}/${sub}`));
+ return Promise.all(pending);
+ }
+
/**
* Based on the url's classification as local or remote, gleans
* as much information as possible about the specified image
@@ -102,65 +170,64 @@ export namespace DashUploadUtils {
if (isLocal) {
return results;
}
- const metadata = (await new Promise<any>((resolve, reject) => {
- request.head(source, async (error, res) => {
- if (error) {
- return reject(error);
- }
- resolve(res);
- });
- })).headers;
+ const { headers } = (await new Promise<any>((resolve, reject) => {
+ request.head(source, (error, res) => error ? reject(error) : resolve(res));
+ }));
return {
- contentSize: parseInt(metadata[size]),
- contentType: metadata[type],
+ contentSize: parseInt(headers[size]),
+ contentType: headers[type],
...results
};
};
- export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, prefix = ""): Promise<UploadInformation> => {
+ export async function MoveParsedFile(absolutePath: string, destination: Directory): Promise<{ clientAccessPath: Opt<string> }> {
+ return new Promise<{ clientAccessPath: Opt<string> }>(resolve => {
+ const filename = basename(absolutePath);
+ const destinationPath = serverPathToFile(destination, filename);
+ fs.rename(absolutePath, destinationPath, error => {
+ resolve({ clientAccessPath: error ? undefined : clientPathToFile(destination, filename) });
+ });
+ });
+ }
+
+ export const UploadInspectedImage = async (metadata: InspectionResults, filename?: string, format?: string, prefix = ""): Promise<ImageUploadInformation> => {
const { isLocal, stream, normalizedUrl, contentSize, contentType, exifData } = metadata;
- const resolved = filename ? sanitize(filename) : generate(prefix, normalizedUrl);
- const extension = sanitizeExtension(normalizedUrl || resolved);
- let information: UploadInformation = {
- mediaPaths: [],
- fileNames: { clean: resolved },
+ const resolved = filename || generate(prefix, normalizedUrl);
+ const extension = format || sanitizeExtension(normalizedUrl || resolved);
+ let information: ImageUploadInformation = {
+ clientAccessPath: clientPathToFile(Directory.images, resolved),
+ serverAccessPaths: {},
exifData,
contentSize,
contentType,
};
- return new Promise<UploadInformation>(async (resolve, reject) => {
+ const { pngs, jpgs } = AcceptibleMedia;
+ return new Promise<ImageUploadInformation>(async (resolve, reject) => {
const resizers = [
- { resizer: sharp().rotate(), suffix: "_o" },
+ { resizer: sharp().rotate(), suffix: SizeSuffix.Original },
...Object.values(Sizes).map(size => ({
resizer: sharp().resize(size.width, undefined, { withoutEnlargement: true }).rotate(),
suffix: size.suffix
}))
];
- let nonVisual = false;
if (pngs.includes(extension)) {
resizers.forEach(element => element.resizer = element.resizer.png());
} else if (jpgs.includes(extension)) {
resizers.forEach(element => element.resizer = element.resizer.jpeg());
- } else if (![...imageFormats, ...videoFormats].includes(extension.toLowerCase())) {
- nonVisual = true;
}
- if (imageFormats.includes(extension)) {
- for (let resizer of resizers) {
- const suffix = resizer.suffix;
- let mediaPath: string;
- await new Promise<void>(resolve => {
- const filename = resolved.substring(0, resolved.length - extension.length) + suffix + extension;
- information.mediaPaths.push(mediaPath = uploadDirectory + filename);
- information.fileNames[suffix] = filename;
- stream(normalizedUrl).pipe(resizer.resizer).pipe(fs.createWriteStream(mediaPath))
- .on('close', resolve)
- .on('error', reject);
- });
- }
- }
- if (!isLocal || nonVisual) {
+ for (let { resizer, suffix } of resizers) {
+ let mediaPath: string;
await new Promise<void>(resolve => {
- stream(normalizedUrl).pipe(fs.createWriteStream(uploadDirectory + resolved)).on('close', resolve);
+ const filename = InjectSize(resolved, suffix);
+ information.serverAccessPaths[suffix] = serverPathToFile(Directory.images, filename);
+ stream(normalizedUrl).pipe(resizer).pipe(fs.createWriteStream(serverPathToFile(Directory.images, filename)))
+ .on('close', resolve)
+ .on('error', reject);
+ });
+ }
+ if (isLocal) {
+ await new Promise<boolean>(resolve => {
+ fs.unlink(normalizedUrl, error => resolve(error === null));
});
}
resolve(information);
@@ -188,13 +255,4 @@ export namespace DashUploadUtils {
});
};
- export const createIfNotExists = async (path: string) => {
- if (await new Promise<boolean>(resolve => fs.exists(path, resolve))) {
- return true;
- }
- return new Promise<boolean>(resolve => fs.mkdir(path, error => resolve(error === null)));
- };
-
- export const Destroy = (mediaPath: string) => new Promise<boolean>(resolve => fs.unlink(mediaPath, error => resolve(error === null)));
-
} \ No newline at end of file
diff --git a/src/server/Initialization.ts b/src/server/Initialization.ts
new file mode 100644
index 000000000..8b633a7cd
--- /dev/null
+++ b/src/server/Initialization.ts
@@ -0,0 +1,139 @@
+import * as express from 'express';
+import * as expressValidator from 'express-validator';
+import * as session from 'express-session';
+import * as passport from 'passport';
+import * as bodyParser from 'body-parser';
+import * as cookieParser from 'cookie-parser';
+import expressFlash = require('express-flash');
+import flash = require('connect-flash');
+import { Database } from './database';
+import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
+const MongoStore = require('connect-mongo')(session);
+import RouteManager from './RouteManager';
+import * as webpack from 'webpack';
+const config = require('../../webpack.config');
+const compiler = webpack(config);
+import * as wdm from 'webpack-dev-middleware';
+import * as whm from 'webpack-hot-middleware';
+import * as fs from 'fs';
+import * as request from 'request';
+import RouteSubscriber from './RouteSubscriber';
+import { publicDirectory } from '.';
+import { ConsoleColors, logPort } from './ActionUtilities';
+import { timeMap } from './ApiManagers/UserManager';
+
+/* RouteSetter is a wrapper around the server that prevents the server
+ from being exposed. */
+export type RouteSetter = (server: RouteManager) => void;
+export interface InitializationOptions {
+ listenAtPort: number;
+ routeSetter: RouteSetter;
+}
+
+export default async function InitializeServer(options: InitializationOptions) {
+ const { listenAtPort, routeSetter } = options;
+ const server = buildWithMiddleware(express());
+
+ server.use(express.static(publicDirectory));
+ server.use("/images", express.static(publicDirectory));
+
+ server.use("*", ({ user, originalUrl }, _res, next) => {
+ if (!originalUrl.includes("Heartbeat")) {
+ const userEmail = user?.email;
+ if (userEmail) {
+ timeMap[userEmail] = Date.now();
+ }
+ }
+ next();
+ });
+
+ server.use(wdm(compiler, { publicPath: config.output.publicPath }));
+ server.use(whm(compiler));
+
+ registerAuthenticationRoutes(server);
+ registerCorsProxy(server);
+
+ const isRelease = determineEnvironment(); //vs. dev mode
+ routeSetter(new RouteManager(server, isRelease));
+
+ server.listen(listenAtPort, () => logPort("server", listenAtPort));
+ return isRelease;
+}
+
+const week = 7 * 24 * 60 * 60 * 1000;
+const secret = "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc";
+
+function buildWithMiddleware(server: express.Express) {
+ [
+ cookieParser(),
+ session({
+ secret,
+ resave: true,
+ cookie: { maxAge: week },
+ saveUninitialized: true,
+ store: new MongoStore({ url: Database.url })
+ }),
+ flash(),
+ expressFlash(),
+ bodyParser.json({ limit: "10mb" }),
+ bodyParser.urlencoded({ extended: true }),
+ expressValidator(),
+ passport.initialize(),
+ passport.session(),
+ (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ res.locals.user = req.user;
+ next();
+ }
+ ].forEach(next => server.use(next));
+ return server;
+}
+
+/* Determine if the enviroment is dev mode or release mode. */
+function determineEnvironment() {
+ const isRelease = process.env.RELEASE === "true";
+
+ console.log(`running server in ${isRelease ? 'release' : 'debug'} mode`);
+ console.log(process.env.PWD);
+
+ let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8");
+ clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(isRelease))}`;
+ fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8");
+
+ return isRelease;
+}
+
+function registerAuthenticationRoutes(server: express.Express) {
+ server.get("/signup", getSignup);
+ server.post("/signup", postSignup);
+
+ server.get("/login", getLogin);
+ server.post("/login", postLogin);
+
+ server.get("/logout", getLogout);
+
+ server.get("/forgotPassword", getForgot);
+ server.post("/forgotPassword", postForgot);
+
+ const reset = new RouteSubscriber("resetPassword").add("token").build;
+ server.get(reset, getReset);
+ server.post(reset, postReset);
+}
+
+function registerCorsProxy(server: express.Express) {
+ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
+ server.use("/corsProxy", (req, res) => {
+ req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
+ const headers = Object.keys(res.headers);
+ headers.forEach(headerName => {
+ const header = res.headers[headerName];
+ if (Array.isArray(header)) {
+ res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
+ } else if (header) {
+ if (headerCharRegex.test(header as any)) {
+ delete res.headers[headerName];
+ }
+ }
+ });
+ }).pipe(res);
+ });
+} \ No newline at end of file
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
new file mode 100644
index 000000000..7c49485f1
--- /dev/null
+++ b/src/server/RouteManager.ts
@@ -0,0 +1,151 @@
+import RouteSubscriber from "./RouteSubscriber";
+import { DashUserModel } from "./authentication/models/user_model";
+import * as express from 'express';
+import { ConsoleColors } from "./ActionUtilities";
+
+export enum Method {
+ GET,
+ POST
+}
+
+export interface CoreArguments {
+ req: express.Request;
+ res: express.Response;
+ isRelease: boolean;
+}
+
+export type OnValidation = (core: CoreArguments & { user: DashUserModel }) => any | Promise<any>;
+export type OnUnauthenticated = (core: CoreArguments) => any | Promise<any>;
+export type OnError = (core: CoreArguments & { error: any }) => any | Promise<any>;
+
+export interface RouteInitializer {
+ method: Method;
+ subscription: string | RouteSubscriber | (string | RouteSubscriber)[];
+ onValidation: OnValidation;
+ onUnauthenticated?: OnUnauthenticated;
+ onError?: OnError;
+}
+
+const registered = new Map<string, Set<Method>>();
+
+export default class RouteManager {
+ private server: express.Express;
+ private _isRelease: boolean;
+
+ public get isRelease() {
+ return this._isRelease;
+ }
+
+ constructor(server: express.Express, isRelease: boolean) {
+ this.server = server;
+ this._isRelease = isRelease;
+ }
+
+ /**
+ *
+ * @param initializer
+ */
+ addSupervisedRoute = (initializer: RouteInitializer): void => {
+ const { method, subscription, onValidation, onUnauthenticated, onError } = initializer;
+ const isRelease = this._isRelease;
+ let supervised = async (req: express.Request, res: express.Response) => {
+ const { user, originalUrl: target } = req;
+ const core = { req, res, isRelease };
+ const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => {
+ try {
+ await toExecute(args);
+ } catch (e) {
+ console.log(ConsoleColors.Red, target, user?.email ?? "<user logged out>");
+ if (onError) {
+ onError({ ...core, error: e });
+ } else {
+ _error(res, `The server encountered an internal error when serving ${target}.`, e);
+ }
+ }
+ };
+ if (user) {
+ await tryExecute(onValidation, { ...core, user });
+ } else {
+ req.session!.target = target;
+ if (onUnauthenticated) {
+ await tryExecute(onUnauthenticated, core);
+ if (!res.headersSent) {
+ res.redirect("/login");
+ }
+ } else {
+ res.redirect("/login");
+ }
+ }
+ 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 });
+ }
+ }, 1000);
+ };
+ const subscribe = (subscriber: RouteSubscriber | string) => {
+ let route: string;
+ if (typeof subscriber === "string") {
+ route = subscriber;
+ } else {
+ route = subscriber.build;
+ }
+ const existing = registered.get(route);
+ if (existing) {
+ if (existing.has(method)) {
+ console.log(ConsoleColors.Red, `\nDuplicate registration error: already registered ${route} with Method[${method}]`);
+ console.log('Please remove duplicate registrations before continuing...\n');
+ process.exit(0);
+ }
+ } else {
+ const specific = new Set<Method>();
+ specific.add(method);
+ registered.set(route, specific);
+ }
+ switch (method) {
+ case Method.GET:
+ this.server.get(route, supervised);
+ break;
+ case Method.POST:
+ this.server.post(route, supervised);
+ break;
+ }
+ };
+ if (Array.isArray(subscription)) {
+ subscription.forEach(subscribe);
+ } else {
+ subscribe(subscription);
+ }
+ }
+
+}
+
+export const STATUS = {
+ OK: 200,
+ BAD_REQUEST: 400,
+ EXECUTION_ERROR: 500,
+ PERMISSION_DENIED: 403
+};
+
+export function _error(res: express.Response, message: string, error?: any) {
+ console.error(message);
+ res.statusMessage = message;
+ res.status(STATUS.EXECUTION_ERROR).send(error);
+}
+
+export function _success(res: express.Response, body: any) {
+ res.status(STATUS.OK).send(body);
+}
+
+export function _invalid(res: express.Response, message: string) {
+ res.statusMessage = message;
+ res.status(STATUS.BAD_REQUEST).send();
+}
+
+export function _permission_denied(res: express.Response, message?: string) {
+ if (message) {
+ res.statusMessage = message;
+ }
+ res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
+}
diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts
deleted file mode 100644
index 7426ffb39..000000000
--- a/src/server/RouteStore.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-// PREPEND ALL ROUTES WITH FORWARD SLASHES!
-
-export enum RouteStore {
- // GENERAL
- root = "/",
- home = "/home",
- corsProxy = "/corsProxy",
- delete = "/delete",
- deleteAll = "/deleteAll",
-
- // UPLOAD AND STATIC FILE SERVING
- public = "/public",
- upload = "/upload",
- dataUriToImage = "/uploadURI",
- images = "/images",
- inspectImage = "/inspectImage",
- imageHierarchyExport = "/imageHierarchyExport",
-
- // USER AND WORKSPACES
- getCurrUser = "/getCurrentUser",
- getUsers = "/getUsers",
- getUserDocumentId = "/getUserDocumentId",
- updateCursor = "/updateCursor",
-
- openDocumentWithId = "/doc/:docId",
-
- // AUTHENTICATION
- signup = "/signup",
- login = "/login",
- logout = "/logout",
- forgot = "/forgotpassword",
- reset = "/reset/:token",
-
- // APIS
- cognitiveServices = "/cognitiveservices",
- googleDocs = "/googleDocs",
- readGoogleAccessToken = "/readGoogleAccessToken",
- writeGoogleAccessToken = "/writeGoogleAccessToken",
- googlePhotosMediaUpload = "/googlePhotosMediaUpload",
- googlePhotosMediaDownload = "/googlePhotosMediaDownload",
- googleDocsGet = "/googleDocsGet"
-
-} \ No newline at end of file
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
index e49be8af5..a1cf7c1c4 100644
--- a/src/server/RouteSubscriber.ts
+++ b/src/server/RouteSubscriber.ts
@@ -3,7 +3,7 @@ export default class RouteSubscriber {
private requestParameters: string[] = [];
constructor(root: string) {
- this._root = root;
+ this._root = `/${root}`;
}
add(...parameters: string[]) {
diff --git a/src/server/SharedMediaTypes.ts b/src/server/SharedMediaTypes.ts
new file mode 100644
index 000000000..8d0f441f0
--- /dev/null
+++ b/src/server/SharedMediaTypes.ts
@@ -0,0 +1,8 @@
+export namespace AcceptibleMedia {
+ export const gifs = [".gif"];
+ export const pngs = [".png"];
+ export const jpgs = [".jpg", ".jpeg"];
+ export const imageFormats = [...pngs, ...jpgs, ...gifs];
+ export const videoFormats = [".mov", ".mp4"];
+ export const applicationFormats = [".pdf"];
+} \ No newline at end of file
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
new file mode 100644
index 000000000..fbf71f707
--- /dev/null
+++ b/src/server/Websocket/Websocket.ts
@@ -0,0 +1,217 @@
+import { Utils } from "../../Utils";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes } from "../Message";
+import { Client } from "../Client";
+import { Socket } from "socket.io";
+import { Database } from "../database";
+import { Search } from "../Search";
+import * as io from 'socket.io';
+import YoutubeApi from "../apis/youtube/youtubeApiSample";
+import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader";
+import { ConsoleColors, logPort } from "../ActionUtilities";
+import { timeMap } from "../ApiManagers/UserManager";
+
+export namespace WebSocket {
+
+ let clients: { [key: string]: Client } = {};
+ export const socketMap = new Map<SocketIO.Socket, string>();
+
+ export async function start(serverPort: number, isRelease: boolean) {
+ await preliminaryFunctions();
+ initialize(serverPort, isRelease);
+ }
+
+ async function preliminaryFunctions() {
+ }
+
+ export function initialize(socketPort: number, isRelease: boolean) {
+ const endpoint = io();
+ endpoint.on("connection", function (socket: Socket) {
+ socket.use((_packet, next) => {
+ let userEmail = socketMap.get(socket);
+ if (userEmail) {
+ timeMap[userEmail] = Date.now();
+ }
+ next();
+ });
+
+ Utils.Emit(socket, MessageStore.Foo, "handshooken");
+
+ Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
+ Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
+ if (isRelease) {
+ Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
+ }
+
+ Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
+ Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
+ Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
+ Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
+ Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+ });
+ endpoint.listen(socketPort);
+ logPort("websocket", socketPort);
+ }
+
+ function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
+ const { ProjectCredentials } = GoogleCredentialsLoader;
+ switch (query.type) {
+ case YoutubeQueryTypes.Channels:
+ YoutubeApi.authorizedGetChannel(ProjectCredentials);
+ break;
+ case YoutubeQueryTypes.SearchVideo:
+ YoutubeApi.authorizedGetVideos(ProjectCredentials, query.userInput, callback);
+ case YoutubeQueryTypes.VideoDetails:
+ YoutubeApi.authorizedGetVideoDetails(ProjectCredentials, query.videoIds, callback);
+ }
+ }
+
+ export async function deleteFields() {
+ await Database.Instance.deleteAll();
+ await Search.Instance.clear();
+ await Database.Instance.deleteAll('newDocuments');
+ }
+
+ export async function deleteAll() {
+ await Database.Instance.deleteAll();
+ await Database.Instance.deleteAll('newDocuments');
+ await Database.Instance.deleteAll('sessions');
+ await Database.Instance.deleteAll('users');
+ await Search.Instance.clear();
+ }
+
+ function barReceived(socket: SocketIO.Socket, userEmail: string) {
+ clients[userEmail] = new Client(userEmail.toString());
+ console.log(ConsoleColors.Green, `user ${userEmail} has connected to the web socket`);
+ socketMap.set(socket, userEmail);
+ }
+
+ function getField([id, callback]: [string, (result?: Transferable) => void]) {
+ Database.Instance.getDocument(id, (result?: Transferable) =>
+ callback(result ? result : undefined));
+ }
+
+ function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) {
+ Database.Instance.getDocuments(ids, callback);
+ }
+
+ function setField(socket: Socket, newValue: Transferable) {
+ Database.Instance.update(newValue.id, newValue, () =>
+ socket.broadcast.emit(MessageStore.SetField.Message, newValue));
+ if (newValue.type === Types.Text) {
+ Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data });
+ console.log("set field");
+ console.log("checking in");
+ }
+ }
+
+ function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+ Database.Instance.getDocument(id, callback, "newDocuments");
+ }
+
+ function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
+ Database.Instance.getDocuments(ids, callback, "newDocuments");
+ }
+
+ const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
+ "number": "_n",
+ "string": "_t",
+ "boolean": "_b",
+ "image": ["_t", "url"],
+ "video": ["_t", "url"],
+ "pdf": ["_t", "url"],
+ "audio": ["_t", "url"],
+ "web": ["_t", "url"],
+ "date": ["_d", value => new Date(value.date).toISOString()],
+ "proxy": ["_i", "fieldId"],
+ "list": ["_l", list => {
+ const results = [];
+ for (const value of list.fields) {
+ const term = ToSearchTerm(value);
+ if (term) {
+ results.push(term.value);
+ }
+ }
+ return results.length ? results : null;
+ }]
+ };
+
+ function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
+ if (val === null || val === undefined) {
+ return;
+ }
+ const type = val.__type || typeof val;
+ let suffix = suffixMap[type];
+ if (!suffix) {
+ return;
+ }
+
+ if (Array.isArray(suffix)) {
+ const accessor = suffix[1];
+ if (typeof accessor === "function") {
+ val = accessor(val);
+ } else {
+ val = val[accessor];
+ }
+ suffix = suffix[0];
+ }
+
+ return { suffix, value: val };
+ }
+
+ function getSuffix(value: string | [string, any]): string {
+ return typeof value === "string" ? value : value[0];
+ }
+
+ function UpdateField(socket: Socket, diff: Diff) {
+ Database.Instance.update(diff.id, diff.diff,
+ () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
+ const docfield = diff.diff.$set;
+ if (!docfield) {
+ return;
+ }
+ const update: any = { id: diff.id };
+ let dynfield = false;
+ for (let key in docfield) {
+ if (!key.startsWith("fields.")) continue;
+ dynfield = true;
+ let val = docfield[key];
+ key = key.substring(7);
+ Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
+ let term = ToSearchTerm(val);
+ if (term !== undefined) {
+ let { suffix, value } = term;
+ update[key + suffix] = { set: value };
+ }
+ }
+ if (dynfield) {
+ Search.Instance.updateDocument(update);
+ }
+ }
+
+ function DeleteField(socket: Socket, id: string) {
+ Database.Instance.delete({ _id: id }, "newDocuments").then(() => {
+ socket.broadcast.emit(MessageStore.DeleteField.Message, id);
+ });
+
+ Search.Instance.deleteDocuments([id]);
+ }
+
+ function DeleteFields(socket: Socket, ids: string[]) {
+ Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => {
+ socket.broadcast.emit(MessageStore.DeleteFields.Message, ids);
+ });
+
+ Search.Instance.deleteDocuments(ids);
+
+ }
+
+ function CreateField(newValue: any) {
+ Database.Instance.insert(newValue, "newDocuments");
+ }
+
+}
+
diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 5714c9928..b0f3ba993 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -1,142 +1,282 @@
import { google } from "googleapis";
-import { createInterface } from "readline";
-import { readFile, writeFile } from "fs";
-import { OAuth2Client, Credentials } from "google-auth-library";
+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';
-import Photos = require('googlephotos');
import { Database } from "../../database";
+import { GoogleCredentialsLoader } from "../../credentials/CredentialsLoader";
+
/**
- * Server side authentication for Google Api queries.
+ * 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.
*/
-export namespace GoogleApiServerUtils {
+const scope = [
+ 'documents.readonly',
+ 'documents',
+ 'presentations',
+ 'presentations.readonly',
+ 'drive',
+ 'drive.file',
+ 'photoslibrary',
+ 'photoslibrary.appendonly',
+ 'photoslibrary.sharing',
+ 'userinfo.profile'
+].map(relative => `https://www.googleapis.com/auth/${relative}`);
- // If modifying these scopes, delete token.json.
- 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 const parseBuffer = (data: Buffer) => JSON.parse(data.toString());
+/**
+ * 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 {
+ /**
+ * 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",
Slides = "Slides"
}
- 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 oAuthOptions: 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;
+
+ /**
+ * 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 function processProjectCredentials(): void {
+ const { client_secret, client_id, redirect_uris } = GoogleCredentialsLoader.ProjectCredentials;
+ // initialize the global authorization client
+ oAuthOptions = {
+ clientId: client_id,
+ clientSecret: client_secret,
+ redirectUri: redirect_uris[0]
+ };
+ worker = generateClient();
}
+ /**
+ * 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";
- export type Endpoint = { get: ApiHandler, create: ApiHandler, batchUpdate: ApiHandler };
- export type EndpointParameters = GlobalOptions & { version: "v1" };
-
- export const GetEndpoint = (sector: string, paths: CredentialInformation) => {
- return new Promise<Opt<Endpoint>>(resolve => {
- RetrieveCredentials(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 RetrieveAccessToken = (information: CredentialInformation) => {
- return new Promise<string>((resolve, reject) => {
- RetrieveCredentials(information).then(
- credentials => resolve(credentials.token.access_token!),
- error => reject(`Error: unable to authenticate Google Photos API request.\n${error}`)
- );
+ /**
+ * 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;
+ create: ApiHandler;
+ batchUpdate: ApiHandler;
+ }
+
+ /**
+ * 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>();
+
+ /**
+ * 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(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);
});
- };
+ }
- const RetrieveOAuthClient = async (information: CredentialInformation) => {
- return new Promise<OAuth2Client>((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]));
- });
+ /**
+ * 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(async resolve => {
+ const { credentials } = await retrieveCredentials(userId);
+ if (!credentials) {
+ return resolve();
+ }
+ resolve(credentials.access_token!);
});
- };
+ }
- export const GenerateAuthenticationUrl = async (information: CredentialInformation) => {
- const client = await RetrieveOAuthClient(information);
- return client.generateAuthUrl({
- access_type: 'offline',
- scope: SCOPES.map(relative => prefix + relative),
+ /**
+ * 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.
+ */
+ export async function retrieveOAuthClient(userId: string): Promise<OAuth2Client> {
+ 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);
});
- };
+ }
+
+ /**
+ * Creates a new OAuth2Client instance, and if provided, sets
+ * 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(oAuthOptions);
+ credentials && client.setCredentials(credentials);
+ return client;
+ }
+
+ /**
+ * 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({ scope, access_type: 'offline' });
+ }
+ /**
+ * 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;
}
- export const ProcessClientSideCode = async (information: CredentialInformation, authenticationCode: string): Promise<GoogleAuthenticationResult> => {
- const oAuth2Client = await RetrieveOAuthClient(information);
- return new Promise<GoogleAuthenticationResult>((resolve, reject) => {
- oAuth2Client.getToken(authenticationCode, async (err, token) => {
- if (err || !token) {
+
+ /**
+ * 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.
+ *
+ * EXAMPLE CODE: 4/sgF2A5uGg4xASHf7VQDnLtdqo3mUlfQqLSce_HYz5qf1nFtHj9YTeGs
+ *
+ * @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 async function processNewUser(userId: string, authenticationCode: string): Promise<GoogleAuthenticationResult> {
+ const credentials = await new Promise<Credentials>((resolve, reject) => {
+ worker.getToken(authenticationCode, async (err, credentials) => {
+ if (err || !credentials) {
reject(err);
- return console.error('Error retrieving access token', err);
+ return;
}
- oAuth2Client.setCredentials(token);
- const enriched = injectUserInfo(token);
- await Database.Auxiliary.GoogleAuthenticationToken.Write(information.userId, enriched);
- const { given_name, picture } = enriched.userInfo;
- resolve({
- access_token: enriched.access_token!,
- avatar: picture,
- name: given_name
- });
+ resolve(credentials);
});
});
- };
+ const enriched = injectUserInfo(credentials);
+ await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched);
+ const { given_name, picture } = enriched.userInfo;
+ return {
+ access_token: enriched.access_token!,
+ avatar: picture,
+ name: given_name
+ };
+ }
/**
- * It's pretty cool: the credentials id_token is split into thirds by periods.
- * The middle third contains a base64-encoded JSON string with all the
- * user info contained in the interface below. So, we isolate that middle third,
- * base64 decode with atob and parse the JSON.
- * @param credentials the client credentials returned from OAuth after the user
- * has executed the authentication routine
+ * 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.
*/
- const injectUserInfo = (credentials: Credentials): EnrichedCredentials => {
- const userInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
- return { ...credentials, userInfo };
- };
-
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;
@@ -152,70 +292,73 @@ export namespace GoogleApiServerUtils {
sub: string;
}
- export const RetrieveCredentials = (information: CredentialInformation) => {
- return new Promise<TokenResult>((resolve, reject) => {
- readFile(information.credentialsPath, async (err, credentials) => {
- if (err) {
- reject(err);
- return console.log('Error loading client secret file:', err);
- }
- authorize(parseBuffer(credentials), information.userId).then(resolve, reject);
- });
- });
- };
-
- export const RetrievePhotosEndpoint = (paths: CredentialInformation) => {
- return new Promise<any>((resolve, reject) => {
- RetrieveAccessToken(paths).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<TokenResult> {
- 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<TokenResult>((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 });
- });
- });
+ /**
+ * It's pretty cool: the credentials id_token is split into thirds by periods.
+ * The middle third contains a base64-encoded JSON string with all the
+ * user info contained in the interface below. So, we isolate that middle third,
+ * base64 decode with atob and parse the JSON.
+ * @param credentials the client credentials returned from OAuth after the user
+ * has executed the authentication routine
+ * @returns the full set of credentials in the structure in which they'll be stored
+ * in the database.
+ */
+ function injectUserInfo(credentials: Credentials): EnrichedCredentials {
+ const userInfo: UserInfo = JSON.parse(atob(credentials.id_token!.split(".")[1]));
+ return { ...credentials, userInfo };
}
- const refreshEndpoint = "https://oauth2.googleapis.com/token";
- const refreshToken = (credentials: Credentials, client_id: string, client_secret: string, oAuth2Client: OAuth2Client, userId: string) => {
- return new Promise<TokenResult>(resolve => {
- 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(async response => {
- let { access_token, expires_in } = JSON.parse(response);
- const expiry_date = new Date().getTime() + (expires_in * 1000);
- 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 });
- });
+ /**
+ * 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, each user might
+ * be associated with multiple different sets of Google credentials.
+ * @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<{ credentials: Opt<Credentials>, refreshed: boolean }> {
+ let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
+ let refreshed = false;
+ if (!credentials) {
+ return { credentials: undefined, refreshed };
+ }
+ // check for token expiry
+ if (credentials.expiry_date! <= new Date().getTime()) {
+ credentials = await refreshAccessToken(credentials, userId);
+ }
+ return { credentials, refreshed };
+ }
+
+ /**
+ * 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 the id of the Dash user implicitly requesting that
+ * his/her credentials be refreshed
+ * @returns the updated credentials
+ */
+ async function refreshAccessToken(credentials: Credentials, userId: string): Promise<Credentials> {
+ let headerParameters = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } };
+ const { client_id, client_secret } = GoogleCredentialsLoader.ProjectCredentials;
+ let url = `https://oauth2.googleapis.com/token?${qs.stringify({
+ refreshToken: credentials.refresh_token,
+ client_id,
+ client_secret,
+ grant_type: "refresh_token"
+ })}`;
+ const { access_token, expires_in } = await new Promise<any>(async resolve => {
+ const response = await request.post(url, headerParameters);
+ resolve(JSON.parse(response));
});
- };
+ // expires_in is in seconds, but we're building the new expiry date in milliseconds
+ const expiry_date = new Date().getTime() + (expires_in * 1000);
+ await Database.Auxiliary.GoogleAuthenticationToken.Update(userId, access_token, expiry_date);
+ // update the relevant properties
+ credentials.access_token = access_token;
+ credentials.expiry_date = expiry_date;
+ return credentials;
+ }
} \ No newline at end of file
diff --git a/src/server/apis/google/GooglePhotosUploadUtils.ts b/src/server/apis/google/GooglePhotosUploadUtils.ts
index 36256822c..8ae63caa3 100644
--- a/src/server/apis/google/GooglePhotosUploadUtils.ts
+++ b/src/server/apis/google/GooglePhotosUploadUtils.ts
@@ -1,74 +1,137 @@
import request = require('request-promise');
-import { GoogleApiServerUtils } from './GoogleApiServerUtils';
import * as path from 'path';
-import { MediaItemCreationResult, NewMediaItemResult } from './SharedTypes';
-import { NewMediaItem } from "../../index";
+import { NewMediaItemResult } from './SharedTypes';
import { BatchedArray, TimeUnit } from 'array-batcher';
import { DashUploadUtils } from '../../DashUploadUtils';
+/**
+ * This namespace encompasses the logic
+ * necessary to upload images to Google's server,
+ * and then initialize / create those images in the Photos
+ * API given the upload tokens returned from the initial
+ * uploading process.
+ *
+ * https://developers.google.com/photos/library/reference/rest/v1/mediaItems/batchCreate
+ */
export namespace GooglePhotosUploadUtils {
- export interface Paths {
- uploadDirectory: string;
- credentialsPath: string;
- tokenPath: string;
- }
-
- export interface MediaInput {
+ /**
+ * Specifies the structure of the object
+ * necessary to upload bytes to Google's servers.
+ * The url is streamed to access the image's bytes,
+ * and the description is what appears in Google Photos'
+ * description field.
+ */
+ export interface UploadSource {
url: string;
description: string;
}
- const prepend = (extension: string) => `https://photoslibrary.googleapis.com/v1/${extension}`;
- const headers = (type: string) => ({
- 'Content-Type': `application/${type}`,
- 'Authorization': Bearer,
- });
+ /**
+ * This is the format needed to pass
+ * into the BatchCreate API request
+ * to take a reference to raw uploaded bytes
+ * and actually create an image in Google Photos.
+ *
+ * So, to instantiate this interface you must have already dispatched an upload
+ * and received an upload token.
+ */
+ export interface NewMediaItem {
+ description: string;
+ simpleMediaItem: {
+ uploadToken: string;
+ };
+ }
- let Bearer: string;
+ /**
+ * A utility function to streamline making
+ * calls to the API's url - accentuates
+ * the relative path in the caller.
+ * @param extension the desired
+ * subset of the API
+ */
+ function prepend(extension: string): string {
+ return `https://photoslibrary.googleapis.com/v1/${extension}`;
+ }
- export const initialize = async (information: GoogleApiServerUtils.CredentialInformation) => {
- const token = await GoogleApiServerUtils.RetrieveAccessToken(information);
- Bearer = `Bearer ${token}`;
- };
+ /**
+ * Factors out the creation of the API request's
+ * authentication elements stored in the header.
+ * @param type the contents of the request
+ * @param token the user-specific Google access token
+ */
+ function headers(type: string, token: string) {
+ return {
+ 'Content-Type': `application/${type}`,
+ 'Authorization': `Bearer ${token}`,
+ };
+ }
- export const DispatchGooglePhotosUpload = async (url: string) => {
- if (!DashUploadUtils.imageFormats.includes(path.extname(url))) {
+ /**
+ * This is the first step in the remote image creation process.
+ * Here we upload the raw bytes of the image to Google's servers by
+ * setting authentication and other required header properties and including
+ * the raw bytes to the image, to be uploaded, in the body of the request.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param url the url of the image to upload
+ * @param filename an optional name associated with the uploaded image - if not specified
+ * defaults to the filename (basename) in the url
+ */
+ export const DispatchGooglePhotosUpload = async (bearerToken: string, url: string, filename?: string): Promise<any> => {
+ // check if the url points to a non-image or an unsupported format
+ if (!DashUploadUtils.validateExtension(url)) {
return undefined;
}
- const body = await request(url, { encoding: null });
const parameters = {
method: 'POST',
+ uri: prepend('uploads'),
headers: {
- ...headers('octet-stream'),
- 'X-Goog-Upload-File-Name': path.basename(url),
+ ...headers('octet-stream', bearerToken),
+ 'X-Goog-Upload-File-Name': filename || path.basename(url),
'X-Goog-Upload-Protocol': 'raw'
},
- uri: prepend('uploads'),
- body
+ body: await request(url, { encoding: null }) // returns a readable stream with the unencoded binary image data
};
- return new Promise<any>((resolve, reject) => request(parameters, (error, _response, body) => {
+ return new Promise((resolve, reject) => request(parameters, (error, _response, body) => {
if (error) {
- console.log(error);
+ // on rejection, the server logs the error and the offending image
return reject(error);
}
resolve(body);
}));
};
- export const CreateMediaItems = async (newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
+ /**
+ * This is the second step in the remote image creation process: having uploaded
+ * the raw bytes of the image and received / stored pointers (upload tokens) to those
+ * bytes, we can now instruct the API to finalize the creation of those images by
+ * submitting a batch create request with the list of upload tokens and the description
+ * to be associated with reach resulting new image.
+ * @param bearerToken the user-specific Google access token, specifies the account associated
+ * with the eventual image creation
+ * @param newMediaItems a list of objects containing a description and, effectively, the
+ * pointer to the uploaded bytes
+ * @param album if included, will add all of the newly created remote images to the album
+ * with the specified id
+ */
+ export const CreateMediaItems = async (bearerToken: string, newMediaItems: NewMediaItem[], album?: { id: string }): Promise<NewMediaItemResult[]> => {
+ // it's important to note that the API can't handle more than 50 items in each request and
+ // seems to need at least some latency between requests (spamming it synchronously has led to the server returning errors)...
const batched = BatchedArray.from(newMediaItems, { batchSize: 50 });
+ // ...so we execute them in delayed batches and await the entire execution
return batched.batchedMapPatientInterval(
{ magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch, collector) => {
+ async (batch: NewMediaItem[], collector: any): Promise<any> => {
const parameters = {
method: 'POST',
- headers: headers('json'),
+ headers: headers('json', bearerToken),
uri: prepend('mediaItems:batchCreate'),
body: { newMediaItems: batch } as any,
json: true
};
+ // register the target album, if provided
album && (parameters.body.albumId = album.id);
collector.push(...(await new Promise<NewMediaItemResult[]>((resolve, reject) => {
request(parameters, (error, _response, body) => {
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
index 8915a4abf..726df7fd7 100644
--- a/src/server/authentication/config/passport.ts
+++ b/src/server/authentication/config/passport.ts
@@ -3,7 +3,6 @@ import * as passportLocal from 'passport-local';
import _ from "lodash";
import { default as User } from '../models/user_model';
import { Request, Response, NextFunction } from "express";
-import { RouteStore } from '../../RouteStore';
const LocalStrategy = passportLocal.Strategy;
@@ -35,13 +34,13 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) =
if (req.isAuthenticated()) {
return next();
}
- return res.redirect(RouteStore.login);
+ return res.redirect("/login");
};
export let isAuthorized = (req: Request, res: Response, next: NextFunction) => {
const provider = req.path.split("/").slice(-1)[0];
- if (_.find((req.user as any).tokens, { kind: provider })) {
+ if (_.find((req.user as any).tokens!, { kind: provider })) {
next();
} else {
res.redirect(`/auth/${provider}`);
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
index f5c6e1610..517353479 100644
--- a/src/server/authentication/controllers/user_controller.ts
+++ b/src/server/authentication/controllers/user_controller.ts
@@ -3,17 +3,11 @@ import { Request, Response, NextFunction } from "express";
import * as passport from "passport";
import { IVerifyOptions } from "passport-local";
import "../config/passport";
-import * as request from "express-validator";
import flash = require("express-flash");
-import * as session from "express-session";
-import * as pug from 'pug';
import * as async from 'async';
import * as nodemailer from 'nodemailer';
import c = require("crypto");
-import { RouteStore } from "../../RouteStore";
import { Utils } from "../../../Utils";
-import { Schema } from "mongoose";
-import { Opt } from "../../../new_fields/Doc";
import { MailOptions } from "nodemailer/lib/stream-transport";
/**
@@ -23,8 +17,7 @@ import { MailOptions } from "nodemailer/lib/stream-transport";
*/
export let getSignup = (req: Request, res: Response) => {
if (req.user) {
- let user = req.user;
- return res.redirect(RouteStore.home);
+ return res.redirect("/home");
}
res.render("signup.pug", {
title: "Sign Up",
@@ -45,7 +38,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
const errors = req.validationErrors();
if (errors) {
- return res.redirect(RouteStore.signup);
+ return res.redirect("/signup");
}
const email = req.body.email as String;
@@ -62,7 +55,7 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => {
User.findOne({ email }, (err, existingUser) => {
if (err) { return next(err); }
if (existingUser) {
- return res.redirect(RouteStore.login);
+ return res.redirect("/login");
}
user.save((err: any) => {
if (err) { return next(err); }
@@ -81,7 +74,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
req.session.target = undefined;
res.redirect(target);
} else {
- res.redirect(RouteStore.home);
+ res.redirect("/home");
}
};
@@ -93,7 +86,7 @@ let tryRedirectToTarget = (req: Request, res: Response) => {
export let getLogin = (req: Request, res: Response) => {
if (req.user) {
req.session!.target = undefined;
- return res.redirect(RouteStore.home);
+ return res.redirect("/home");
}
res.render("login.pug", {
title: "Log In",
@@ -115,13 +108,13 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => {
if (errors) {
req.flash("errors", "Unable to login at this time. Please try again.");
- return res.redirect(RouteStore.signup);
+ return res.redirect("/signup");
}
passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => {
if (err) { next(err); return; }
if (!user) {
- return res.redirect(RouteStore.signup);
+ return res.redirect("/signup");
}
req.logIn(user, (err) => {
if (err) { next(err); return; }
@@ -141,7 +134,7 @@ export let getLogout = (req: Request, res: Response) => {
if (sess) {
sess.destroy((err) => { if (err) { console.log(err); } });
}
- res.redirect(RouteStore.login);
+ res.redirect("/login");
};
export let getForgot = function (req: Request, res: Response) {
@@ -168,7 +161,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
User.findOne({ email }, function (err, user: DashUserModel) {
if (!user) {
// NO ACCOUNT WITH SUBMITTED EMAIL
- res.redirect(RouteStore.forgot);
+ res.redirect("/forgotPassword");
return;
}
user.passwordResetToken = token;
@@ -192,7 +185,7 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
subject: 'Dash Password Reset',
text: 'You are receiving this because you (or someone else) have requested the reset of the password for your account.\n\n' +
'Please click on the following link, or paste this into your browser to complete the process:\n\n' +
- 'http://' + req.headers.host + '/reset/' + token + '\n\n' +
+ 'http://' + req.headers.host + '/resetPassword/' + token + '\n\n' +
'If you did not request this, please ignore this email and your password will remain unchanged.\n'
} as MailOptions;
smtpTransport.sendMail(mailOptions, function (err: Error | null) {
@@ -202,14 +195,14 @@ export let postForgot = function (req: Request, res: Response, next: NextFunctio
}
], function (err) {
if (err) return next(err);
- res.redirect(RouteStore.forgot);
+ res.redirect("/forgotPassword");
});
};
export let getReset = function (req: Request, res: Response) {
User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) {
if (!user || err) {
- return res.redirect(RouteStore.forgot);
+ return res.redirect("/forgotPassword");
}
res.render("reset.pug", {
title: "Reset Password",
@@ -239,7 +232,7 @@ export let postReset = function (req: Request, res: Response) {
user.save(function (err) {
if (err) {
- res.redirect(RouteStore.login);
+ res.redirect("/login");
return;
}
req.logIn(user, function (err) {
@@ -271,6 +264,6 @@ export let postReset = function (req: Request, res: Response) {
});
}
], function (err) {
- res.redirect(RouteStore.login);
+ res.redirect("/login");
});
}; \ No newline at end of file
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 5b9bba47d..ac4462f78 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -1,4 +1,4 @@
-import { action, computed, observable, reaction, runInAction } from "mobx";
+import { action, computed, observable, reaction } from "mobx";
import * as rp from 'request-promise';
import { DocServer } from "../../../client/DocServer";
import { Docs } from "../../../client/documents/Documents";
@@ -11,10 +11,9 @@ import { listSpec } from "../../../new_fields/Schema";
import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
import { Cast, PromiseValue } from "../../../new_fields/Types";
import { Utils } from "../../../Utils";
-import { RouteStore } from "../../RouteStore";
-import { InkingControl } from "../../../client/views/InkingControl";
-import { DragManager } from "../../../client/util/DragManager";
import { nullAudio } from "../../../new_fields/URLField";
+import { DragManager } from "../../../client/util/DragManager";
+import { InkingControl } from "../../../client/views/InkingControl";
export class CurrentUserUtils {
private static curr_id: string;
@@ -206,8 +205,8 @@ export class CurrentUserUtils {
return doc;
}
- public static loadCurrentUser() {
- return rp.get(Utils.prepend(RouteStore.getCurrUser)).then(response => {
+ public static async loadCurrentUser() {
+ return rp.get(Utils.prepend("/getCurrentUser")).then(response => {
if (response) {
const result: { id: string, email: string } = JSON.parse(response);
return result;
@@ -220,7 +219,7 @@ export class CurrentUserUtils {
public static async loadUserDocument({ id, email }: { id: string, email: string }) {
this.curr_id = id;
Doc.CurrentUserEmail = email;
- await rp.get(Utils.prepend(RouteStore.getUserDocumentId)).then(id => {
+ await rp.get(Utils.prepend("/getUserDocumentId")).then(id => {
if (id && id !== "guest") {
return DocServer.GetRefField(id).then(async field =>
Doc.SetUserDoc(await this.updateUserDocument(field instanceof Doc ? field : new Doc(id, true))));
diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts
index 45fbf23b1..cc670a03a 100644
--- a/src/server/authentication/models/user_model.ts
+++ b/src/server/authentication/models/user_model.ts
@@ -1,20 +1,8 @@
//@ts-ignore
import * as bcrypt from "bcrypt-nodejs";
//@ts-ignore
-import * as mongoose from "mongoose";
-var url = 'mongodb://localhost:27017/Dash';
+import * as mongoose from 'mongoose';
-mongoose.connect(url, { useNewUrlParser: true });
-
-mongoose.connection.on('connected', function () {
- console.log('Stablished connection on ' + url);
-});
-mongoose.connection.on('error', function (error) {
- console.log('Something wrong happened: ' + error);
-});
-mongoose.connection.on('disconnected', function () {
- console.log('connection closed');
-});
export type DashUserModel = mongoose.Document & {
email: String,
password: string,
diff --git a/src/server/credentials/CredentialsLoader.ts b/src/server/credentials/CredentialsLoader.ts
new file mode 100644
index 000000000..e3f4d167b
--- /dev/null
+++ b/src/server/credentials/CredentialsLoader.ts
@@ -0,0 +1,29 @@
+import { readFile } from "fs";
+
+export namespace GoogleCredentialsLoader {
+
+ export interface InstalledCredentials {
+ client_id: string;
+ project_id: string;
+ auth_uri: string;
+ token_uri: string;
+ auth_provider_x509_cert_url: string;
+ client_secret: string;
+ redirect_uris: string[];
+ }
+
+ export let ProjectCredentials: InstalledCredentials;
+
+ export async function loadCredentials() {
+ ProjectCredentials = await new Promise<InstalledCredentials>(resolve => {
+ readFile(__dirname + '/google_project_credentials.json', function processClientSecrets(err, content) {
+ if (err) {
+ console.log('Error loading client secret file: ' + err);
+ return;
+ }
+ resolve(JSON.parse(content.toString()).installed);
+ });
+ });
+ }
+
+}
diff --git a/src/server/credentials/google_docs_credentials.json b/src/server/credentials/google_project_credentials.json
index 955c5a3c1..955c5a3c1 100644
--- a/src/server/credentials/google_docs_credentials.json
+++ b/src/server/credentials/google_project_credentials.json
diff --git a/src/server/database.ts b/src/server/database.ts
index db86b472d..db81245c1 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -5,19 +5,65 @@ import { Utils, emptyFunction } from '../Utils';
import { DashUploadUtils } from './DashUploadUtils';
import { Credentials } from 'google-auth-library';
import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils';
+import * as mongoose from 'mongoose';
export namespace Database {
+ const schema = 'Dash';
+ const port = 27017;
+ export const url = `mongodb://localhost:${port}/${schema}`;
+
+ enum ConnectionStates {
+ disconnected = 0,
+ connected = 1,
+ connecting = 2,
+ disconnecting = 3,
+ uninitialized = 99,
+ }
+
+ export async function tryInitializeConnection() {
+ try {
+ const { connection } = mongoose;
+ process.on('SIGINT', () => {
+ connection.close(() => {
+ console.log(`SIGINT closed mongoose connection at ${url}`);
+ process.exit(0);
+ });
+ });
+ if (connection.readyState === ConnectionStates.disconnected) {
+ await new Promise<void>((resolve, reject) => {
+ connection.on('error', reject);
+ connection.on('disconnected', () => {
+ console.log(`disconnecting mongoose connection at ${url}`);
+ });
+ connection.on('connected', () => {
+ console.log(`mongoose established default connection at ${url}`);
+ resolve();
+ });
+ mongoose.connect(url, { useNewUrlParser: true });
+ });
+ }
+ } catch (e) {
+ console.error(`Mongoose FAILED to establish default connection at ${url} with the following error:`);
+ console.error(e);
+ console.log('Since a valid database connection is required to use Dash, the server process will now exit.\nPlease try again later.');
+ process.exit(1);
+ }
+ }
+
class Database {
public static DocumentsCollection = 'documents';
private MongoClient = mongodb.MongoClient;
- private url = 'mongodb://localhost:27017/Dash';
private currentWrites: { [id: string]: Promise<void> } = {};
private db?: mongodb.Db;
private onConnect: (() => void)[] = [];
constructor() {
- this.MongoClient.connect(this.url, (err, client) => {
+ this.MongoClient.connect(url, (_err, client) => {
+ if (!client) {
+ console.error("\nPlease start MongoDB by running 'mongod' in a terminal before continuing...\n");
+ process.exit(0);
+ }
this.db = client.db();
this.onConnect.forEach(fn => fn());
});
@@ -247,7 +293,7 @@ export namespace Database {
};
export const QueryUploadHistory = async (contentSize: number) => {
- return SanitizedSingletonQuery<DashUploadUtils.UploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
+ return SanitizedSingletonQuery<DashUploadUtils.ImageUploadInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
};
export namespace GoogleAuthenticationToken {
@@ -256,7 +302,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<Opt<StoredCredentials>> => {
return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId);
};
@@ -276,7 +322,7 @@ export namespace Database {
}
- export const LogUpload = async (information: DashUploadUtils.UploadInformation) => {
+ export const LogUpload = async (information: DashUploadUtils.ImageUploadInformation) => {
const bundle = {
_id: Utils.GenerateDeterministicGuid(String(information.contentSize!)),
...information
diff --git a/src/server/index.ts b/src/server/index.ts
index ddd909479..d77923710 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -1,1257 +1,112 @@
require('dotenv').config();
-import * as bodyParser from 'body-parser';
-import { exec, ExecOptions } from 'child_process';
-import * as cookieParser from 'cookie-parser';
-import * as express from 'express';
-import * as session from 'express-session';
-import * as expressValidator from 'express-validator';
-import * as formidable from 'formidable';
-import * as fs from 'fs';
-import * as sharp from 'sharp';
-import * as Pdfjs from 'pdfjs-dist';
-const imageDataUri = require('image-data-uri');
+import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils";
import * as mobileDetect from 'mobile-detect';
-import * as passport from 'passport';
import * as path from 'path';
-import * as request from 'request';
-import * as io from 'socket.io';
-import { Socket } from 'socket.io';
-import * as webpack from 'webpack';
-import * as wdm from 'webpack-dev-middleware';
-import * as whm from 'webpack-hot-middleware';
-import { Utils } from '../Utils';
-import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller';
-import { DashUserModel } from './authentication/models/user_model';
-import { Client } from './Client';
import { Database } from './database';
-import { MessageStore, Transferable, Types, Diff, YoutubeQueryTypes as YoutubeQueryType, YoutubeQueryInput } from "./Message";
-import { RouteStore } from './RouteStore';
-import v4 = require('uuid/v4');
-const app = express();
-const config = require('../../webpack.config');
-import { createCanvas } from "canvas";
-const compiler = webpack(config);
-const port = 1050; // default port to listen
const serverPort = 4321;
-import expressFlash = require('express-flash');
-import flash = require('connect-flash');
-import { Search } from './Search';
-import * as Archiver from 'archiver';
-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";
-const MongoStore = require('connect-mongo')(session);
-const mongoose = require('mongoose');
-const probe = require("probe-image-size");
-const pdf = require('pdf-parse');
-var findInFiles = require('find-in-files');
-import { GooglePhotosUploadUtils } from './apis/google/GooglePhotosUploadUtils';
-import * as qs from 'query-string';
-import { Opt } from '../new_fields/Doc';
import { DashUploadUtils } from './DashUploadUtils';
-import { BatchedArray, TimeUnit } from 'array-batcher';
-import { ParsedPDF } from "./PdfTypes";
-import { reject } from 'bluebird';
-import { ExifData } from 'exif';
-import { Result } from '../client/northstar/model/idea/idea';
import RouteSubscriber from './RouteSubscriber';
-
-const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest));
-let youtubeApiKey: string;
-YoutubeApi.readApiKey((apiKey: string) => youtubeApiKey = apiKey);
-
-const release = process.env.RELEASE === "true";
-if (process.env.RELEASE === "true") {
- console.log("Running server in release mode");
-} else {
- console.log("Running server in debug mode");
-}
-console.log(process.env.PWD);
-let clientUtils = fs.readFileSync("./src/client/util/ClientUtils.ts.temp", "utf8");
-clientUtils = `//AUTO-GENERATED FILE: DO NOT EDIT\n${clientUtils.replace('"mode"', String(release))}`;
-fs.writeFileSync("./src/client/util/ClientUtils.ts", clientUtils, "utf8");
-
-const mongoUrl = 'mongodb://localhost:27017/Dash';
-mongoose.connection.readyState === 0 && mongoose.connect(mongoUrl);
-mongoose.connection.on('connected', () => console.log("connected"));
-
-// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE
-// ORDER OF IMPORTS MATTERS
-
-app.use(cookieParser());
-app.use(session({
- secret: "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc",
- resave: true,
- cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 },
- saveUninitialized: true,
- store: new MongoStore({ url: 'mongodb://localhost:27017/Dash' })
-}));
-
-app.use(flash());
-app.use(expressFlash());
-app.use(bodyParser.json({ limit: "10mb" }));
-app.use(bodyParser.urlencoded({ extended: true }));
-app.use(expressValidator());
-app.use(passport.initialize());
-app.use(passport.session());
-app.use((req, res, next) => {
- res.locals.user = req.user;
- next();
-});
-
-app.get("/hello", (req, res) => res.send("<p>Hello</p>"));
-
-enum Method {
- GET,
- POST
-}
-
-export type ValidationHandler = (user: DashUserModel, req: express.Request, res: express.Response) => any | Promise<any>;
-export type RejectionHandler = (req: express.Request, res: express.Response) => any | Promise<any>;
-export type ErrorHandler = (req: express.Request, res: express.Response, error: any) => any | Promise<any>;
-
-const LoginRedirect: RejectionHandler = (_req, res) => res.redirect(RouteStore.login);
-
-export interface RouteInitializer {
- method: Method;
- subscribers: string | RouteSubscriber | (string | RouteSubscriber)[];
- onValidation: ValidationHandler;
- onRejection?: RejectionHandler;
- onError?: ErrorHandler;
-}
-
-const isSharedDocAccess = (target: string) => {
- const shared = qs.parse(qs.extract(target), { sort: false }).sharing === "true";
- const docAccess = target.startsWith("/doc/");
- return shared && docAccess;
-};
+import initializeServer from './Initialization';
+import RouteManager, { Method, _success, _permission_denied, _error, _invalid, OnUnauthenticated } from './RouteManager';
+import * as qs from 'query-string';
+import UtilManager from './ApiManagers/UtilManager';
+import SearchManager from './ApiManagers/SearchManager';
+import UserManager from './ApiManagers/UserManager';
+import { WebSocket } from './Websocket/Websocket';
+import DownloadManager from './ApiManagers/DownloadManager';
+import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
+import DeleteManager from "./ApiManagers/DeleteManager";
+import PDFManager from "./ApiManagers/PDFManager";
+import UploadManager from "./ApiManagers/UploadManager";
+import { log_execution } from "./ActionUtilities";
+import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager";
+import GooglePhotosManager from "./ApiManagers/GooglePhotosManager";
+import DiagnosticManager from "./ApiManagers/DiagnosticManager";
+
+export const publicDirectory = path.resolve(__dirname, "public");
+export const filesDirectory = path.resolve(publicDirectory, "files");
/**
- * Please invoke this function when adding a new route to Dash's server.
- * It ensures that any requests leading to or containing user-sensitive information
- * does not execute unless Passport authentication detects a user logged in.
- * @param method whether or not the request is a GET or a POST
- * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response
- * @param onRejection an optional callback invoked on return if no user is found to be logged in
- * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler
+ * These are the functions run before the server starts
+ * listening. Anything that must be complete
+ * before clients can access the server should be run or awaited here.
*/
-function addSecureRoute(initializer: RouteInitializer) {
- const { method, subscribers, onValidation, onRejection, onError } = initializer;
- let abstracted = async (req: express.Request, res: express.Response) => {
- const { user, originalUrl: target } = req;
- if (user || isSharedDocAccess(target)) {
- try {
- await onValidation(user as any, req, res);
- } catch (e) {
- if (onError) {
- onError(req, res, e);
- } else {
- _error(res, `The server encountered an internal error handling ${target}.`, e);
- }
- }
- } else {
- req.session!.target = target;
- try {
- await (onRejection || LoginRedirect)(req, res);
- } catch (e) {
- if (onError) {
- onError(req, res, e);
- } else {
- _error(res, `The server encountered an internal error when rejecting ${target}.`, e);
- }
- }
- }
- };
- const subscribe = (subscriber: RouteSubscriber | string) => {
- let route: string;
- if (typeof subscriber === "string") {
- route = subscriber;
- } else {
- route = subscriber.build;
- }
- switch (method) {
- case Method.GET:
- app.get(route, abstracted);
- break;
- case Method.POST:
- app.post(route, abstracted);
- break;
- }
- };
- if (Array.isArray(subscribers)) {
- subscribers.forEach(subscribe);
- } else {
- subscribe(subscribers);
- }
-}
-
-// STATIC FILE SERVING
-app.use(express.static(__dirname + RouteStore.public));
-app.use(RouteStore.images, express.static(__dirname + RouteStore.public));
-
-app.get("/pull", (req, res) =>
- exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', (err, stdout, stderr) => {
- if (err) {
- res.send(err.message);
- return;
- }
- res.redirect("/");
- }));
-
-app.get("/buxton", (req, 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);
-});
-
-const STATUS = {
- OK: 200,
- BAD_REQUEST: 400,
- EXECUTION_ERROR: 500,
- PERMISSION_DENIED: 403
-};
-
-const command_line = (command: string, fromDirectory?: string) => {
- return new Promise<string>((resolve, reject) => {
- let options: ExecOptions = {};
- if (fromDirectory) {
- options.cwd = path.join(__dirname, fromDirectory);
- }
- exec(command, options, (err, stdout) => err ? reject(err) : resolve(stdout));
- });
-};
-
-const read_text_file = (relativePath: string) => {
- let target = path.join(__dirname, relativePath);
- return new Promise<string>((resolve, reject) => {
- fs.readFile(target, (err, data) => err ? reject(err) : resolve(data.toString()));
+async function preliminaryFunctions() {
+ await GoogleCredentialsLoader.loadCredentials();
+ GoogleApiServerUtils.processProjectCredentials();
+ await DashUploadUtils.buildFileDirectories();
+ await log_execution({
+ startMessage: "attempting to initialize mongodb connection",
+ endMessage: "connection outcome determined",
+ action: Database.tryInitializeConnection
});
-};
-
-const write_text_file = (relativePath: string, contents: any) => {
- let target = path.join(__dirname, relativePath);
- return new Promise<void>((resolve, reject) => {
- fs.writeFile(target, contents, (err) => err ? reject(err) : resolve());
- });
-};
-
-app.get("/version", (req, res) => {
- exec('"C:\\Program Files\\Git\\bin\\git.exe" rev-parse HEAD', (err, stdout, stderr) => {
- if (err) {
- res.send(err.message);
- return;
- }
- res.send(stdout);
- });
-});
-
-// SEARCH
-const solrURL = "http://localhost:8983/solr/#/dash";
-
-// GETTERS
-
-app.get("/textsearch", async (req, res) => {
- let q = req.query.q;
- if (q === undefined) {
- res.send([]);
- return;
- }
- let results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, uploadDirectory + "text", ".txt$");
- let resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] };
- for (var result in results) {
- resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, ""));
- resObj.lines.push(results[result].line);
- resObj.numFound++;
- }
- res.send(resObj);
-});
-
-app.get("/search", async (req, res) => {
- const solrQuery: any = {};
- ["q", "fq", "start", "rows", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]);
- if (solrQuery.q === undefined) {
- res.send([]);
- return;
- }
- let results = await Search.Instance.search(solrQuery);
- res.send(results);
-});
-
-function msToTime(duration: number) {
- let milliseconds = Math.floor((duration % 1000) / 100),
- seconds = Math.floor((duration / 1000) % 60),
- minutes = Math.floor((duration / (1000 * 60)) % 60),
- hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
-
- let hoursS = (hours < 10) ? "0" + hours : hours;
- let minutesS = (minutes < 10) ? "0" + minutes : minutes;
- let secondsS = (seconds < 10) ? "0" + seconds : seconds;
-
- return hoursS + ":" + minutesS + ":" + secondsS + "." + milliseconds;
-}
-
-async function getDocs(id: string) {
- const files = new Set<string>();
- const docs: { [id: string]: any } = {};
- const fn = (doc: any): string[] => {
- const id = doc.id;
- if (typeof id === "string" && id.endsWith("Proto")) {
- //Skip protos
- return [];
- }
- const ids: string[] = [];
- for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) {
- continue;
- }
- const field = doc.fields[key];
- if (field === undefined || field === null) {
- continue;
- }
-
- if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
- ids.push(field.fieldId);
- } else if (field.__type === "script" || field.__type === "computed") {
- if (field.captures) {
- ids.push(field.captures.fieldId);
- }
- } else if (field.__type === "list") {
- ids.push(...fn(field));
- } else if (typeof field === "string") {
- const re = /"(?:dataD|d)ocumentId"\s*:\s*"([\w\-]*)"/g;
- let match: string[] | null;
- while ((match = re.exec(field)) !== null) {
- ids.push(match[1]);
- }
- } else if (field.__type === "RichTextField") {
- const re = /"href"\s*:\s*"(.*?)"/g;
- let match: string[] | null;
- while ((match = re.exec(field.Data)) !== null) {
- const urlString = match[1];
- const split = new URL(urlString).pathname.split("doc/");
- if (split.length > 1) {
- ids.push(split[split.length - 1]);
- }
- }
- const re2 = /"src"\s*:\s*"(.*?)"/g;
- while ((match = re2.exec(field.Data)) !== null) {
- const urlString = match[1];
- const pathname = new URL(urlString).pathname;
- files.add(pathname);
- }
- } else if (["audio", "image", "video", "pdf", "web"].includes(field.__type)) {
- const url = new URL(field.url);
- const pathname = url.pathname;
- files.add(pathname);
- }
- }
-
- if (doc.id) {
- docs[doc.id] = doc;
- }
- return ids;
- };
- await Database.Instance.visit([id], fn);
- return { id, docs, files };
-}
-app.get("/serializeDoc/:docId", async (req, res) => {
- const { docs, files } = await getDocs(req.params.docId);
- res.send({ docs, files: Array.from(files) });
-});
-
-export type Hierarchy = { [id: string]: string | Hierarchy };
-export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
-
-addSecureRoute({
- method: Method.GET,
- subscribers: new RouteSubscriber(RouteStore.imageHierarchyExport).add('docId'),
- onValidation: async (_user, req, res) => {
- const id = req.params.docId;
- const hierarchy: Hierarchy = {};
- await targetedVisitorRecursive(id, hierarchy);
- BuildAndDispatchZip(res, async zip => {
- await hierarchyTraverserRecursive(zip, hierarchy);
- });
- }
-});
-
-const BuildAndDispatchZip = async (res: Response, mutator: ZipMutator): Promise<void> => {
- const zip = Archiver('zip');
- zip.pipe(res);
- await mutator(zip);
- return zip.finalize();
-};
-
-const targetedVisitorRecursive = async (seedId: string, hierarchy: Hierarchy): Promise<void> => {
- const local: Hierarchy = {};
- const { title, data } = await getData(seedId);
- const label = `${title} (${seedId})`;
- if (Array.isArray(data)) {
- hierarchy[label] = local;
- await Promise.all(data.map(proxy => targetedVisitorRecursive(proxy.fieldId, local)));
- } else {
- hierarchy[label + path.extname(data)] = data;
- }
-};
-
-const getData = async (seedId: string): Promise<{ data: string | any[], title: string }> => {
- return new Promise<{ data: string | any[], title: string }>((resolve, reject) => {
- Database.Instance.getDocument(seedId, async (result: any) => {
- const { data, proto, title } = result.fields;
- if (data) {
- if (data.url) {
- resolve({ data: data.url, title });
- } else if (data.fields) {
- resolve({ data: data.fields, title });
- } else {
- reject();
- }
- }
- if (proto) {
- getData(proto.fieldId).then(resolve, reject);
- }
- });
- });
-};
-
-const hierarchyTraverserRecursive = async (file: Archiver.Archiver, hierarchy: Hierarchy, prefix = "Dash Export"): Promise<void> => {
- for (const key of Object.keys(hierarchy)) {
- const result = hierarchy[key];
- if (typeof result === "string") {
- let path: string;
- let matches: RegExpExecArray | null;
- if ((matches = /\:1050\/files\/(upload\_[\da-z]{32}.*)/g.exec(result)) !== null) {
- path = `${__dirname}/public/files/${matches[1]}`;
- } else {
- const information = await DashUploadUtils.UploadImage(result);
- path = information.mediaPaths[0];
- }
- file.file(path, { name: key, prefix });
- } else {
- await hierarchyTraverserRecursive(file, result, `${prefix}/${key}`);
- }
- }
-};
-
-app.get("/downloadId/:docId", async (req, res) => {
- res.set('Content-disposition', `attachment;`);
- res.set('Content-Type', "application/zip");
- const { id, docs, files } = await getDocs(req.params.docId);
- const docString = JSON.stringify({ id, docs });
- const zip = Archiver('zip');
- zip.pipe(res);
- zip.append(docString, { name: "doc.json" });
- files.forEach(val => {
- zip.file(__dirname + RouteStore.public + val, { name: val.substring(1) });
- });
- zip.finalize();
-});
-
-app.post("/uploadDoc", (req, res) => {
- let form = new formidable.IncomingForm();
- form.keepExtensions = true;
- // let path = req.body.path;
- const ids: { [id: string]: string } = {};
- let remap = true;
- const getId = (id: string): string => {
- if (!remap) return id;
- if (id.endsWith("Proto")) return id;
- if (id in ids) {
- return ids[id];
- } else {
- return ids[id] = v4();
- }
- };
- const mapFn = (doc: any) => {
- if (doc.id) {
- doc.id = getId(doc.id);
- }
- for (const key in doc.fields) {
- if (!doc.fields.hasOwnProperty(key)) {
- continue;
- }
- const field = doc.fields[key];
- if (field === undefined || field === null) {
- continue;
- }
-
- if (field.__type === "proxy" || field.__type === "prefetch_proxy") {
- field.fieldId = getId(field.fieldId);
- } else if (field.__type === "script" || field.__type === "computed") {
- if (field.captures) {
- field.captures.fieldId = getId(field.captures.fieldId);
- }
- } else if (field.__type === "list") {
- mapFn(field);
- } else if (typeof field === "string") {
- const re = /("(?:dataD|d)ocumentId"\s*:\s*")([\w\-]*)"/g;
- doc.fields[key] = (field as any).replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- } else if (field.__type === "RichTextField") {
- const re = /("href"\s*:\s*")(.*?)"/g;
- field.Data = field.Data.replace(re, (match: any, p1: string, p2: string) => {
- return `${p1}${getId(p2)}"`;
- });
- }
- }
- };
- 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 {
- 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); }
- });
-});
-
-app.get("/whosOnline", (req, res) => {
- let users: any = { active: {}, inactive: {} };
- const now = Date.now();
-
- for (const user in timeMap) {
- const time = timeMap[user];
- const key = ((now - time) / 1000) < (60 * 5) ? "active" : "inactive";
- users[key][user] = `Last active ${msToTime(now - time)} ago`;
- }
-
- res.send(users);
-});
-app.get("/thumbnail/:filename", (req, res) => {
- 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);
- }
- });
-});
-
-function LoadPage(file: string, pageNumber: number, res: Response) {
- console.log(file);
- Pdfjs.getDocument(file).promise
- .then((pdf: Pdfjs.PDFDocumentProxy) => {
- let factory = new NodeCanvasFactory();
- console.log(pageNumber);
- pdf.getPage(pageNumber).then((page: Pdfjs.PDFPageProxy) => {
- console.log("reading " + page);
- let viewport = page.getViewport(1 as any);
- let canvasAndContext = factory.create(viewport.width, viewport.height);
- let renderContext = {
- canvasContext: canvasAndContext.context,
- viewport: viewport,
- canvasFactory: factory
- };
- console.log("read " + pageNumber);
-
- page.render(renderContext).promise
- .then(() => {
- console.log("saving " + pageNumber);
- let stream = canvasAndContext.canvas.createPNGStream();
- let pngFile = `${file.substring(0, file.length - ".pdf".length)}-${pageNumber}.PNG`;
- let out = fs.createWriteStream(pngFile);
- stream.pipe(out);
- out.on("finish", () => {
- console.log(`Success! Saved to ${pngFile}`);
- let name = path.basename(pngFile);
- res.send({ path: "/files/" + name, width: viewport.width, height: viewport.height });
- });
- }, (reason: string) => {
- console.error(reason + ` ${pageNumber}`);
- });
- });
- });
}
/**
- * Anyone attempting to navigate to localhost at this port will
- * first have to log in.
+ * Either clustered together as an API manager
+ * or individually referenced below, by the completion
+ * of this function's execution, all routes will
+ * be registered on the server
+ * @param router the instance of the route manager
+ * that will manage the registration of new routes
+ * with the server
*/
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.root,
- onValidation: (_user, _req, res) => res.redirect(RouteStore.home)
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.getUsers,
- onValidation: async (_user, _req, res) => {
- const cursor = await Database.Instance.query({}, { email: 1, userDocumentId: 1 }, "users");
- const results = await cursor.toArray();
- res.send(results.map(user => ({ email: user.email, userDocumentId: user.userDocumentId })));
- },
-});
+function routeSetter({ isRelease, addSupervisedRoute }: RouteManager) {
+ const managers = [
+ new UserManager(),
+ new UploadManager(),
+ new DownloadManager(),
+ new DiagnosticManager(),
+ new SearchManager(),
+ new PDFManager(),
+ new DeleteManager(),
+ new UtilManager(),
+ new GeneralGoogleManager(),
+ new GooglePhotosManager(),
+ ];
+
+ // initialize API Managers
+ managers.forEach(manager => manager.register(addSupervisedRoute));
+
+ // initialize the web socket (bidirectional communication: if a user changes
+ // a field on one client, that change must be broadcast to all other clients)
+ WebSocket.initialize(serverPort, isRelease);
+
+ /**
+ * Accessing root index redirects to home
+ */
+ addSupervisedRoute({
+ method: Method.GET,
+ subscription: "/",
+ onValidation: ({ res }) => res.redirect("/home")
+ });
-addSecureRoute({
- method: Method.GET,
- subscribers: [RouteStore.home, RouteStore.openDocumentWithId],
- onValidation: (_user, req, res) => {
+ const serve: OnUnauthenticated = ({ req, res }) => {
let detector = new mobileDetect(req.headers['user-agent'] || "");
let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html';
res.sendFile(path.join(__dirname, '../../deploy/' + filename));
- },
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.getUserDocumentId,
- onValidation: (user, _req, res) => res.send(user.userDocumentId),
- onRejection: (_req, res) => res.send(undefined)
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.getCurrUser,
- onValidation: (user, _req, res) => { res.send(JSON.stringify(user)); },
- onRejection: (_req, res) => res.send(JSON.stringify({ id: "__guest__", email: "" }))
-});
-
-const ServicesApiKeyMap = new Map<string, string | undefined>([
- ["face", process.env.FACE],
- ["vision", process.env.VISION],
- ["handwriting", process.env.HANDWRITING]
-]);
-
-addSecureRoute({
- method: Method.GET,
- subscribers: new RouteSubscriber(RouteStore.cognitiveServices).add('requestedservice'),
- onValidation: (_user, req, res) => {
- let service = req.params.requestedservice;
- res.send(ServicesApiKeyMap.get(service));
- }
-});
-
-class NodeCanvasFactory {
- create = (width: number, height: number) => {
- var canvas = createCanvas(width, height);
- var context = canvas.getContext('2d');
- return {
- canvas: canvas,
- context: context,
- };
- }
-
- reset = (canvasAndContext: any, width: number, height: number) => {
- canvasAndContext.canvas.width = width;
- canvasAndContext.canvas.height = height;
- }
-
- destroy = (canvasAndContext: any) => {
- canvasAndContext.canvas.width = 0;
- canvasAndContext.canvas.height = 0;
- canvasAndContext.canvas = null;
- canvasAndContext.context = null;
- }
-}
-
-const pngTypes = [".png", ".PNG"];
-const jpgTypes = [".jpg", ".JPG", ".jpeg", ".JPEG"];
-const uploadDirectory = __dirname + "/public/files/";
-const pdfDirectory = uploadDirectory + "text";
-DashUploadUtils.createIfNotExists(pdfDirectory);
-
-interface ImageFileResponse {
- name: string;
- path: string;
- type: string;
- exif: Opt<DashUploadUtils.EnrichedExifData>;
-}
-
-addSecureRoute({
- method: Method.POST,
- subscribers: RouteStore.upload,
- onValidation: (_user, 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<DashUploadUtils.UploadInformation>;
- if (filename.endsWith(".pdf")) {
- let dataBuffer = fs.readFileSync(uploadDirectory + filename);
- const result: ParsedPDF = await pdf(dataBuffer);
- await new Promise<void>(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 if (type.indexOf("audio") !== -1) {
- // nothing to be done yet-- although transcribing the audio a la pdfs would make sense.
- } else {
- uploadInformation = await DashUploadUtils.UploadImage(uploadDirectory + filename, filename);
- }
- const exif = uploadInformation ? uploadInformation.exifData : undefined;
- results.push({ name, type, path: `/files/${filename}`, exif });
-
- }
- _success(res, results);
- });
- }
-});
-
-addSecureRoute({
- method: Method.POST,
- subscribers: RouteStore.inspectImage,
- onValidation: async (_user, req, res) => {
- const { source } = req.body;
- if (typeof source === "string") {
- const uploadInformation = await DashUploadUtils.UploadImage(source);
- return res.send(await DashUploadUtils.InspectImage(uploadInformation.mediaPaths[0]));
- }
- res.send({});
- }
-});
-
-addSecureRoute({
- method: Method.POST,
- subscribers: RouteStore.dataUriToImage,
- onValidation: (_user, req, res) => {
- const uri = req.body.uri;
- const filename = req.body.name;
- if (!uri || !filename) {
- res.status(401).send("incorrect parameters specified");
- 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" },
- { resizer: sharp().resize(400, undefined, { withoutEnlargement: true }), suffix: "_m" },
- { resizer: sharp().resize(900, undefined, { withoutEnlargement: true }), suffix: "_l" },
- ];
- let isImage = false;
- if (pngTypes.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.png();
- });
- isImage = true;
- } else if (jpgTypes.includes(ext)) {
- resizers.forEach(element => {
- element.resizer = element.resizer.jpeg();
- });
- isImage = true;
- }
- if (isImage) {
- resizers.forEach(resizer => {
- fs.createReadStream(savedName).pipe(resizer.resizer).pipe(fs.createWriteStream(uploadDirectory + filename + resizer.suffix + ext));
- });
- }
- res.send("/files/" + filename + ext);
- });
- }
-});
-
-// AUTHENTICATION
-
-// Sign Up
-app.get(RouteStore.signup, getSignup);
-app.post(RouteStore.signup, postSignup);
-
-// Log In
-app.get(RouteStore.login, getLogin);
-app.post(RouteStore.login, postLogin);
-
-// Log Out
-app.get(RouteStore.logout, getLogout);
-
-// FORGOT PASSWORD EMAIL HANDLING
-app.get(RouteStore.forgot, getForgot);
-app.post(RouteStore.forgot, postForgot);
-
-// RESET PASSWORD EMAIL HANDLING
-app.get(RouteStore.reset, getReset);
-app.post(RouteStore.reset, postReset);
-
-const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
-app.use(RouteStore.corsProxy, (req, res) => {
- req.pipe(request(decodeURIComponent(req.url.substring(1)))).on("response", res => {
- const headers = Object.keys(res.headers);
- headers.forEach(headerName => {
- const header = res.headers[headerName];
- if (Array.isArray(header)) {
- res.headers[headerName] = header.filter(h => !headerCharRegex.test(h));
- } else if (header) {
- if (headerCharRegex.test(header as any)) {
- delete res.headers[headerName];
- }
- }
- });
- }).pipe(res);
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.delete,
- onValidation: (_user, _req, res) => {
- if (release) {
- return _permission_denied(res, deletionPermissionError);
- }
- deleteFields().then(() => res.redirect(RouteStore.home));
- }
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.deleteAll,
- onValidation: (_user, _req, res) => {
- if (release) {
- return _permission_denied(res, deletionPermissionError);
- }
- deleteAll().then(() => res.redirect(RouteStore.home));
- }
-});
-
-app.use(wdm(compiler, { publicPath: config.output.publicPath }));
-
-app.use(whm(compiler));
-
-// start the Express server
-app.listen(port, () =>
- console.log(`server started at http://localhost:${port}`));
-
-const server = io();
-interface Map {
- [key: string]: Client;
-}
-let clients: Map = {};
-
-let socketMap = new Map<SocketIO.Socket, string>();
-let timeMap: { [id: string]: number } = {};
-
-server.on("connection", function (socket: Socket) {
- socket.use((packet, next) => {
- let id = socketMap.get(socket);
- if (id) {
- timeMap[id] = Date.now();
- }
- next();
- });
-
- Utils.Emit(socket, MessageStore.Foo, "handshooken");
-
- Utils.AddServerHandler(socket, MessageStore.Bar, guid => barReceived(socket, guid));
- Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args));
- Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField);
- Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields);
- if (!release) {
- Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields);
- }
-
- Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField);
- Utils.AddServerHandlerCallback(socket, MessageStore.YoutubeApiQuery, HandleYoutubeQuery);
- Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff));
- Utils.AddServerHandler(socket, MessageStore.DeleteField, id => DeleteField(socket, id));
- Utils.AddServerHandler(socket, MessageStore.DeleteFields, ids => DeleteFields(socket, ids));
- Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
- Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
-});
-
-async function deleteFields() {
- await Database.Instance.deleteAll();
- await Search.Instance.clear();
- await Database.Instance.deleteAll('newDocuments');
-}
-
-async function deleteAll() {
- await Database.Instance.deleteAll();
- await Database.Instance.deleteAll('newDocuments');
- await Database.Instance.deleteAll('sessions');
- await Database.Instance.deleteAll('users');
- await Search.Instance.clear();
-}
-
-function barReceived(socket: SocketIO.Socket, guid: string) {
- clients[guid] = new Client(guid.toString());
- console.log(`User ${guid} has connected`);
- socketMap.set(socket, guid);
-}
-
-function getField([id, callback]: [string, (result?: Transferable) => void]) {
- Database.Instance.getDocument(id, (result?: Transferable) =>
- callback(result ? result : undefined));
-}
-
-function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) {
- Database.Instance.getDocuments(ids, callback);
-}
-
-function setField(socket: Socket, newValue: Transferable) {
- Database.Instance.update(newValue.id, newValue, () =>
- socket.broadcast.emit(MessageStore.SetField.Message, newValue));
- if (newValue.type === Types.Text) {
- Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data });
- console.log("set field");
- console.log("checking in");
- }
-}
-
-function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
- Database.Instance.getDocument(id, callback, "newDocuments");
-}
-
-function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
- Database.Instance.getDocuments(ids, callback, "newDocuments");
-}
-
-function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) {
- switch (query.type) {
- case YoutubeQueryType.Channels:
- YoutubeApi.authorizedGetChannel(youtubeApiKey);
- break;
- case YoutubeQueryType.SearchVideo:
- YoutubeApi.authorizedGetVideos(youtubeApiKey, query.userInput, callback);
- case YoutubeQueryType.VideoDetails:
- YoutubeApi.authorizedGetVideoDetails(youtubeApiKey, query.videoIds, callback);
- }
-}
-
-const credentialsPath = path.join(__dirname, "./credentials/google_docs_credentials.json");
-
-const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([
- ["create", (api, params) => api.create(params)],
- ["retrieve", (api, params) => api.get(params)],
- ["update", (api, params) => api.batchUpdate(params)],
-]);
-
-app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
- 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 => {
- let handler = EndpointHandlerMap.get(action);
- if (endpoint && handler) {
- let execute = handler(endpoint, req.body).then(
- response => res.send(response.data),
- rejection => res.send(rejection)
- );
- execute.catch(exception => res.send(exception));
- return;
- }
- res.send(undefined);
- });
-});
-
-addSecureRoute({
- method: Method.GET,
- subscribers: RouteStore.readGoogleAccessToken,
- onValidation: async (user, _req, 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));
- }
- GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token));
- }
-});
-
-addSecureRoute({
- method: Method.POST,
- subscribers: RouteStore.writeGoogleAccessToken,
- onValidation: async (user, req, res) => {
- const userId = user.id;
- const information = { credentialsPath, userId };
- res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, 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!";
-
-export interface NewMediaItem {
- description: string;
- simpleMediaItem: {
- uploadToken: string;
};
-}
-
-addSecureRoute({
- method: Method.POST,
- subscribers: 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 batched = BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 });
- const newMediaItems = await batched.batchedMapPatientInterval<NewMediaItem>(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch, collector) => {
- for (let index = 0; index < batch.length; index++) {
- const { url, description } = batch[index];
- const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(url);
- if (!uploadToken) {
- failed.push(index);
- } else {
- collector.push({
- description,
- simpleMediaItem: { uploadToken }
- });
- }
- }
+ addSupervisedRoute({
+ method: Method.GET,
+ subscription: ["/home", new RouteSubscriber("doc").add("docId")],
+ onValidation: serve,
+ onUnauthenticated: ({ req, ...remaining }) => {
+ const { originalUrl: target } = req;
+ const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true";
+ const docAccess = target.startsWith("/doc/");
+ if (sharing && docAccess) {
+ serve({ req, ...remaining });
}
- );
-
- const failedCount = failed.length;
- if (failedCount) {
- console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
- }
-
- GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
- results => _success(res, { results, failed }),
- error => _error(res, mediaError, error)
- );
- }
-});
-
-interface MediaItem {
- baseUrl: string;
- filename: string;
-}
-const prefix = "google_photos_";
-
-const downloadError = "Encountered an error while executing downloads.";
-const requestError = "Unable to execute download: the body's media items were malformed.";
-const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!";
-
-app.get("/deleteWithAux", async (_req, res) => {
- if (release) {
- return _permission_denied(res, deletionPermissionError);
- }
- await Database.Auxiliary.DeleteAll();
- res.redirect(RouteStore.delete);
-});
-
-app.get("/deleteWithGoogleCredentials", async (req, res) => {
- if (release) {
- return _permission_denied(res, deletionPermissionError);
- }
- await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll();
- res.redirect(RouteStore.delete);
-});
-
-const UploadError = (count: number) => `Unable to upload ${count} images to Dash's server`;
-app.post(RouteStore.googlePhotosMediaDownload, async (req, res) => {
- const contents: { mediaItems: MediaItem[] } = req.body;
- let failed = 0;
- if (contents) {
- const completed: Opt<DashUploadUtils.UploadInformation>[] = [];
- for (let item of contents.mediaItems) {
- const { contentSize, ...attributes } = await DashUploadUtils.InspectImage(item.baseUrl);
- const found: Opt<DashUploadUtils.UploadInformation> = await Database.Auxiliary.QueryUploadHistory(contentSize!);
- if (!found) {
- const upload = await DashUploadUtils.UploadInspectedImage({ contentSize, ...attributes }, item.filename, prefix).catch(error => _error(res, downloadError, error));
- if (upload) {
- completed.push(upload);
- await Database.Auxiliary.LogUpload(upload);
- } else {
- failed++;
- }
- } else {
- completed.push(found);
- }
- }
- if (failed) {
- return _error(res, UploadError(failed));
}
- return _success(res, completed);
- }
- _invalid(res, requestError);
-});
-
-const _error = (res: Response, message: string, error?: any) => {
- res.statusMessage = message;
- res.status(STATUS.EXECUTION_ERROR).send(error);
-};
-
-const _success = (res: Response, body: any) => {
- res.status(STATUS.OK).send(body);
-};
-
-const _invalid = (res: Response, message: string) => {
- res.statusMessage = message;
- res.status(STATUS.BAD_REQUEST).send();
-};
-
-const _permission_denied = (res: Response, message: string) => {
- res.statusMessage = message;
- res.status(STATUS.BAD_REQUEST).send("Permission Denied!");
-};
-
-const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = {
- "number": "_n",
- "string": "_t",
- "boolean": "_b",
- "image": ["_t", "url"],
- "video": ["_t", "url"],
- "pdf": ["_t", "url"],
- "audio": ["_t", "url"],
- "web": ["_t", "url"],
- "RichTextField": ["_t", value => value.Text],
- "date": ["_d", value => new Date(value.date).toISOString()],
- "proxy": ["_i", "fieldId"],
- "list": ["_l", list => {
- const results = [];
- for (const value of list.fields) {
- const term = ToSearchTerm(value);
- if (term) {
- results.push(term.value);
- }
- }
- return results.length ? results : null;
- }]
-};
-
-function ToSearchTerm(val: any): { suffix: string, value: any } | undefined {
- if (val === null || val === undefined) {
- return;
- }
- const type = val.__type || typeof val;
- let suffix = suffixMap[type];
- if (!suffix) {
- return;
- }
-
- if (Array.isArray(suffix)) {
- const accessor = suffix[1];
- if (typeof accessor === "function") {
- val = accessor(val);
- } else {
- val = val[accessor];
- }
- suffix = suffix[0];
- }
-
- return { suffix, value: val };
-}
-
-function getSuffix(value: string | [string, any]): string {
- return typeof value === "string" ? value : value[0];
-}
-
-function UpdateField(socket: Socket, diff: Diff) {
- Database.Instance.update(diff.id, diff.diff,
- () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments");
- const docfield = diff.diff.$set;
- if (!docfield) {
- return;
- }
- const update: any = { id: diff.id };
- let dynfield = false;
- for (let key in docfield) {
- if (!key.startsWith("fields.")) continue;
- dynfield = true;
- let val = docfield[key];
- key = key.substring(7);
- Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
- let term = ToSearchTerm(val);
- if (term !== undefined) {
- let { suffix, value } = term;
- update[key + suffix] = { set: value };
- }
- }
- if (dynfield) {
- Search.Instance.updateDocument(update);
- }
-}
-
-function DeleteField(socket: Socket, id: string) {
- Database.Instance.delete({ _id: id }, "newDocuments").then(() => {
- socket.broadcast.emit(MessageStore.DeleteField.Message, id);
});
-
- Search.Instance.deleteDocuments([id]);
}
-function DeleteFields(socket: Socket, ids: string[]) {
- Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => {
- socket.broadcast.emit(MessageStore.DeleteFields.Message, ids);
+(async function start() {
+ await log_execution({
+ startMessage: "starting execution of preliminary functions",
+ endMessage: "completed preliminary functions",
+ action: preliminaryFunctions
});
-
- Search.Instance.deleteDocuments(ids);
-
-}
-
-function CreateField(newValue: any) {
- Database.Instance.insert(newValue, "newDocuments");
-}
-
-server.listen(serverPort);
-console.log(`listening on port ${serverPort}`);
-
+ await initializeServer({ listenAtPort: 1050, routeSetter });
+})();
diff --git a/src/server/public/files/.gitignore b/src/server/public/files/.gitignore
deleted file mode 100644
index c96a04f00..000000000
--- a/src/server/public/files/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-*
-!.gitignore \ No newline at end of file
diff --git a/views/login.pug b/views/login.pug
index 9bc40a495..26da5e29e 100644
--- a/views/login.pug
+++ b/views/login.pug
@@ -14,11 +14,9 @@ block content
.inner.login
h3.auth_header Log In
.form-group
- //- label.col-sm-3.control-label(for='email', id='email_label') Email
.col-sm-7
input.form-control(type='email', name='email', id='email', placeholder='Email', autofocus, required)
.form-group
- //- label.col-sm-3.control-label(for='password') Password
.col-sm-7
input.form-control(type='password', name='password', id='password', placeholder='Password', required)
.form-group
diff --git a/views/stylesheets/authentication.css b/views/stylesheets/authentication.css
index 36bb880af..ff1f4aace 100644
--- a/views/stylesheets/authentication.css
+++ b/views/stylesheets/authentication.css
@@ -139,4 +139,85 @@ body {
padding-right: 10px;
font-family: Arial, Helvetica, sans-serif;
font-size: 16px;
+}
+
+.outermost, .online-container {
+ display: flex;
+ flex-direction: row;
+ height: 98vh;
+ justify-content: center;
+}
+
+.online-container {
+ background: white;
+ display: flex;
+ flex-direction: row;
+ height: 80%;
+ width: 80%;
+ align-self: center;
+ justify-content: center;
+ border-radius: 8px;
+ box-shadow: 10px 10px 10px #00000099;
+}
+
+.partition {
+ width: 50%;
+ display: flex;
+ flex-direction: column;
+ border: 1px solid black;
+}
+
+.inner-activity {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ height: 100%;
+ border-top: 2px solid black;
+ background: white;
+ padding: 20px;
+ overflow: scroll;
+}
+
+ol {
+ align-self: center;
+}
+
+li {
+ font-family: Arial, Helvetica, sans-serif;
+ border: 1px solid black;
+ padding: 10px;
+ border-radius: 5px;
+ margin-bottom: 5px;
+}
+
+.duration {
+ font-style: italic;
+}
+
+span.user-type {
+ align-self: center;
+ font-family: Arial, Helvetica, sans-serif;
+ font-weight: bold;
+ font-size: 20px;
+ margin: 50px;
+}
+
+#active-partition {
+ background: green;
+ border-top-left-radius: 8px;
+ border-bottom-left-radius: 8px;
+}
+
+#active-inner {
+ border-bottom-left-radius: 8px;
+}
+
+#inactive-partition {
+ background: red;
+ border-top-right-radius: 8px;
+ border-bottom-right-radius: 8px;
+}
+
+#inactive-inner {
+ border-bottom-right-radius: 8px;
} \ No newline at end of file
diff --git a/views/user_activity.pug b/views/user_activity.pug
new file mode 100644
index 000000000..68e42140d
--- /dev/null
+++ b/views/user_activity.pug
@@ -0,0 +1,19 @@
+extends ./layout
+
+block content
+ style
+ include ./stylesheets/authentication.css
+ .outermost
+ .online-container
+ .partition(id="active-partition")
+ span.user-type Active Users
+ .inner-activity(id="active-inner")
+ ol
+ each val in active
+ li= val
+ .partition(id="inactive-partition")
+ span.user-type Inactive Users
+ .inner-activity(id="inactive-inner")
+ ol
+ each val in inactive
+ li= val \ No newline at end of file