diff options
Diffstat (limited to 'src')
22 files changed, 769 insertions, 282 deletions
diff --git a/src/client/SocketStub.ts b/src/client/SocketStub.ts index 18df4ca0a..c48f21f63 100644 --- a/src/client/SocketStub.ts +++ b/src/client/SocketStub.ts @@ -48,6 +48,7 @@ export class SocketStub { public static SEND_FIELDS_REQUEST(fieldIds: FieldId[], callback: (fields: { [key: string]: Field }) => any) { Utils.EmitCallback(Server.Socket, MessageStore.GetFields, fieldIds, (fields: any[]) => { + console.log(fieldIds); let fieldMap: any = {}; for (let field of fields) { fieldMap[field._id] = ServerUtils.FromJson(field); diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index bb463b36f..4cab53030 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -59,14 +59,14 @@ export namespace Documents { const videoProtoId = "videoProto" const audioProtoId = "audioProto"; - export function initProtos(mainDocId: string, callback: (mainDoc?: Document) => void) { - Server.GetFields([collProtoId, textProtoId, imageProtoId, mainDocId], (fields) => { + export function initProtos(callback: () => void) { + Server.GetFields([collProtoId, textProtoId, imageProtoId], (fields) => { collProto = fields[collProtoId] as Document; imageProto = fields[imageProtoId] as Document; textProto = fields[textProtoId] as Document; webProto = fields[webProtoId] as Document; kvpProto = fields[kvpProtoId] as Document; - callback(fields[mainDocId] as Document) + callback(); }); } function assignOptions(doc: Document, options: DocumentOptions): Document { @@ -89,9 +89,11 @@ export namespace Documents { function setupPrototypeOptions(protoId: string, title: string, layout: string, options: DocumentOptions): Document { return assignOptions(new Document(protoId), { ...options, title: title, layout: layout }); } - function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: T, ctor: { new(): U }, id?: string) { + function SetInstanceOptions<T, U extends Field & { Data: T }>(doc: Document, options: DocumentOptions, value: T | undefined, ctor: { new(): U } | undefined, id?: string) { var deleg = doc.MakeDelegate(id); - deleg.SetData(KeyStore.Data, value, ctor); + if (value !== undefined && ctor !== undefined) { + deleg.SetData(KeyStore.Data, value, ctor); + } return assignOptions(deleg, options); } @@ -160,7 +162,7 @@ export namespace Documents { return SetInstanceOptions(GetAudioPrototype(), options, new URL(url), AudioField); } export function TextDocument(options: DocumentOptions = {}) { - return SetInstanceOptions(GetTextPrototype(), options, "", RichTextField); + return SetInstanceOptions(GetTextPrototype(), options, undefined, undefined); } export function PdfDocument(url: string, options: DocumentOptions = {}) { return SetInstanceOptions(GetPdfPrototype(), options, new URL(url), PDFField); diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index 84b1b91c3..55a49863d 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -20,6 +20,7 @@ export interface EditableProps { */ contents: any; height: number + display: string; } /** @@ -46,10 +47,10 @@ export class EditableView extends React.Component<EditableProps> { render() { if (this.editing) { return <input defaultValue={this.props.GetValue()} onKeyDown={this.onKeyDown} autoFocus onBlur={action(() => this.editing = false)} - style={{ display: "inline" }}></input> + style={{ display: this.props.display }}></input> } else { return ( - <div className="editableView-container-editing" style={{ display: "inline", height: "100%", maxHeight: `${this.props.height}` }} + <div className="editableView-container-editing" style={{ display: this.props.display, height: "100%", maxHeight: `${this.props.height}` }} onClick={action(() => this.editing = true)}> {this.props.contents} </div> diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx index b78f59681..4a0f2b021 100644 --- a/src/client/views/Main.tsx +++ b/src/client/views/Main.tsx @@ -1,4 +1,4 @@ -import { action, configure } from 'mobx'; +import { action, configure, observable } from 'mobx'; import "normalize.css"; import * as React from 'react'; import * as ReactDOM from 'react-dom'; @@ -7,111 +7,191 @@ import { KeyStore } from '../../fields/KeyStore'; import "./Main.scss"; import { MessageStore } from '../../server/Message'; import { Utils } from '../../Utils'; +import * as request from 'request' import { Documents } from '../documents/Documents'; import { Server } from '../Server'; import { setupDrag } from '../util/DragManager'; import { Transform } from '../util/Transform'; import { UndoManager } from '../util/UndoManager'; +import { WorkspacesMenu } from '../../server/authentication/controllers/WorkspacesMenu'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { ContextMenu } from './ContextMenu'; import { DocumentDecorations } from './DocumentDecorations'; import { DocumentView } from './nodes/DocumentView'; import "./Main.scss"; +import { observer } from 'mobx-react'; +import { Field, Opt } from '../../fields/Field'; import { InkingControl } from './InkingControl'; +import { RouteStore } from '../../server/RouteStore'; +import { Database } from '../../server/database'; +@observer +export class Main extends React.Component { + // dummy initializations keep the compiler happy + @observable private mainContainer?: Document; + @observable private mainfreeform?: Document; + @observable private userWorkspaces: Document[] = []; + @observable private activeUsers: Document[] = []; -configure({ enforceActions: "observed" }); // causes errors to be generated when modifying an observable outside of an action -window.addEventListener("drop", (e) => e.preventDefault(), false) -window.addEventListener("dragover", (e) => e.preventDefault(), false) -document.addEventListener("pointerdown", action(function (e: PointerEvent) { - if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { - ContextMenu.Instance.clearItems() + constructor(props: Readonly<{}>) { + super(props); + // causes errors to be generated when modifying an observable outside of an action + configure({ enforceActions: "observed" }); + + this.initEventListeners(); + Documents.initProtos(() => { + this.initAuthenticationRouters(); + }); } -}), true) + initEventListeners = () => { + window.addEventListener("drop", (e) => e.preventDefault(), false) // drop event handler + window.addEventListener("dragover", (e) => e.preventDefault(), false) // drag event handler + // click interactions for the context menu + document.addEventListener("pointerdown", action(function (e: PointerEvent) { + if (!ContextMenu.Instance.intersects(e.pageX, e.pageY)) { + ContextMenu.Instance.clearItems(); + } + }), true); + } -const mainDocId = "mainDoc"; -let mainContainer: Document; -let mainfreeform: Document; -Documents.initProtos(mainDocId, (res?: Document) => { - if (res instanceof Document) { - mainContainer = res; - mainContainer.GetAsync(KeyStore.ActiveFrame, field => mainfreeform = field as Document); + initAuthenticationRouters = () => { + // Load the user's active workspace, or create a new one if initial session after signup + request.get(this.prepend(RouteStore.getActiveWorkspace), (error, response, body) => { + if (body) { + Server.GetField(body, field => { + if (field instanceof Document) { + this.openWorkspace(field); + this.populateWorkspaces(); + } else { + this.createNewWorkspace(true); + } + }); + } else { + this.createNewWorkspace(true); + } + }); } - else { - mainContainer = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: "main container" }, mainDocId); + + @action + createNewWorkspace = (init: boolean): void => { + let mainDoc = Documents.DockDocument(JSON.stringify({ content: [{ type: 'row', content: [] }] }), { title: `Main Container ${this.userWorkspaces.length + 1}` }); + let newId = mainDoc.Id; + request.post(this.prepend(RouteStore.addWorkspace), { + body: { target: newId }, + json: true + }, () => { if (init) this.populateWorkspaces(); }); // bcz: strangely, we need a timeout to prevent exceptions/issues initializing GoldenLayout (the rendering engine for Main Container) setTimeout(() => { - mainfreeform = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); - - var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(mainfreeform)] }] }; - mainContainer.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); - mainContainer.Set(KeyStore.ActiveFrame, mainfreeform); + let freeformDoc = Documents.FreeformDocument([], { x: 0, y: 400, title: "mini collection" }); + var dockingLayout = { content: [{ type: 'row', content: [CollectionDockingView.makeDocumentConfig(freeformDoc)] }] }; + mainDoc.SetText(KeyStore.Data, JSON.stringify(dockingLayout)); + mainDoc.Set(KeyStore.ActiveFrame, freeformDoc); + this.openWorkspace(mainDoc); }, 0); + this.userWorkspaces.push(mainDoc); + mainDoc.GetList<Document>(KeyStore.ActiveUsers, []); + } + + @action + populateWorkspaces = () => { + // retrieve all workspace documents from the server + request.get(this.prepend(RouteStore.getAllWorkspaces), (error, res, body) => { + let ids = JSON.parse(body) as string[]; + Server.GetFields(ids, action((fields: { [id: string]: Field }) => this.userWorkspaces = ids.map(id => fields[id] as Document))); + }); + } + + @action + openWorkspace = (doc: Document): void => { + request.post(this.prepend(RouteStore.setActiveWorkspace), { + body: { target: doc.Id }, + json: true + }); + this.mainContainer = doc; + this.mainContainer.GetAsync(KeyStore.ActiveFrame, field => this.mainfreeform = field as Document); + } + + toggleWorkspaces = () => { + if (WorkspacesMenu.Instance) { + WorkspacesMenu.Instance.toggle() + } + } + + prepend = (extension: string) => window.location.origin + extension; + + render() { + let imgRef = React.createRef<HTMLDivElement>(); + let pdfRef = React.createRef<HTMLDivElement>(); + let webRef = React.createRef<HTMLDivElement>(); + let textRef = React.createRef<HTMLDivElement>(); + let schemaRef = React.createRef<HTMLDivElement>(); + let videoRef = React.createRef<HTMLDivElement>(); + let audioRef = React.createRef<HTMLDivElement>(); + let colRef = React.createRef<HTMLDivElement>(); + let workspacesRef = React.createRef<HTMLDivElement>(); + + let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; + let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" + let weburl = "https://cs.brown.edu/courses/cs166/"; + let audiourl = "http://techslides.com/demos/samples/sample.mp3"; + let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; + + let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) + let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) + let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); + let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); + let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" })); + let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); + let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); + let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); + let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })) + + let addClick = (creator: () => Document) => action(() => this.mainfreeform!.GetList<Document>(KeyStore.Data, []).push(creator())); + + if (!this.mainContainer) { + return <div></div> + } + return ( + <div style={{ position: "absolute", width: "100%", height: "100%" }}> + <DocumentView Document={this.mainContainer} + AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} + ContentScaling={() => 1} + PanelWidth={() => 0} + PanelHeight={() => 0} + isTopMost={true} + SelectOnLoad={false} + focus={() => { }} + ContainingCollectionView={undefined} /> + <DocumentDecorations /> + <ContextMenu /> + <WorkspacesMenu active={this.mainContainer} open={this.openWorkspace} new={this.createNewWorkspace} allWorkspaces={this.userWorkspaces} /> + <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > + <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> + <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > + <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> + <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> + <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> + <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> + <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> + <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> + <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> + <div className="main-buttonDiv" style={{ bottom: '125px' }} > + <button onClick={clearDatabase}>Clear Database</button></div> + <div className="main-buttonDiv" style={{ top: '25px' }} ref={workspacesRef}> + <button onClick={this.toggleWorkspaces}>View Workspaces</button></div> + <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}> + <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div> + <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}> + <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div> + <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> + <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> + <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> + <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> + </div> + ); } +} - let imgurl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg"; - let pdfurl = "http://www.adobe.com/support/products/enterprise/knowledgecenter/media/c4611_sample_explain.pdf" - let weburl = "https://cs.brown.edu/courses/cs166/"; - let audiourl = "http://techslides.com/demos/samples/sample.mp3"; - let videourl = "http://techslides.com/demos/sample-videos/small.mp4"; - let clearDatabase = action(() => Utils.Emit(Server.Socket, MessageStore.DeleteAll, {})) - let addTextNode = action(() => Documents.TextDocument({ width: 200, height: 200, title: "a text note" })) - let addColNode = action(() => Documents.FreeformDocument([], { width: 200, height: 200, title: "a freeform collection" })); - let addSchemaNode = action(() => Documents.SchemaDocument([Documents.TextDocument()], { width: 200, height: 200, title: "a schema collection" })); - let addVideoNode = action(() => Documents.VideoDocument(videourl, { width: 200, height: 200, title: "video node" })); - let addPDFNode = action(() => Documents.PdfDocument(pdfurl, { width: 200, height: 200, title: "a schema collection" })); - let addImageNode = action(() => Documents.ImageDocument(imgurl, { width: 200, height: 200, title: "an image of a cat" })); - let addWebNode = action(() => Documents.WebDocument(weburl, { width: 200, height: 200, title: "a sample web page" })); - let addAudioNode = action(() => Documents.AudioDocument(audiourl, { width: 200, height: 200, title: "audio node" })) - let addClick = (creator: () => Document) => action(() => - mainfreeform.GetList<Document>(KeyStore.Data, []).push(creator()) - ); - - let imgRef = React.createRef<HTMLDivElement>(); - let pdfRef = React.createRef<HTMLDivElement>(); - let webRef = React.createRef<HTMLDivElement>(); - let textRef = React.createRef<HTMLDivElement>(); - let schemaRef = React.createRef<HTMLDivElement>(); - let videoRef = React.createRef<HTMLDivElement>(); - let audioRef = React.createRef<HTMLDivElement>(); - let colRef = React.createRef<HTMLDivElement>(); - - ReactDOM.render(( - <div style={{ position: "absolute", width: "100%", height: "100%" }}> - <DocumentView Document={mainContainer} - AddDocument={undefined} RemoveDocument={undefined} ScreenToLocalTransform={() => Transform.Identity} - ContentScaling={() => 1} - PanelWidth={() => 0} - PanelHeight={() => 0} - isTopMost={true} - SelectOnLoad={false} - focus={() => { }} - ContainingCollectionView={undefined} /> - <DocumentDecorations /> - <ContextMenu /> - <div className="main-buttonDiv" style={{ bottom: '0px' }} ref={imgRef} > - <button onPointerDown={setupDrag(imgRef, addImageNode)} onClick={addClick(addImageNode)}>Add Image</button></div> - <div className="main-buttonDiv" style={{ bottom: '25px' }} ref={webRef} > - <button onPointerDown={setupDrag(webRef, addWebNode)} onClick={addClick(addWebNode)}>Add Web</button></div> - <div className="main-buttonDiv" style={{ bottom: '50px' }} ref={textRef}> - <button onPointerDown={setupDrag(textRef, addTextNode)} onClick={addClick(addTextNode)}>Add Text</button></div> - <div className="main-buttonDiv" style={{ bottom: '75px' }} ref={colRef}> - <button onPointerDown={setupDrag(colRef, addColNode)} onClick={addClick(addColNode)}>Add Collection</button></div> - <div className="main-buttonDiv" style={{ bottom: '100px' }} ref={schemaRef}> - <button onPointerDown={setupDrag(schemaRef, addSchemaNode)} onClick={addClick(addSchemaNode)}>Add Schema</button></div> - <div className="main-buttonDiv" style={{ bottom: '125px' }} > - <button onClick={clearDatabase}>Clear Database</button></div> - <div className="main-buttonDiv" style={{ bottom: '175px' }} ref={videoRef}> - <button onPointerDown={setupDrag(videoRef, addVideoNode)} onClick={addClick(addVideoNode)}>Add Video</button></div> - <div className="main-buttonDiv" style={{ bottom: '200px' }} ref={audioRef}> - <button onPointerDown={setupDrag(audioRef, addAudioNode)} onClick={addClick(addAudioNode)}>Add Audio</button></div> - <div className="main-buttonDiv" style={{ bottom: '150px' }} ref={pdfRef}> - <button onPointerDown={setupDrag(pdfRef, addPDFNode)} onClick={addClick(addPDFNode)}>Add PDF</button></div> - <button className="main-undoButtons" style={{ bottom: '25px' }} onClick={() => UndoManager.Undo()}>Undo</button> - <button className="main-undoButtons" style={{ bottom: '0px' }} onClick={() => UndoManager.Redo()}>Redo</button> - <InkingControl /> - </div>), - document.getElementById('root')); -}) +ReactDOM.render(<Main />, document.getElementById('root')); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 94005a4c0..b5cafe681 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -144,7 +144,7 @@ export class CollectionDockingView extends React.Component<SubCollectionViewProp if (this._containerRef.current) { reaction( () => this.props.Document.GetText(KeyStore.Data, ""), - () => this.setupGoldenLayout(), { fireImmediately: true }); + () => setTimeout(() => this.setupGoldenLayout(), 1), { fireImmediately: true }); window.addEventListener('resize', this.onResize); // bcz: would rather add this event to the parent node, but resize events only come from Window } diff --git a/src/client/views/collections/CollectionFreeFormView.scss b/src/client/views/collections/CollectionFreeFormView.scss index d487cd7ce..28242c939 100644 --- a/src/client/views/collections/CollectionFreeFormView.scss +++ b/src/client/views/collections/CollectionFreeFormView.scss @@ -1,11 +1,10 @@ .collectionfreeformview-container { - - .collectionfreeformview > .jsx-parser{ + + .collectionfreeformview > .jsx-parser{ position:absolute; height: 100%; width: 100%; } - border-style: solid; box-sizing: border-box; position: relative; diff --git a/src/client/views/collections/CollectionFreeFormView.tsx b/src/client/views/collections/CollectionFreeFormView.tsx index 74d37ccec..0c74d1852 100644 --- a/src/client/views/collections/CollectionFreeFormView.tsx +++ b/src/client/views/collections/CollectionFreeFormView.tsx @@ -261,19 +261,19 @@ export class CollectionFreeFormView extends CollectionViewBase { @action onKeyDown = (e: React.KeyboardEvent<Element>) => { //if not these keys, make a textbox if preview cursor is active! - if (!e.ctrlKey && !e.altKey) { - if (this._previewCursorVisible) { - //make textbox and add it to this collection - let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); - let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); - // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself - this._selectOnLoaded = newBox.Id; - //set text to be the typed key and get focus on text box - this.props.CollectionView.addDocument(newBox); - //remove cursor from screen - this._previewCursorVisible = false; - } - } + // if (!e.ctrlKey && !e.altKey) { + // if (this._previewCursorVisible) { + // //make textbox and add it to this collection + // let [x, y] = this.getTransform().transformPoint(this._downX, this._downY); (this._downX, this._downY); + // let newBox = Documents.TextDocument({ width: 200, height: 100, x: x, y: y, title: "new" }); + // // mark this collection so that when the text box is created we can send it the SelectOnLoad prop to focus itself + // this._selectOnLoaded = newBox.Id; + // //set text to be the typed key and get focus on text box + // this.props.CollectionView.addDocument(newBox); + // //remove cursor from screen + // this._previewCursorVisible = false; + // } + // } } @action @@ -390,6 +390,9 @@ export class CollectionFreeFormView extends CollectionViewBase { const panx: number = -this.props.Document.GetNumber(KeyStore.PanX, 0); const pany: number = -this.props.Document.GetNumber(KeyStore.PanY, 0); + // const panx: number = this.props.Document.GetNumber(KeyStore.PanX, 0) + this.centeringShiftX; + // const pany: number = this.props.Document.GetNumber(KeyStore.PanY, 0) + this.centeringShiftY; + // console.log("center:", this.getLocalTransform().transformPoint(this.centeringShiftX, this.centeringShiftY)); return ( <div className={`collectionfreeformview${this.isAnnotationOverlay ? "-overlay" : "-container"}`} diff --git a/src/client/views/collections/CollectionSchemaView.scss b/src/client/views/collections/CollectionSchemaView.scss index d40e6d314..88a3b73d4 100644 --- a/src/client/views/collections/CollectionSchemaView.scss +++ b/src/client/views/collections/CollectionSchemaView.scss @@ -1,5 +1,3 @@ - - .collectionSchemaView-container { border-style: solid; box-sizing: border-box; @@ -27,16 +25,18 @@ float: left; height: 100%; } + ::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(0, 0, 0, .5); + } .collectionSchemaView-tableContainer { position: relative; float: left; height: 100%; } - .ReactTable { - position: absolute; - // display: inline-block; - // overflow: auto; + position: absolute; // display: inline-block; + // overflow: auto; width: 100%; height: 100%; background: white; @@ -45,10 +45,8 @@ overflow-y: auto; overflow-x: auto; height: 100%; - display: -webkit-inline-box; - direction: ltr; - // direction:rtl; + direction: ltr; // direction:rtl; // display:block; } .rt-tbody { @@ -63,8 +61,8 @@ border-width: 1; border-right-color: #aaa; .imageBox-cont { - position:relative; - max-height:100%; + position: relative; + max-height: 100%; } .imageBox-cont img { object-fit: contain; @@ -77,9 +75,24 @@ border-bottom-color: #aaa } } + .ReactTable .rt-table { + overflow-y: auto; + overflow-x: auto; + height: 100%; + display: -webkit-inline-box; + direction: ltr; // direction:rtl; + // display:block; + } + .ReactTable .rt-tbody { + //direction: ltr; + direction: rtl; + } + .ReactTable .rt-tr-group { + direction: ltr; + } .ReactTable .rt-thead.-header { background:grey; - } + } .ReactTable .rt-th, .ReactTable .rt-td { max-height: 44; padding: 3px 7px; diff --git a/src/client/views/collections/CollectionSchemaView.tsx b/src/client/views/collections/CollectionSchemaView.tsx index 04f017378..17eeaa5a2 100644 --- a/src/client/views/collections/CollectionSchemaView.tsx +++ b/src/client/views/collections/CollectionSchemaView.tsx @@ -50,7 +50,9 @@ export class CollectionSchemaView extends CollectionViewBase { let onItemDown = setupDrag(reference, () => props.doc); return ( <div onPointerDown={onItemDown} key={props.doc.Id} ref={reference}> - <EditableView contents={contents} + <EditableView + display={"inline"} + contents={contents} height={36} GetValue={() => { let field = props.doc.Get(props.fieldKey); if (field && field instanceof Field) { @@ -59,7 +61,7 @@ export class CollectionSchemaView extends CollectionViewBase { return field || ""; }} SetValue={(value: string) => { - let script = CompileScript(value, undefined, true); + let script = CompileScript(value); if (!script.compiled) { return false; } diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 8b06d9ac4..9c31bdae2 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -69,7 +69,9 @@ class TreeView extends React.Component<TreeViewProps> { return <div key={this.props.document.Id}></div>; } - return <div className="docContainer"> <EditableView contents={title.Data} + return <div className="docContainer"> <EditableView + display={"inline"} + contents={title.Data} height={36} GetValue={() => { let title = this.props.document.GetT<TextField>(KeyStore.Title, TextField); if (title && title !== "<Waiting>") @@ -159,6 +161,7 @@ export class CollectionTreeView extends CollectionViewBase { <div id="body" className="collectionTreeView-dropTarget" onDrop={(e: React.DragEvent) => this.onDrop(e, {})} ref={this.createDropTarget} style={{ borderWidth: `${COLLECTION_BORDER_WIDTH}px` }}> <h3> <EditableView contents={titleStr} + display={"inline"} height={72} GetValue={() => { return this.props.Document.Title; }} SetValue={(value: string) => { diff --git a/src/client/views/collections/CollectionViewBase.tsx b/src/client/views/collections/CollectionViewBase.tsx index 31c89a75d..ddde05011 100644 --- a/src/client/views/collections/CollectionViewBase.tsx +++ b/src/client/views/collections/CollectionViewBase.tsx @@ -11,6 +11,7 @@ import { Documents, DocumentOptions } from "../../documents/Documents"; import { Key } from "../../../fields/Key"; import { Transform } from "../../util/Transform"; import { CollectionView } from "./CollectionView"; +import { RouteStore } from "../../../server/RouteStore"; export interface CollectionViewProps { fieldKey: Key; @@ -78,7 +79,7 @@ export class CollectionViewBase extends React.Component<SubCollectionViewProps> console.log(e.dataTransfer.items.length); for (let i = 0; i < e.dataTransfer.items.length; i++) { - const upload = window.location.origin + "/upload"; + const upload = window.location.origin + RouteStore.upload; let item = e.dataTransfer.items[i]; if (item.kind === "string" && item.type.indexOf("uri") != -1) { e.dataTransfer.items[i].getAsString(function (s) { diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 368a80b8e..1f873f8c5 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -17,6 +17,7 @@ import "./ImageBox.scss"; import "./PDFBox.scss"; import { Sticky } from './Sticky'; //you should look at sticky and annotation, because they are used here import React = require("react") +import { RouteStore } from "../../../server/RouteStore"; /** ALSO LOOK AT: Annotation.tsx, Sticky.tsx * This method renders PDF and puts all kinds of functionalities such as annotation, highlighting, @@ -441,7 +442,7 @@ export class PDFBox extends React.Component<FieldViewProps> { let pdfUrl = this.props.doc.GetT(this.props.fieldKey, PDFField); let xf = this.props.doc.GetNumber(KeyStore.NativeHeight, 0) / renderHeight; return <div className="pdfBox-contentContainer" key="container" style={{ transform: `scale(${xf}, ${xf})` }}> - <Document file={window.origin + "/corsProxy/" + `${pdfUrl}`}> + <Document file={window.origin + RouteStore.corsProxy + `${pdfUrl}`}> <Measure onResize={this.setScaling}> {({ measureRef }) => <div className="pdfBox-page" ref={measureRef}> diff --git a/src/fields/KeyStore.ts b/src/fields/KeyStore.ts index f93a68c85..71b189e19 100644 --- a/src/fields/KeyStore.ts +++ b/src/fields/KeyStore.ts @@ -35,4 +35,5 @@ export namespace KeyStore { export const CurPage = new Key("CurPage"); export const NumPages = new Key("NumPages"); export const Ink = new Key("Ink"); + export const ActiveUsers = new Key("Active Users"); } diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts new file mode 100644 index 000000000..ace2152d7 --- /dev/null +++ b/src/server/RouteStore.ts @@ -0,0 +1,28 @@ +// PREPEND ALL ROUTES WITH FORWARD SLASHES! + +export enum RouteStore { + // GENERAL + root = "/root", + home = "/home", + corsProxy = "/corsProxy", + delete = "/delete", + + // UPLOAD AND STATIC FILE SERVING + public = "/public", + upload = "/upload", + images = "/images", + + // USER AND WORKSPACES + addWorkspace = "/addWorkspaceId", + getAllWorkspaces = "/getAllWorkspaceIds", + getActiveWorkspace = "/getActiveWorkspaceId", + setActiveWorkspace = "/setActiveWorkspaceId", + + // AUTHENTICATION + signup = "/signup", + login = "/login", + logout = "/logout", + forgot = "/forgotpassword", + reset = "/reset/:token", + +}
\ No newline at end of file diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 05f6c3133..b6fe15655 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -2,8 +2,9 @@ import * as passport from 'passport' import * as passportLocal from 'passport-local'; import * as mongodb from 'mongodb'; import * as _ from "lodash"; -import { default as User } from '../models/User'; +import { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; +import { RouteStore } from '../../RouteStore'; const LocalStrategy = passportLocal.Strategy; @@ -18,7 +19,7 @@ passport.deserializeUser<any, any>((id, done) => { }); // AUTHENTICATE JUST WITH EMAIL AND PASSWORD -passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { +passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }, (req, email, password, done) => { User.findOne({ email: email.toLowerCase() }, (error: any, user: any) => { if (error) return done(error); if (!user) return done(undefined, false, { message: "Invalid email or password" }) // invalid email @@ -35,7 +36,7 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = if (req.isAuthenticated()) { return next(); } - return res.redirect("/login"); + return res.redirect(RouteStore.login); } export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { diff --git a/src/server/authentication/controllers/WorkspacesMenu.css b/src/server/authentication/controllers/WorkspacesMenu.css new file mode 100644 index 000000000..b89039965 --- /dev/null +++ b/src/server/authentication/controllers/WorkspacesMenu.css @@ -0,0 +1,3 @@ +.ids:hover { + color: darkblue; +}
\ No newline at end of file diff --git a/src/server/authentication/controllers/WorkspacesMenu.tsx b/src/server/authentication/controllers/WorkspacesMenu.tsx new file mode 100644 index 000000000..ffef2e11c --- /dev/null +++ b/src/server/authentication/controllers/WorkspacesMenu.tsx @@ -0,0 +1,99 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import { observable, action, configure, reaction, computed, ObservableMap, runInAction } from 'mobx'; +import { observer } from "mobx-react"; +import * as request from 'request' +import './WorkspacesMenu.css' +import { Document } from '../../../fields/Document'; +import { Server } from '../../../client/Server'; +import { Field } from '../../../fields/Field'; +import { EditableView } from '../../../client/views/EditableView'; +import { KeyStore } from '../../../fields/KeyStore'; + +export interface WorkspaceMenuProps { + active: Document; + open: (workspace: Document) => void; + new: (init: boolean) => void; + allWorkspaces: Document[]; +} + +@observer +export class WorkspacesMenu extends React.Component<WorkspaceMenuProps> { + static Instance: WorkspacesMenu; + @observable private workspacesExposed: boolean = false; + + constructor(props: WorkspaceMenuProps) { + super(props); + WorkspacesMenu.Instance = this; + this.addNewWorkspace = this.addNewWorkspace.bind(this); + } + + @action + addNewWorkspace() { + this.props.new(false); + this.toggle(); + } + + @action + toggle() { + this.workspacesExposed = !this.workspacesExposed; + } + + render() { + let p = this.props; + return ( + <div + style={{ + width: "auto", + height: "auto", + borderRadius: 5, + position: "absolute", + top: 55, + left: this.workspacesExposed ? 11 : -500, + background: "white", + border: "black solid 2px", + transition: "all 1s ease", + zIndex: 15, + padding: 10, + paddingRight: 12, + }}> + <img + src="https://bit.ly/2IBBkxk" + style={{ + width: 20, + height: 20, + marginTop: 3, + marginLeft: 3, + marginBottom: 3, + cursor: "grab" + }} + onClick={this.addNewWorkspace} + /> + {this.props.allWorkspaces.map((s, i) => + <div + key={s.Id} + onContextMenu={(e) => { + e.preventDefault(); + this.props.open(s); + }} + style={{ + marginTop: 10 + }} + > + <span>{i + 1} - </span> + <EditableView + display={"inline"} + GetValue={() => { return s.Title }} + SetValue={(title: string): boolean => { + s.SetText(KeyStore.Title, title); + return true; + }} + contents={s.Title} + height={20} + /> + </div> + )} + </div> + ); + } +}
\ No newline at end of file diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts deleted file mode 100644 index f74ff9039..000000000 --- a/src/server/authentication/controllers/user.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { default as User, UserModel, AuthToken } from "../models/User"; -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"; -const flash = require("express-flash"); -import * as session from "express-session"; -import * as pug from 'pug'; - -/** - * GET /signup - * Signup page. - */ -export let getSignup = (req: Request, res: Response) => { - if (req.user) { - return res.redirect("/"); - } - res.render("signup.pug", { - title: "Sign Up" - }); -}; - -/** - * POST /signup - * Create a new local account. - */ -export let postSignup = (req: Request, res: Response, next: NextFunction) => { - req.assert("email", "Email is not valid").isEmail(); - req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); - req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); - req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); - - const errors = req.validationErrors(); - - if (errors) { - req.flash("errors", "Unable to facilitate sign up. Please try again."); - return res.redirect("/signup"); - } - - const user = new User({ - email: req.body.email, - password: req.body.password - }); - - User.findOne({ email: req.body.email }, (err, existingUser) => { - if (err) { return next(err); } - if (existingUser) { - req.flash("errors", "Account with that email address already exists."); - return res.redirect("/signup"); - } - user.save((err) => { - if (err) { return next(err); } - req.logIn(user, (err) => { - if (err) { - return next(err); - } - res.redirect("/"); - }); - }); - }); -}; - - -/** - * GET /login - * Login page. - */ -export let getLogin = (req: Request, res: Response) => { - if (req.user) { - return res.redirect("/"); - } - res.send("<p>dear lord please render</p>"); - // res.render("account/login", { - // title: "Login" - // }); -}; - -/** - * POST /login - * Sign in using email and password. - */ -export let postLogin = (req: Request, res: Response, next: NextFunction) => { - req.assert("email", "Email is not valid").isEmail(); - req.assert("password", "Password cannot be blank").notEmpty(); - req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); - - const errors = req.validationErrors(); - - if (errors) { - req.flash("errors", "Unable to login at this time. Please try again."); - return res.redirect("/login"); - } - - passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => { - if (err) { return next(err); } - if (!user) { - req.flash("errors", info.message); - return res.redirect("/login"); - } - req.logIn(user, (err) => { - if (err) { return next(err); } - req.flash("success", "Success! You are logged in."); - res.redirect("/"); - }); - })(req, res, next); -};
\ No newline at end of file diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts new file mode 100644 index 000000000..2cef958e8 --- /dev/null +++ b/src/server/authentication/controllers/user_controller.ts @@ -0,0 +1,263 @@ +import { default as User, DashUserModel, AuthToken } from "../models/user_model"; +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"; +const 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"; + +/** + * GET /signup + * Directs user to the signup page + * modeled by signup.pug in views + */ +export let getSignup = (req: Request, res: Response) => { + if (req.user) { + let user = req.user; + return res.redirect(RouteStore.home); + } + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); +}; + +/** + * POST /signup + * Create a new local account. + */ +export let postSignup = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); + req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); + return res.redirect(RouteStore.signup); + } + + const email = req.body.email; + const password = req.body.password; + + const user = new User({ + email, + password, + userDoc: "document here" + }); + + User.findOne({ email }, (err, existingUser) => { + if (err) { return next(err); } + if (existingUser) { + return res.redirect(RouteStore.login); + } + user.save((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.redirect(RouteStore.home); + }); + }); + }); + +}; + + +/** + * GET /login + * Login page. + */ +export let getLogin = (req: Request, res: Response) => { + if (req.user) { + return res.redirect(RouteStore.home); + } + res.render("login.pug", { + title: "Log In", + user: req.user + }); +}; + +/** + * POST /login + * Sign in using email and password. + * On failure, redirect to signup page + */ +export let postLogin = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password cannot be blank").notEmpty(); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + req.flash("errors", "Unable to login at this time. Please try again."); + return res.redirect(RouteStore.signup); + } + + passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { + if (err) { return next(err); } + if (!user) { + return res.redirect(RouteStore.signup); + } + req.logIn(user, (err) => { + if (err) { return next(err); } + res.redirect(RouteStore.home); + }); + })(req, res, next); +}; + +/** + * GET /logout + * Invokes the logout function on the request + * and destroys the user's current session. + */ +export let getLogout = (req: Request, res: Response) => { + req.logout(); + const sess = req.session; + if (sess) { + sess.destroy((err) => { if (err) { console.log(err); } }); + } + res.redirect(RouteStore.login); +} + +export let getForgot = function (req: Request, res: Response) { + res.render("forgot.pug", { + title: "Recover Password", + user: req.user, + }); +} + +export let postForgot = function (req: Request, res: Response, next: NextFunction) { + const email = req.body.email; + async.waterfall([ + function (done: any) { + let token: string; + c.randomBytes(20, function (err: any, buffer: Buffer) { + if (err) { + done(null); + return; + } + done(null, buffer.toString('hex')); + }) + }, + function (token: string, done: any) { + User.findOne({ email }, function (err, user: DashUserModel) { + if (!user) { + // NO ACCOUNT WITH SUBMITTED EMAIL + return res.redirect(RouteStore.forgot); + } + user.passwordResetToken = token; + user.passwordResetExpires = new Date(Date.now() + 3600000); // 1 HOUR + user.save(function (err: any) { + done(null, token, user); + }); + }); + }, + function (token: Uint16Array, user: DashUserModel, done: any) { + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + const mailOptions = { + to: user.email, + from: 'brownptcdash@gmail.com', + 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' + + 'If you did not request this, please ignore this email and your password will remain unchanged.\n' + }; + smtpTransport.sendMail(mailOptions, function (err) { + // req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.'); + done(null, err, 'done'); + }); + } + ], function (err) { + if (err) return next(err); + res.redirect(RouteStore.forgot); + }) +} + +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); + } + res.render("reset.pug", { + title: "Reset Password", + user: req.user, + }); + }); +} + +export let postReset = function (req: Request, res: Response) { + async.waterfall([ + function (done: any) { + User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { + if (!user || err) { + return res.redirect('back'); + } + + req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); + req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); + + if (req.validationErrors()) { + return res.redirect('back'); + } + + user.password = req.body.password; + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + + user.save(function (err) { + if (err) { + return res.redirect(RouteStore.login); + } + req.logIn(user, function (err) { + if (err) { + return; + } + }); + done(null, user); + }); + }); + }, + function (user: DashUserModel, done: any) { + const smtpTransport = nodemailer.createTransport({ + service: 'Gmail', + auth: { + user: 'brownptcdash@gmail.com', + pass: 'browngfx1' + } + }); + const mailOptions = { + to: user.email, + from: 'brownptcdash@gmail.com', + subject: 'Your password has been changed', + text: 'Hello,\n\n' + + 'This is a confirmation that the password for your account ' + user.email + ' has just been changed.\n' + }; + smtpTransport.sendMail(mailOptions, function (err) { + done(null, err); + }); + } + ], function (err) { + res.redirect(RouteStore.login); + }); +}
\ No newline at end of file diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/user_model.ts index 9752c4260..3d4ed6896 100644 --- a/src/server/authentication/models/User.ts +++ b/src/server/authentication/models/user_model.ts @@ -1,6 +1,5 @@ //@ts-ignore import * as bcrypt from "bcrypt-nodejs"; -import * as crypto from "crypto"; //@ts-ignore import * as mongoose from "mongoose"; var url = 'mongodb://localhost:27017/Dash' @@ -16,12 +15,15 @@ mongoose.connection.on('error', function (error) { mongoose.connection.on('disconnected', function () { console.log('connection closed'); }); -export type UserModel = mongoose.Document & { +export type DashUserModel = mongoose.Document & { email: string, password: string, - passwordResetToken: string, - passwordResetExpires: Date, - tokens: AuthToken[], + passwordResetToken: string | undefined, + passwordResetExpires: Date | undefined, + + allWorkspaceIds: Array<String>, + activeWorkspaceId: String, + activeUsersId: String, profile: { name: string, @@ -47,10 +49,16 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, + allWorkspaceIds: { + type: Array, + default: [] + }, + activeWorkspaceId: String, + activeUsersId: String, + facebook: String, twitter: String, google: String, - tokens: Array, profile: { name: String, @@ -65,7 +73,7 @@ const userSchema = new mongoose.Schema({ * Password hash middleware. */ userSchema.pre("save", function save(next) { - const user = this as UserModel; + const user = this as DashUserModel; if (!user.isModified("password")) { return next(); } bcrypt.genSalt(10, (err, salt) => { if (err) { return next(err); } @@ -77,7 +85,7 @@ userSchema.pre("save", function save(next) { }); }); -const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) { +const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => { cb(err, isMatch); }); diff --git a/src/server/database.ts b/src/server/database.ts index f3c1c9427..f414266e2 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -70,6 +70,16 @@ export class Database { let collection = this.db.collection('documents'); let cursor = collection.find({ _id: { "$in": ids } }) cursor.toArray((err, docs) => { + if (err) { + console.log("Error"); + console.log(err.message); + console.log(err.errmsg); + console.log(ids); + console.log(["afca93a8-c6bd-4b58-967e-07784c5b12c8"]); + console.log("MAKES SENSE: " + (ids instanceof Array)); + } + console.log(typeof ids); + console.log("DATABASE: " + docs); fn(docs); }) }; diff --git a/src/server/index.ts b/src/server/index.ts index 83fa84746..6f6f620d8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -15,54 +15,60 @@ import { FieldId, Field } from '../fields/Field'; import { Database } from './database'; import { ServerUtils } from './ServerUtil'; import { ObjectID } from 'mongodb'; +import * as bcrypt from "bcrypt-nodejs"; import { Document } from '../fields/Document'; import * as io from 'socket.io' import * as passportConfig from './authentication/config/passport'; -import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user'; +import { getLogin, postLogin, getSignup, postSignup, getLogout, postReset, getForgot, postForgot, getReset } from './authentication/controllers/user_controller'; const config = require('../../webpack.config'); const compiler = webpack(config); const port = 1050; // default port to listen const serverPort = 1234; import * as expressValidator from 'express-validator'; import expressFlash = require('express-flash'); +import flash = require('connect-flash'); import * as bodyParser from 'body-parser'; import * as session from 'express-session'; +// import cookieSession = require('cookie-session'); +import * as cookieParser from 'cookie-parser'; import c = require("crypto"); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); -const bluebird = require('bluebird'); import { performance } from 'perf_hooks' +import User, { DashUserModel } from './authentication/models/user_model'; import * as fs from 'fs'; import * as request from 'request' +import { RouteStore } from './RouteStore'; const download = (url: string, dest: fs.PathLike) => { request.get(url).pipe(fs.createWriteStream(dest)); } const mongoUrl = 'mongodb://localhost:27017/Dash'; -// mongoose.Promise = bluebird; -mongoose.connect(mongoUrl)//.then( -// () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, -// ).catch((err: any) => { -// console.log("MongoDB connection error. Please make sure MongoDB is running. " + err); -// process.exit(); -// }); +mongoose.connect(mongoUrl) mongoose.connection.on('connected', function () { console.log("connected"); }) -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(expressValidator()); -app.use(expressFlash()); -app.use(require('express-session')({ +// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE +// ORDER OF IMPORTS MATTERS + +app.use(cookieParser(`${c.randomBytes(64)}`)); +app.use(session({ secret: `${c.randomBytes(64)}`, resave: true, + cookie: { maxAge: 7 * 24 * 60 * 60 }, saveUninitialized: true, store: new MongoStore({ url: 'mongodb://localhost:27017/Dash' }) })); + +app.use(flash()); +app.use(expressFlash()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(expressValidator()); app.use(passport.initialize()); app.use(passport.session()); app.use((req, res, next) => { @@ -70,13 +76,50 @@ app.use((req, res, next) => { next(); }); -app.get("/signup", getSignup); -app.post("/signup", postSignup); -app.get("/login", getLogin); -app.post("/login", postLogin); +enum Method { + GET, + POST +} + +/** + * 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 route the forward slash prepended path name (reference and add to RouteStore.ts) + * @param handler the action to invoke, recieving a DashUserModel and the expected request and response + * @param onRejection an optional callback invoked on return if no user is found to be logged in + */ +function addSecureRoute(method: Method, + route: string, + handler: (user: DashUserModel, req: express.Request, res: express.Response) => void, + onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout)) { + switch (method) { + case Method.GET: + app.get(route, (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) return onRejection(res); + handler(dashUser, req, res); + }); + break; + case Method.POST: + app.post(route, (req, res) => { + const dashUser: DashUserModel = req.user; + if (!dashUser) return onRejection(res); + handler(dashUser, req, res); + }); + break; + } +} -// IMAGE UPLOADING HANDLER -app.post("/upload", (req, res, err) => { +// STATIC FILE SERVING + +let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); + +app.use(express.static(__dirname + RouteStore.public)); +app.use(RouteStore.images, express.static(__dirname + RouteStore.public)) + +addSecureRoute(Method.POST, RouteStore.upload, (user, req, res) => { let form = new formidable.IncomingForm() form.uploadDir = __dirname + "/public/files/" form.keepExtensions = true @@ -91,29 +134,65 @@ app.post("/upload", (req, res, err) => { } res.send(names); }); -}) +}); -app.use(express.static(__dirname + '/public')); -app.use('/images', express.static(__dirname + '/public')) +// anyone attempting to navigate to localhost at this port will +// first have to login +addSecureRoute(Method.GET, RouteStore.root, (user, req, res) => { -let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); +}, res => { + res.send() +}); -// define a route handler for the default home page -app.get("/", (req, res) => { +// YAY! SHOW THEM THEIR WORKSPACES NOW +addSecureRoute(Method.GET, RouteStore.home, (user, req, res) => { res.sendFile(path.join(__dirname, '../../deploy/index.html')); }); -app.get("/hello", (req, res) => { - res.send("<p>Hello</p>"); -}) +addSecureRoute(Method.GET, RouteStore.getActiveWorkspace, (user, req, res) => { + res.send(user.activeWorkspaceId || ""); +}); + +addSecureRoute(Method.GET, RouteStore.getAllWorkspaces, (user, req, res) => { + res.send(JSON.stringify(user.allWorkspaceIds as Array<String>)); +}); + +addSecureRoute(Method.POST, RouteStore.setActiveWorkspace, (user, req) => { + user.update({ $set: { activeWorkspaceId: req.body.target } }, () => { }); +}); + +addSecureRoute(Method.POST, RouteStore.addWorkspace, (user, req) => { + user.update({ $push: { allWorkspaceIds: req.body.target } }, () => { }); +}); -app.use("/corsProxy", (req, res) => { +// 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); + +app.use(RouteStore.corsProxy, (req, res) => { req.pipe(request(req.url.substring(1))).pipe(res); }); -app.get("/delete", (req, res) => { +app.get(RouteStore.delete, (req, res) => { deleteAll(); - res.redirect("/"); + res.redirect(RouteStore.home); }); app.use(wdm(compiler, { @@ -153,10 +232,6 @@ function barReceived(guid: String) { clients[guid.toString()] = new Client(guid.toString()); } -function addDocument(document: Document) { - -} - function getField([id, callback]: [string, (result: any) => void]) { Database.Instance.getDocument(id, (result: any) => { if (result) { |
