aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/server/RouteSubscriber.ts26
-rw-r--r--src/server/index.ts340
2 files changed, 228 insertions, 138 deletions
diff --git a/src/server/RouteSubscriber.ts b/src/server/RouteSubscriber.ts
new file mode 100644
index 000000000..e49be8af5
--- /dev/null
+++ b/src/server/RouteSubscriber.ts
@@ -0,0 +1,26 @@
+export default class RouteSubscriber {
+ private _root: string;
+ private requestParameters: string[] = [];
+
+ constructor(root: string) {
+ this._root = root;
+ }
+
+ add(...parameters: string[]) {
+ this.requestParameters.push(...parameters);
+ return this;
+ }
+
+ public get root() {
+ return this._root;
+ }
+
+ public get build() {
+ let output = this._root;
+ if (this.requestParameters.length) {
+ output = `${output}/:${this.requestParameters.join("/:")}`;
+ }
+ return output;
+ }
+
+} \ No newline at end of file
diff --git a/src/server/index.ts b/src/server/index.ts
index 010a851bc..2203ae2e1 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -55,6 +55,7 @@ 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;
@@ -106,6 +107,26 @@ enum Method {
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;
+};
+
/**
* 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
@@ -115,22 +136,40 @@ enum Method {
* @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
*/
-function addSecureRoute(method: Method,
- handler: (user: DashUserModel, res: express.Response, req: express.Request) => void,
- onRejection: (res: express.Response, req: express.Request) => any = res => res.redirect(RouteStore.login),
- ...subscribers: string[]
-) {
- let abstracted = (req: express.Request, res: express.Response) => {
- let sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true";
- sharing = sharing && req.originalUrl.startsWith("/doc/");
- if (req.user || sharing) {
- handler(req.user as any, res, req);
+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, 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 = req.originalUrl;
- onRejection(res, req);
+ 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);
+ }
+ }
}
};
- subscribers.forEach(route => {
+ 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);
@@ -139,7 +178,12 @@ function addSecureRoute(method: Method,
app.post(route, abstracted);
break;
}
- });
+ };
+ if (Array.isArray(subscribers)) {
+ subscribers.forEach(subscribe);
+ } else {
+ subscribe(subscribers);
+ }
}
// STATIC FILE SERVING
@@ -323,13 +367,17 @@ app.get("/serializeDoc/:docId", async (req, res) => {
export type Hierarchy = { [id: string]: string | Hierarchy };
export type ZipMutator = (file: Archiver.Archiver) => void | Promise<void>;
-app.get(`${RouteStore.imageHierarchyExport}/:docId`, async (req, res) => {
- const id = req.params.docId;
- const hierarchy: Hierarchy = {};
- await targetedVisitorRecursive(id, hierarchy);
- BuildAndDispatchZip(res, async zip => {
- await hierarchyTraverserRecursive(zip, hierarchy);
- });
+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> => {
@@ -576,50 +624,49 @@ function LoadPage(file: string, pageNumber: number, res: Response) {
});
}
-// anyone attempting to navigate to localhost at this port will
-// first have to login
-addSecureRoute(
- Method.GET,
- (user, res) => res.redirect(RouteStore.home),
- undefined,
- RouteStore.root
-);
-
-addSecureRoute(
- Method.GET,
- async (_, res) => {
+/**
+ * Anyone attempting to navigate to localhost at this port will
+ * first have to log in.
+ */
+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 })));
},
- undefined,
- RouteStore.getUsers
-);
+});
-addSecureRoute(
- Method.GET,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: [RouteStore.home, RouteStore.openDocumentWithId],
+ onValidation: (_user, 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));
},
- undefined,
- RouteStore.home, RouteStore.openDocumentWithId
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => res.send(user.userDocumentId),
- (res) => res.send(undefined),
- RouteStore.getUserDocumentId,
-);
-
-addSecureRoute(
- Method.GET,
- (user, res) => { res.send(JSON.stringify({ id: user.id, email: user.email })); },
- (res) => res.send(JSON.stringify({ id: "__guest__", email: "" })),
- RouteStore.getCurrUser
-);
+});
+
+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],
@@ -627,10 +674,14 @@ const ServicesApiKeyMap = new Map<string, string | undefined>([
["handwriting", process.env.HANDWRITING]
]);
-addSecureRoute(Method.GET, (user, res, req) => {
- let service = req.params.requestedservice;
- res.send(ServicesApiKeyMap.get(service));
-}, undefined, `${RouteStore.cognitiveServices}/:requestedservice`);
+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) => {
@@ -668,10 +719,10 @@ interface ImageFileResponse {
exif: Opt<DashUploadUtils.EnrichedExifData>;
}
-// SETTERS
-app.post(
- RouteStore.upload,
- (req, res) => {
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.upload,
+ onValidation: (_user, req, res) => {
let form = new formidable.IncomingForm();
form.uploadDir = uploadDirectory;
form.keepExtensions = true;
@@ -704,20 +755,25 @@ app.post(
_success(res, results);
});
}
-);
+});
-app.post(RouteStore.inspectImage, async (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]));
+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({});
}
- res.send({});
});
-addSecureRoute(
- Method.POST,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.POST,
+ subscribers: RouteStore.dataUriToImage,
+ onValidation: (_user, req, res) => {
const uri = req.body.uri;
const filename = req.body.name;
if (!uri || !filename) {
@@ -750,10 +806,9 @@ addSecureRoute(
}
res.send("/files/" + filename + ext);
});
- },
- undefined,
- RouteStore.dataUriToImage
-);
+ }
+});
+
// AUTHENTICATION
// Sign Up
@@ -792,29 +847,27 @@ app.use(RouteStore.corsProxy, (req, res) => {
}).pipe(res);
});
-addSecureRoute(
- Method.GET,
- (user, res, req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.delete,
+ onValidation: (_user, _req, res) => {
if (release) {
return _permission_denied(res, deletionPermissionError);
}
deleteFields().then(() => res.redirect(RouteStore.home));
- },
- undefined,
- RouteStore.delete
-);
+ }
+});
-addSecureRoute(
- Method.GET,
- (_user, res, _req) => {
+addSecureRoute({
+ method: Method.GET,
+ subscribers: RouteStore.deleteAll,
+ onValidation: (_user, _req, res) => {
if (release) {
return _permission_denied(res, deletionPermissionError);
}
deleteAll().then(() => res.redirect(RouteStore.home));
- },
- undefined,
- RouteStore.deleteAll
-);
+ }
+});
app.use(wdm(compiler, { publicPath: config.output.publicPath }));
@@ -945,20 +998,28 @@ app.post(RouteStore.googleDocs + "/:sector/:action", (req, res) => {
});
});
-app.get(RouteStore.readGoogleAccessToken, async (req, res) => {
- const userId = req.header("userId")!;
- const token = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
- const information = { credentialsPath, userId };
- if (!token) {
- return res.send(await GoogleApiServerUtils.GenerateAuthenticationUrl(information));
+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));
}
- GoogleApiServerUtils.RetrieveAccessToken(information).then(token => res.send(token));
});
-app.post(RouteStore.writeGoogleAccessToken, async (req, res) => {
- const userId = req.header("userId")!;
- const information = { credentialsPath, userId };
- res.send(await GoogleApiServerUtils.ProcessClientSideCode(information, req.body.authenticationCode));
+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!";
@@ -972,47 +1033,50 @@ export interface NewMediaItem {
};
}
-app.post(RouteStore.googlePhotosMediaUpload, async (req, res) => {
- const { media } = req.body;
- const userId = req.header("userId");
-
- if (!userId) {
- return _error(res, userIdError);
- }
-
- await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
-
- let failed: number[] = [];
+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);
+ }
- const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
- { magnitude: 100, unit: TimeUnit.Milliseconds },
- async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
- const newMediaItems: NewMediaItem[] = [];
- for (let index = 0; index < batch.length; index++) {
- const element = batch[index];
- const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
- if (!uploadToken) {
- failed.push(index);
- } else {
- newMediaItems.push({
- description: element.description,
- simpleMediaItem: { uploadToken }
- });
+ await GooglePhotosUploadUtils.initialize({ credentialsPath, userId });
+
+ let failed: number[] = [];
+
+ const newMediaItems = await BatchedArray.from<GooglePhotosUploadUtils.MediaInput>(media, { batchSize: 25 }).batchedMapPatientInterval(
+ { magnitude: 100, unit: TimeUnit.Milliseconds },
+ async (batch: GooglePhotosUploadUtils.MediaInput[]) => {
+ const newMediaItems: NewMediaItem[] = [];
+ for (let index = 0; index < batch.length; index++) {
+ const element = batch[index];
+ const uploadToken = await GooglePhotosUploadUtils.DispatchGooglePhotosUpload(element.url);
+ if (!uploadToken) {
+ failed.push(index);
+ } else {
+ newMediaItems.push({
+ description: element.description,
+ simpleMediaItem: { uploadToken }
+ });
+ }
}
+ return newMediaItems;
}
- return newMediaItems;
+ );
+
+ const failedCount = failed.length;
+ if (failedCount) {
+ console.error(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
}
- );
- const failedCount = failed.length;
- if (failedCount) {
- console.log(`Unable to upload ${failedCount} image${failedCount === 1 ? "" : "s"} to Google's servers`);
+ GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
+ result => _success(res, { results: result.newMediaItemResults, failed }),
+ error => _error(res, mediaError, error)
+ );
}
-
- GooglePhotosUploadUtils.CreateMediaItems(newMediaItems, req.body.album).then(
- result => _success(res, { results: result.newMediaItemResults, failed }),
- error => _error(res, mediaError, error)
- );
});
interface MediaItem {