diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/server/RouteSubscriber.ts | 26 | ||||
-rw-r--r-- | src/server/index.ts | 340 |
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 { |