From 7e1f89f48d1c4e49dea78dff1c1983e75a11a6a6 Mon Sep 17 00:00:00 2001
From: Bob Zeleznik <zzzman@gmail.com>
Date: Mon, 11 May 2020 17:13:53 -0400
Subject: fixed issues with text auto height and scaled documents.  allowed
 #tagging in text notes to support muliple values with ';'.  DashFieldviews
 can have multiple values, too.  logout is moved to settings.

---
 src/client/util/SettingsManager.tsx | 5 +++++
 1 file changed, 5 insertions(+)

(limited to 'src/client/util/SettingsManager.tsx')

diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index ff0b22381..e20434461 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -8,6 +8,8 @@ import { SelectionManager } from "./SelectionManager";
 import "./SettingsManager.scss";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Networking } from "../Network";
+import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { Utils } from "../../Utils";
 
 library.add(fa.faWindowClose);
 
@@ -90,6 +92,9 @@ export default class SettingsManager extends React.Component<{}> {
                     <div className="settings-type">
                         <button onClick={this.onClick} value="password">reset password</button>
                         <button onClick={this.onClick} value="data">reset data</button>
+                        <button onClick={() => window.location.assign(Utils.prepend("/logout"))}>
+                            {CurrentUserUtils.GuestWorkspace ? "Exit" : "Log Out"}
+                        </button>
                     </div>
                     {this.settingsContent === "password" ?
                         <div className="settings-content">
-- 
cgit v1.2.3-70-g09d2


From f9385130fe297088754d4ce46d6c318c8be2121d Mon Sep 17 00:00:00 2001
From: Sam Wilkins <samwilkins333@gmail.com>
Date: Thu, 14 May 2020 01:29:19 -0700
Subject: server side restructure, some documentation to accompany new wiki
 overview entry

---
 src/client/util/CurrentUserUtils.ts                | 738 +++++++++++++++++++++
 src/client/util/SettingsManager.tsx                |   2 +-
 src/client/views/GestureOverlay.tsx                |   2 +-
 src/client/views/InkingControl.tsx                 |   2 +-
 src/client/views/Main.tsx                          |   2 +-
 src/client/views/MainView.tsx                      |   2 +-
 src/client/views/collections/CollectionMapView.tsx |  13 +-
 src/client/views/collections/CollectionSubView.tsx |   2 +-
 src/client/views/collections/CollectionView.tsx    |   2 +-
 .../CollectionFreeFormRemoteCursors.tsx            |   2 +-
 src/client/views/nodes/ColorBox.tsx                |   2 +-
 src/mobile/ImageUpload.tsx                         |   3 +-
 src/mobile/MobileInterface.tsx                     |   4 +-
 src/server/ApiManagers/ApiManager.ts               |   2 +-
 src/server/ApiManagers/DeleteManager.ts            |   2 +-
 src/server/ApiManagers/GeneralGoogleManager.ts     |   2 +-
 src/server/DashSession/DashSessionAgent.ts         |   2 +-
 src/server/RouteManager.ts                         |   2 +-
 src/server/Websocket/Websocket.ts                  | 305 ---------
 src/server/apis/google/CredentialsLoader.ts        |  29 +
 src/server/apis/google/GoogleApiServerUtils.ts     |  10 +-
 .../apis/google/google_project_credentials.json    |  11 +
 src/server/apis/youtube/youtubeApiSample.js        |  22 +-
 src/server/authentication/AuthenticationManager.ts | 268 ++++++++
 src/server/authentication/DashUserModel.ts         |  86 +++
 src/server/authentication/Passport.ts              |  29 +
 src/server/authentication/config/passport.ts       |  29 -
 .../authentication/controllers/user_controller.ts  | 268 --------
 .../authentication/models/current_user_utils.ts    | 738 ---------------------
 src/server/authentication/models/user_model.ts     |  86 ---
 src/server/credentials/CredentialsLoader.ts        |  29 -
 .../credentials/google_project_credentials.json    |  11 -
 src/server/database.ts                             |  78 ++-
 src/server/index.ts                                |   7 +-
 src/server/server_Initialization.ts                |   4 +-
 src/server/websocket.ts                            | 297 +++++++++
 webpack.config.js                                  |  27 +-
 37 files changed, 1586 insertions(+), 1534 deletions(-)
 create mode 100644 src/client/util/CurrentUserUtils.ts
 delete mode 100644 src/server/Websocket/Websocket.ts
 create mode 100644 src/server/apis/google/CredentialsLoader.ts
 create mode 100644 src/server/apis/google/google_project_credentials.json
 create mode 100644 src/server/authentication/AuthenticationManager.ts
 create mode 100644 src/server/authentication/DashUserModel.ts
 create mode 100644 src/server/authentication/Passport.ts
 delete mode 100644 src/server/authentication/config/passport.ts
 delete mode 100644 src/server/authentication/controllers/user_controller.ts
 delete mode 100644 src/server/authentication/models/current_user_utils.ts
 delete mode 100644 src/server/authentication/models/user_model.ts
 delete mode 100644 src/server/credentials/CredentialsLoader.ts
 delete mode 100644 src/server/credentials/google_project_credentials.json
 create mode 100644 src/server/websocket.ts

(limited to 'src/client/util/SettingsManager.tsx')

diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
new file mode 100644
index 000000000..46d356263
--- /dev/null
+++ b/src/client/util/CurrentUserUtils.ts
@@ -0,0 +1,738 @@
+import { computed, observable, reaction } from "mobx";
+import * as rp from 'request-promise';
+import { Utils } from "../../Utils";
+import { DocServer } from "../DocServer";
+import { Docs, DocumentOptions } from "../documents/Documents";
+import { UndoManager } from "./UndoManager";
+import { Doc, DocListCast, DocListCastAsync } from "../../new_fields/Doc";
+import { List } from "../../new_fields/List";
+import { listSpec } from "../../new_fields/Schema";
+import { ScriptField, ComputedField } from "../../new_fields/ScriptField";
+import { Cast, PromiseValue, StrCast, NumCast } from "../../new_fields/Types";
+import { nullAudio } from "../../new_fields/URLField";
+import { DragManager } from "./DragManager";
+import { InkingControl } from "../views/InkingControl";
+import { Scripting } from "./Scripting";
+import { CollectionViewType } from "../views/collections/CollectionView";
+import { makeTemplate } from "./DropConverter";
+import { RichTextField } from "../../new_fields/RichTextField";
+import { PrefetchProxy } from "../../new_fields/Proxy";
+import { FormattedTextBox } from "../views/nodes/formattedText/FormattedTextBox";
+import { MainView } from "../views/MainView";
+import { DocumentType } from "../documents/DocumentTypes";
+import { SchemaHeaderField } from "../../new_fields/SchemaHeaderField";
+import { DimUnit } from "../views/collections/collectionMulticolumn/CollectionMulticolumnView";
+
+export class CurrentUserUtils {
+    private static curr_id: string;
+    //TODO tfs: these should be temporary...
+    private static mainDocId: string | undefined;
+
+    public static get id() { return this.curr_id; }
+    public static get MainDocId() { return this.mainDocId; }
+    public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
+    @computed public static get UserDocument() { return Doc.UserDoc(); }
+    @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).inkPen as Doc; }
+
+    @observable public static GuestTarget: Doc | undefined;
+    @observable public static GuestWorkspace: Doc | undefined;
+    @observable public static GuestMobile: Doc | undefined;
+
+    // sets up the default User Templates - slideView, queryView, descriptionView
+    static setupUserTemplateButtons(doc: Doc) {
+        if (doc["template-button-query"] === undefined) {
+            const queryTemplate = Docs.Create.MulticolumnDocument(
+                [
+                    Docs.Create.QueryDocument({ title: "query", _height: 200 }),
+                    Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true })
+                ],
+                { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
+            );
+            queryTemplate.isTemplateDoc = makeTemplate(queryTemplate);
+            doc["template-button-query"] = CurrentUserUtils.ficon({
+                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+                dragFactory: new PrefetchProxy(queryTemplate) as any as Doc,
+                removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle"
+            });
+        }
+
+        if (doc["template-button-slides"] === undefined) {
+            const slideTemplate = Docs.Create.MultirowDocument(
+                [
+                    Docs.Create.MulticolumnDocument([], { title: "data", _height: 200 }),
+                    Docs.Create.TextDocument("", { title: "text", _height: 100 })
+                ],
+                { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
+            );
+            slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
+            doc["template-button-slides"] = CurrentUserUtils.ficon({
+                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+                dragFactory: new PrefetchProxy(slideTemplate) as any as Doc,
+                removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card"
+            });
+        }
+
+        if (doc["template-button-description"] === undefined) {
+            const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created
+            Doc.GetProto(descriptionTemplate).layout =
+                "<div><FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" +
+                "<FormattedTextBox {...props} height='calc(100% - {this._headerHeight||75}px)' fieldKey={'text'}/></div>";
+            descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
+
+            doc["template-button-description"] = CurrentUserUtils.ficon({
+                onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'),
+                dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc,
+                removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize"
+            });
+        }
+
+        if (doc["template-button-switch"] === undefined) {
+            const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create;
+
+            const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _LODdisable: true, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40 });
+            const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1 });
+            const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, _LODdisable: true });
+            const labelTemplate = {
+                doc: {
+                    type: "doc", content: [{
+                        type: "paragraph",
+                        content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }]
+                    }]
+                },
+                selection: { type: "text", anchor: 1, head: 1 },
+                storedMarks: []
+            };
+            Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS");
+            Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'");
+            // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'");
+            // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true");
+            Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]");
+            // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false");
+            const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, });
+            box.isTemplateDoc = makeTemplate(box, true, "switch");
+
+            doc["template-button-switch"] = CurrentUserUtils.ficon({
+                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+                dragFactory: new PrefetchProxy(box) as any as Doc,
+                removeDropProperties: new List<string>(["dropAction"]), title: "data switch", icon: "toggle-on"
+            });
+        }
+
+        if (doc["template-button-detail"] === undefined) {
+            const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create;
+
+            const openInTarget = ScriptField.MakeScript("openOnRight(self.doubleClickView)");
+            const carousel = CarouselDocument([], {
+                title: "data", _height: 350, _itemIndex: 0, "_carousel-caption-xMargin": 10, "_carousel-caption-yMargin": 10,
+                onChildDoubleClick: openInTarget, backgroundColor: "#9b9b9b3F"
+            });
+
+            const details = TextDocument("", { title: "details", _height: 350, _autoHeight: true });
+            const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 100, _autoHeight: true });
+            const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 350, _autoHeight: true });
+
+            const buxtonFieldKeys = ["year", "originalPrice", "degreesOfFreedom", "company", "attribute", "primaryKey", "secondaryKey", "dimensions"];
+            const detailedTemplate = {
+                doc: {
+                    type: "doc", content: buxtonFieldKeys.map(fieldKey => ({
+                        type: "paragraph",
+                        content: [{ type: "dashField", attrs: { fieldKey } }]
+                    }))
+                },
+                selection: { type: "text", anchor: 1, head: 1 },
+                storedMarks: []
+            };
+            details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" "));
+
+            const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 };
+            const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: 12 };
+            const descriptionWrapperOpts = { title: "descriptions", _height: 300, columnWidth: -1, treeViewHideTitle: true, _pivotField: "title" };
+
+            const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts });
+            descriptionWrapper.sectionHeaders = new List<SchemaHeaderField>([
+                new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false),
+                new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true),
+                new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true),
+            ]);
+            const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts });
+            detailView.isTemplateDoc = makeTemplate(detailView);
+
+            details.title = "Details";
+            short.title = "A Short Description";
+            long.title = "Long Description";
+
+            doc["template-button-detail"] = CurrentUserUtils.ficon({
+                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
+                dragFactory: new PrefetchProxy(detailView) as any as Doc,
+                removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "window-maximize"
+            });
+        }
+
+        if (doc["template-buttons"] === undefined) {
+            doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc,
+            doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc], {
+                title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title",
+                _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
+                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+            }));
+        } else {
+            const curButnTypes = Cast(doc["template-buttons"], Doc, null);
+            const requiredTypes = [doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc,
+            doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc];
+            DocListCastAsync(curButnTypes.data).then(async curBtns => {
+                await Promise.all(curBtns!);
+                requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype));
+            });
+        }
+        return doc["template-buttons"] as Doc;
+    }
+
+    // setup the different note type skins
+    static setupNoteTemplates(doc: Doc) {
+        if (doc["template-note-Note"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Note");
+            doc["template-note-Note"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Idea"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Idea", backgroundColor: "pink" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea");
+            doc["template-note-Idea"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Topic"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Topic", backgroundColor: "lightBlue" });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic");
+            doc["template-note-Topic"] = new PrefetchProxy(noteView);
+        }
+        if (doc["template-note-Todo"] === undefined) {
+            const noteView = Docs.Create.TextDocument("", {
+                title: "text", style: "Todo", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption",
+                layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus")
+            });
+            noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo");
+            doc["template-note-Todo"] = new PrefetchProxy(noteView);
+        }
+        const taskStatusValues = [
+            { title: "todo", _backgroundColor: "blue", color: "white" },
+            { title: "in progress", _backgroundColor: "yellow", color: "black" },
+            { title: "completed", _backgroundColor: "green", color: "white" }
+        ];
+        if (doc.fieldTypes === undefined) {
+            doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
+            Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
+        }
+
+        if (doc["template-notes"] === undefined) {
+            doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc,
+            doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc],
+                { title: "Note Layouts", _height: 75 }));
+        } else {
+            const curNoteTypes = Cast(doc["template-notes"], Doc, null);
+            const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc,
+            doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc];
+            DocListCastAsync(curNoteTypes.data).then(async curNotes => {
+                await Promise.all(curNotes!);
+                requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
+            });
+        }
+
+        return doc["template-notes"] as Doc;
+    }
+
+    // creates Note templates, and initial "user" templates
+    static setupDocTemplates(doc: Doc) {
+        const noteTemplates = CurrentUserUtils.setupNoteTemplates(doc);
+        const userTemplateBtns = CurrentUserUtils.setupUserTemplateButtons(doc);
+        const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
+        if (doc.templateDocs === undefined) {
+            doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
+                title: "template layouts", _xPadding: 0,
+                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
+            }));
+        }
+    }
+
+    // setup templates for different document types when they are iconified from Document Decorations
+    static setupDefaultIconTemplates(doc: Doc) {
+        if (doc["template-icon-view"] === undefined) {
+            const iconView = Docs.Create.TextDocument("", {
+                title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+            });
+            Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
+            iconView.isTemplateDoc = makeTemplate(iconView);
+            doc["template-icon-view"] = new PrefetchProxy(iconView);
+        }
+        if (doc["template-icon-view-rtf"] === undefined) {
+            const iconRtfView = Docs.Create.LabelDocument({
+                title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset",
+                _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+            });
+            iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF);
+            doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView);
+        }
+        if (doc["template-icon-view-img"] === undefined) {
+            const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", {
+                title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
+            });
+            iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG);
+            doc["template-icon-view-img"] = new PrefetchProxy(iconImageView);
+        }
+        if (doc["template-icon-view-col"] === undefined) {
+            const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") });
+            iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL);
+            doc["template-icon-view-col"] = new PrefetchProxy(iconColView);
+        }
+        if (doc["template-icons"] === undefined) {
+            doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
+            doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 }));
+        } else {
+            const templateIconsDoc = Cast(doc["template-icons"], Doc, null);
+            const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
+            doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
+            DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
+                await Promise.all(curIcons!);
+                requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
+            });
+        }
+        return doc["template-icons"] as Doc;
+    }
+
+    static creatorBtnDescriptors(doc: Doc): {
+        title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean,
+        click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc
+    }[] {
+        if (doc.emptyPresentation === undefined) {
+            doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
+                { title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
+        }
+        if (doc.emptyCollection === undefined) {
+            doc.emptyCollection = Docs.Create.FreeformDocument([],
+                { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
+        }
+        if (doc.emptyDocHolder === undefined) {
+            doc.emptyDocHolder = Docs.Create.DocumentDocument(
+                ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
+                { _width: 250, _height: 250, title: "container" });
+        }
+        if (doc.emptyWebpage === undefined) {
+            doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600, UseCors: true });
+        }
+        return [
+            { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
+            { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
+            { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
+            { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
+            { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
+            { title: "Drag a audio recorder", label: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
+            { title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' },
+            { title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc },
+            { title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' },
+            { title: "Drag a scripting box", label: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' },
+            { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
+            { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
+            { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
+            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
+            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
+            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
+            { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
+            { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
+            { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
+        ];
+
+    }
+
+    // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
+    static async setupCreatorButtons(doc: Doc) {
+        let alreadyCreatedButtons: string[] = [];
+        const dragCreatorSet = await Cast(doc.myItemCreators, Doc, null);
+        if (dragCreatorSet) {
+            const dragCreators = await Cast(dragCreatorSet.data, listSpec(Doc));
+            if (dragCreators) {
+                const dragDocs = await Promise.all(dragCreators);
+                alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title));
+            }
+        }
+        const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
+        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
+            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
+            icon,
+            title,
+            label,
+            ignoreClick,
+            dropAction: "copy",
+            onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
+            onClick: click ? ScriptField.MakeScript(click) : undefined,
+            ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined,
+            activePen,
+            backgroundColor,
+            removeDropProperties: new List<string>(["dropAction"]),
+            dragFactory,
+        }));
+
+        if (dragCreatorSet === undefined) {
+            doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, {
+                title: "Basic Item Creators", _showTitle: "title", _xMargin: 0,
+                _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
+                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+            }));
+        } else {
+            creatorBtns.forEach(nb => Doc.AddDocToList(doc.myItemCreators as Doc, "data", nb));
+        }
+        return doc.myItemCreators as Doc;
+    }
+
+    static setupMobileButtons(doc: Doc, buttons?: string[]) {
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
+            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
+            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
+            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
+            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "red", activePen: doc },
+            { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
+            // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
+        ];
+        return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
+            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
+            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
+            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+        }));
+    }
+
+    static setupThumbButtons(doc: Doc) {
+        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
+            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
+            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
+        ];
+        return docProtoData.map(data => Docs.Create.FontIconDocument({
+            _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon,
+            dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick,
+            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
+            clipboard: data.clipboard,
+            onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
+            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
+            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
+        }));
+    }
+
+    static setupThumbDoc(userDoc: Doc) {
+        if (!userDoc.thumbDoc) {
+            const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
+                _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons",
+                _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white"
+            });
+            thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], {
+                _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column"
+            });
+            userDoc.thumbDoc = thumbDoc;
+        }
+        return Cast(userDoc.thumbDoc, Doc);
+    }
+
+    static setupMobileDoc(userDoc: Doc) {
+        return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
+            columnWidth: 100, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5
+        });
+    }
+
+    static setupMobileInkingDoc(userDoc: Doc) {
+        return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white" });
+    }
+
+    static setupMobileUploadDoc(userDoc: Doc) {
+        // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
+        const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
+            title: "Upload Images From the Web", _chromeStatus: "enabled", lockedPosition: true
+        });
+        const uploadDoc = Docs.Create.StackingDocument([], {
+            title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true
+        });
+        return Docs.Create.StackingDocument([webDoc, uploadDoc], {
+            _width: screen.width, lockedPosition: true, _chromeStatus: "disabled", title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray"
+        });
+    }
+
+    // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker. 
+    // when clicked, this panel will be displayed in the target container (ie, sidebarContainer)  
+    static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) {
+        // setup a masonry view of all he creators
+        const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc);
+        const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc);
+
+        if (doc.myCreators === undefined) {
+            doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], {
+                title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0,
+                _width: 500, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
+            }));
+        }
+        // setup a color picker
+        if (doc.myColorPicker === undefined) {
+            const color = Docs.Create.ColorDocument({
+                title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
+            });
+            doc.myColorPicker = new PrefetchProxy(color);
+        }
+
+        if (doc["tabs-button-tools"] === undefined) {
+            doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({
+                _width: 35, _height: 25, title: "Tools", _fontSize: 10,
+                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
+                sourcePanel: new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], {
+                    _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true
+                })) as any as Doc,
+                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
+                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
+            }));
+        }
+        (doc["tabs-button-tools"] as Doc).sourcePanel; // prefetch sourcePanel
+        return doc["tabs-button-tools"] as Doc;
+    }
+
+    static setupWorkspaces(doc: Doc) {
+        // setup workspaces library item
+        if (doc.myWorkspaces === undefined) {
+            doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], {
+                title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true,
+            }));
+        }
+        const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`);
+        (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]);
+        (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]);
+
+        return doc.myWorkspaces as Doc;
+    }
+    static setupCatalog(doc: Doc) {
+        if (doc.myCatalog === undefined) {
+            doc.myCatalog = new PrefetchProxy(Docs.Create.TreeDocument([], {
+                title: "CATALOG", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true,
+            }));
+        }
+        return doc.myCatalog as Doc;
+    }
+    static setupRecentlyClosed(doc: Doc) {
+        // setup Recently Closed library item
+        if (doc.myRecentlyClosed === undefined) {
+            doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], {
+                title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true,
+            }));
+        }
+        // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready
+        PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast));
+        const clearAll = ScriptField.MakeScript(`self.data = new List([])`);
+        (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]);
+        (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]);
+
+        return doc.myRecentlyClosed as Doc;
+    }
+    // setup the Library button which will display the library panel.  This panel includes a collection of workspaces, documents, and recently closed views
+    static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) {
+        const workspaces = CurrentUserUtils.setupWorkspaces(doc);
+        const documents = CurrentUserUtils.setupCatalog(doc);
+        const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc);
+
+        if (doc["tabs-button-library"] === undefined) {
+            doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({
+                _width: 50, _height: 25, title: "Library", _fontSize: 10,
+                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
+                sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], {
+                    title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true
+                })) as any as Doc,
+                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
+                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
+            }));
+        }
+        return doc["tabs-button-library"] as Doc;
+    }
+
+    // setup the Search button which will display the search panel.  
+    static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) {
+        if (doc["tabs-button-search"] === undefined) {
+            doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({
+                _width: 50, _height: 25, title: "Search", _fontSize: 10,
+                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
+                sourcePanel: new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc,
+                searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]),
+                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
+                lockedPosition: true,
+                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
+            }));
+        }
+        return doc["tabs-button-search"] as Doc;
+    }
+
+    static setupSidebarContainer(doc: Doc) {
+        if (doc["tabs-panelContainer"] === undefined) {
+            const sidebarContainer = new Doc();
+            sidebarContainer._chromeStatus = "disabled";
+            sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()");
+            doc["tabs-panelContainer"] = new PrefetchProxy(sidebarContainer);
+        }
+        return doc["tabs-panelContainer"] as Doc;
+    }
+
+    // setup the list of sidebar mode buttons which determine what is displayed in the sidebar
+    static async setupSidebarButtons(doc: Doc) {
+        const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc);
+        const toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer);
+        const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer);
+        const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer);
+
+        // Finally, setup the list of buttons to display in the sidebar
+        if (doc["tabs-buttons"] === undefined) {
+            doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([searchBtn, libraryBtn, toolsBtn], {
+                _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", hideHeadings: true, ignoreClick: true, _chromeStatus: "view-mode",
+                title: "sidebar btn row stack", backgroundColor: "dimGray",
+            }));
+            (toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn });
+        }
+    }
+
+    static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, {
+        ...opts,
+        _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true,
+        dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
+        backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true
+    })) as any as Doc
+
+    static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({
+        ...opts,
+        dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100
+    })) as any as Doc
+
+    /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
+    static setupDockedButtons(doc: Doc) {
+        if (doc["dockedBtn-pen"] === undefined) {
+            doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
+                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)"),
+                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
+            });
+        }
+        if (doc["dockedBtn-undo"] === undefined) {
+            doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
+        }
+        if (doc["dockedBtn-redo"] === undefined) {
+            doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
+        }
+        if (doc.dockedBtns === undefined) {
+            doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc, doc["dockedBtn-pen"] as Doc]);
+        }
+    }
+    // sets up the default set of documents to be shown in the Overlay layer
+    static setupOverlays(doc: Doc) {
+        if (doc.myOverlayDocuments === undefined) {
+            doc.myOverlayDocuments = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6" }));
+        }
+    }
+
+    // the initial presentation Doc to use
+    static setupDefaultPresentation(doc: Doc) {
+        if (doc.activePresentation === undefined) {
+            doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), {
+                title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias",
+                _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0"
+            });
+        }
+    }
+
+    static setupRightSidebar(doc: Doc) {
+        if (doc.rightSidebarCollection === undefined) {
+            doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Right Sidebar" }));
+        }
+    }
+
+    static setupClickEditorTemplates(doc: Doc) {
+        if (doc["clickFuncs-child"] === undefined) {
+            const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
+                "docCast(thisContainer.target).then((target) => {" +
+                "     target && docCast(this.source).then((source) => { " +
+                "           target.proto.data = new List([source || this]); " +
+                "      }); " +
+                "})",
+                { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" });
+
+            const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
+                "openOnRight(self.doubleClickView)",
+                { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" });
+
+            doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" });
+        }
+        // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved.
+        PromiseValue(Cast(doc["clickFuncs-child"], Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
+
+        if (doc.clickFuncs === undefined) {
+            const onClick = Docs.Create.ScriptingDocument(undefined, {
+                title: "onClick", "onClick-rawScript": "console.log('click')",
+                isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200
+            }, "onClick");
+            const onDoubleClick = Docs.Create.ScriptingDocument(undefined, {
+                title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')",
+                isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200
+            }, "onDoubleClick");
+            const onCheckedClick = Docs.Create.ScriptingDocument(undefined, {
+                title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200
+            }, "onCheckedClick");
+            doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" });
+        }
+        PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
+
+        return doc.clickFuncs as Doc;
+    }
+
+    static async updateUserDocument(doc: Doc) {
+        new InkingControl();
+        doc.title = Doc.CurrentUserEmail;
+        doc.activePen = doc;
+        doc.inkColor = StrCast(doc.backgroundColor, "");
+        doc.fontSize = NumCast(doc.fontSize, 12);
+        doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
+        doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
+        Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
+        this.setupDefaultIconTemplates(doc);  // creates a set of icon templates triggered by the document deoration icon
+        this.setupDocTemplates(doc); // sets up the template menu of templates
+        this.setupRightSidebar(doc);  // sets up the right sidebar collection for mobile upload documents and sharing
+        this.setupOverlays(doc);  // documents in overlay layer 
+        this.setupDockedButtons(doc);  // the bottom bar of font icons
+        this.setupDefaultPresentation(doc); // presentation that's initially triggered
+        await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
+        doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument();
+
+        // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
+        doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
+        doc["dockedBtn-redo"] && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc["dockedBtn-redo"] as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
+
+        return doc;
+    }
+    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;
+            } else {
+                throw new Error("There should be a user! Why does Dash think there isn't one?");
+            }
+        });
+    }
+
+    public static async loadUserDocument({ id, email }: { id: string, email: string }) {
+        this.curr_id = id;
+        Doc.CurrentUserEmail = email;
+        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))));
+            } else {
+                throw new Error("There should be a user id! Why does Dash think there isn't one?");
+            }
+        });
+    }
+}
+
+Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); });
+Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); });
+Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); });
\ No newline at end of file
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index e20434461..0e15197c4 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -8,7 +8,7 @@ import { SelectionManager } from "./SelectionManager";
 import "./SettingsManager.scss";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { Networking } from "../Network";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "./CurrentUserUtils";
 import { Utils } from "../../Utils";
 
 library.add(fa.faWindowClose);
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 4f8f9ed69..812ff3b93 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -13,7 +13,7 @@ import { DocUtils, Docs } from "../documents/Documents";
 import { undoBatch } from "../util/UndoManager";
 import { Scripting } from "../util/Scripting";
 import { FieldValue, Cast, NumCast, BoolCast } from "../../new_fields/Types";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import HorizontalPalette from "./Palette";
 import { Utils, emptyPath, emptyFunction, returnFalse, returnOne, returnEmptyString, returnTrue, numberRange, returnZero } from "../../Utils";
 import { DocumentView } from "./nodes/DocumentView";
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 81d99e009..af9c4a562 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -3,7 +3,7 @@ import { ColorState } from 'react-color';
 import { Doc } from "../../new_fields/Doc";
 import { InkTool } from "../../new_fields/InkField";
 import { FieldValue, NumCast, StrCast } from "../../new_fields/Types";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import { Scripting } from "../util/Scripting";
 import { SelectionManager } from "../util/SelectionManager";
 import { undoBatch } from "../util/UndoManager";
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index b21eb9c8f..17c001971 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,6 +1,6 @@
 import { MainView } from "./MainView";
 import { Docs } from "../documents/Documents";
-import { CurrentUserUtils } from "../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../util/CurrentUserUtils";
 import * as ReactDOM from 'react-dom';
 import * as React from 'react';
 import { DocServer } from "../DocServer";
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 9bfef06b4..9bc08de3e 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -19,7 +19,7 @@ import { List } from '../../new_fields/List';
 import { listSpec } from '../../new_fields/Schema';
 import { BoolCast, Cast, FieldValue, StrCast } from '../../new_fields/Types';
 import { TraceMobx } from '../../new_fields/util';
-import { CurrentUserUtils } from '../../server/authentication/models/current_user_utils';
+import { CurrentUserUtils } from '../util/CurrentUserUtils';
 import { emptyFunction, emptyPath, returnFalse, returnOne, returnZero, returnTrue, Utils } from '../../Utils';
 import GoogleAuthenticationManager from '../apis/GoogleAuthenticationManager';
 import { DocServer } from '../DocServer';
diff --git a/src/client/views/collections/CollectionMapView.tsx b/src/client/views/collections/CollectionMapView.tsx
index 13c06b0a3..618285293 100644
--- a/src/client/views/collections/CollectionMapView.tsx
+++ b/src/client/views/collections/CollectionMapView.tsx
@@ -1,4 +1,4 @@
-import { GoogleApiWrapper, Map as GeoMap, IMapProps, Marker } from "google-maps-react";
+import { GoogleApiWrapper, Map as GeoMap, MapProps as IMapProps, Marker } from "google-maps-react";
 import { observer } from "mobx-react";
 import { Doc, Opt, DocListCast, FieldResult, Field } from "../../../new_fields/Doc";
 import { documentSchema } from "../../../new_fields/documentSchemas";
@@ -47,7 +47,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps>
     private _cancelAddrReq = new Map<string, boolean>();
     private _cancelLocReq = new Map<string, boolean>();
     private _initialLookupPending = new Map<string, boolean>();
-    private responders: { location: Lambda, address: Lambda }[] = [];
+    private responders: { locationDisposer: Lambda, addressDisposer: Lambda }[] = [];
 
     /**
      * Note that all the uses of runInAction below are not included
@@ -176,13 +176,16 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps>
     }
 
     @computed get reactiveContents() {
-        this.responders.forEach(({ location, address }) => { location(); address(); });
+        this.responders.forEach(({ locationDisposer, addressDisposer }) => {
+            locationDisposer();
+            addressDisposer();
+        });
         this.responders = [];
         return this.childLayoutPairs.map(({ layout }) => {
             const fieldKey = Doc.LayoutFieldKey(layout);
             const id = layout[Id];
             this.responders.push({
-                location: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] }))
+                locationDisposer: computed(() => ({ lat: layout[`${fieldKey}-lat`], lng: layout[`${fieldKey}-lng`] }))
                     .observe(({ oldValue, newValue }) => {
                         if (this._cancelLocReq.get(id)) {
                             this._cancelLocReq.set(id, false);
@@ -190,7 +193,7 @@ class CollectionMapView extends CollectionSubView<MapSchema, Partial<IMapProps>
                             this.respondToLocationChange(layout, fieldKey, newValue, oldValue);
                         }
                     }),
-                address: computed(() => Cast(layout[`${fieldKey}-address`], "string", null))
+                addressDisposer: computed(() => Cast(layout[`${fieldKey}-address`], "string", null))
                     .observe(({ oldValue, newValue }) => {
                         if (this._cancelAddrReq.get(id)) {
                             this._cancelAddrReq.set(id, false);
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 8b50bd8fc..bff6d121b 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -8,7 +8,7 @@ import { listSpec } from "../../../new_fields/Schema";
 import { ScriptField } from "../../../new_fields/ScriptField";
 import { Cast, ScriptCast } from "../../../new_fields/Types";
 import { GestureUtils } from "../../../pen-gestures/GestureUtils";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { Upload } from "../../../server/SharedMediaTypes";
 import { Utils } from "../../../Utils";
 import { GooglePhotos } from "../../apis/google_docs/GooglePhotosClientUtils";
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index 0f239d385..4b3b2e234 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -35,7 +35,7 @@ import { CollectionTimeView } from './CollectionTimeView';
 import { CollectionTreeView } from "./CollectionTreeView";
 import './CollectionView.scss';
 import { CollectionViewBaseChrome } from './CollectionViewChromes';
-import { CurrentUserUtils } from '../../../server/authentication/models/current_user_utils';
+import { CurrentUserUtils } from '../../util/CurrentUserUtils';
 import { Id } from '../../../new_fields/FieldSymbols';
 import { listSpec } from '../../../new_fields/Schema';
 import { Docs } from '../../documents/Documents';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
index 92fa2781c..9a5b2c27c 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormRemoteCursors.tsx
@@ -3,7 +3,7 @@ import * as mobxUtils from 'mobx-utils';
 import CursorField from "../../../../new_fields/CursorField";
 import { listSpec } from "../../../../new_fields/Schema";
 import { Cast } from "../../../../new_fields/Types";
-import { CurrentUserUtils } from "../../../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../../../util/CurrentUserUtils";
 import { CollectionViewProps } from "../CollectionSubView";
 import "./CollectionFreeFormView.scss";
 import React = require("react");
diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx
index 7ab6d99c2..bc84204b9 100644
--- a/src/client/views/nodes/ColorBox.tsx
+++ b/src/client/views/nodes/ColorBox.tsx
@@ -4,7 +4,7 @@ import { SketchPicker } from 'react-color';
 import { documentSchema } from "../../../new_fields/documentSchemas";
 import { makeInterface } from "../../../new_fields/Schema";
 import { StrCast } from "../../../new_fields/Types";
-import { CurrentUserUtils } from "../../../server/authentication/models/current_user_utils";
+import { CurrentUserUtils } from "../../util/CurrentUserUtils";
 import { SelectionManager } from "../../util/SelectionManager";
 import { ViewBoxBaseComponent } from "../DocComponent";
 import { InkingControl } from "../InkingControl";
diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx
index 295e82142..f30e9869a 100644
--- a/src/mobile/ImageUpload.tsx
+++ b/src/mobile/ImageUpload.tsx
@@ -12,8 +12,7 @@ import { observer } from 'mobx-react';
 import { observable } from 'mobx';
 import { Utils } from '../Utils';
 import MobileInterface from './MobileInterface';
-import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
-import { Scripting } from '../client/util/Scripting';
+import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
 
 
 
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 69a80e1b4..9d4d58ad1 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -10,7 +10,6 @@ import { DocumentManager } from '../client/util/DocumentManager';
 import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu';
 import { Scripting } from '../client/util/Scripting';
 import { Transform } from '../client/util/Transform';
-import { CollectionView } from '../client/views/collections/CollectionView';
 import { DocumentDecorations } from '../client/views/DocumentDecorations';
 import GestureOverlay from '../client/views/GestureOverlay';
 import { InkingControl } from '../client/views/InkingControl';
@@ -23,9 +22,10 @@ import { InkTool } from '../new_fields/InkField';
 import { listSpec } from '../new_fields/Schema';
 import { Cast, FieldValue } from '../new_fields/Types';
 import { WebField } from "../new_fields/URLField";
-import { CurrentUserUtils } from '../server/authentication/models/current_user_utils';
+import { CurrentUserUtils } from '../client/util/CurrentUserUtils';
 import { emptyFunction, emptyPath, returnEmptyString, returnFalse, returnOne, returnTrue, returnZero } from '../Utils';
 import "./MobileInterface.scss";
+import { CollectionView } from '../client/views/collections/CollectionView';
 
 library.add(faLongArrowAltLeft);
 
diff --git a/src/server/ApiManagers/ApiManager.ts b/src/server/ApiManagers/ApiManager.ts
index e2b01d585..27e9de065 100644
--- a/src/server/ApiManagers/ApiManager.ts
+++ b/src/server/ApiManagers/ApiManager.ts
@@ -1,4 +1,4 @@
-import RouteManager, { RouteInitializer } from "../RouteManager";
+import { RouteInitializer } from "../RouteManager";
 
 export type Registration = (initializer: RouteInitializer) => void;
 
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts
index bd80d6500..7fbb37658 100644
--- a/src/server/ApiManagers/DeleteManager.ts
+++ b/src/server/ApiManagers/DeleteManager.ts
@@ -1,6 +1,6 @@
 import ApiManager, { Registration } from "./ApiManager";
 import { Method, _permission_denied } from "../RouteManager";
-import { WebSocket } from "../Websocket/Websocket";
+import { WebSocket } from "../websocket";
 import { Database } from "../database";
 import rimraf = require("rimraf");
 import { filesDirectory } from "..";
diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts
index 17968cc7d..f94b77cac 100644
--- a/src/server/ApiManagers/GeneralGoogleManager.ts
+++ b/src/server/ApiManagers/GeneralGoogleManager.ts
@@ -38,7 +38,7 @@ export default class GeneralGoogleManager extends ApiManager {
             method: Method.GET,
             subscription: "/revokeGoogleAccessToken",
             secureHandler: async ({ user, res }) => {
-                await Database.Auxiliary.GoogleAuthenticationToken.Revoke(user.id);
+                await Database.Auxiliary.GoogleAccessToken.Revoke(user.id);
                 res.send();
             }
         });
diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts
index ef9b88541..ab3dfffcc 100644
--- a/src/server/DashSession/DashSessionAgent.ts
+++ b/src/server/DashSession/DashSessionAgent.ts
@@ -2,7 +2,7 @@ import { Email, pathFromRoot } from "../ActionUtilities";
 import { red, yellow, green, cyan } from "colors";
 import { get } from "request-promise";
 import { Utils } from "../../Utils";
-import { WebSocket } from "../Websocket/Websocket";
+import { WebSocket } from "../websocket";
 import { MessageStore } from "../Message";
 import { launchServer, onWindows } from "..";
 import { readdirSync, statSync, createWriteStream, readFileSync, unlinkSync } from "fs";
diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts
index 80e4a6741..b23215996 100644
--- a/src/server/RouteManager.ts
+++ b/src/server/RouteManager.ts
@@ -1,5 +1,5 @@
 import RouteSubscriber from "./RouteSubscriber";
-import { DashUserModel } from "./authentication/models/user_model";
+import { DashUserModel } from "./authentication/DashUserModel";
 import { Request, Response, Express } from 'express';
 import { cyan, red, green } from 'colors';
 
diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts
deleted file mode 100644
index 844535056..000000000
--- a/src/server/Websocket/Websocket.ts
+++ /dev/null
@@ -1,305 +0,0 @@
-import { Utils } from "../../Utils";
-import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } 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 { logPort } from "../ActionUtilities";
-import { timeMap } from "../ApiManagers/UserManager";
-import { green } from "colors";
-import { networkInterfaces } from "os";
-import executeImport from "../../scraping/buxton/final/BuxtonImporter";
-import { DocumentsCollection } from "../IDatabase";
-
-export namespace WebSocket {
-
-    export let _socket: Socket;
-    const clients: { [key: string]: Client } = {};
-    export const socketMap = new Map<SocketIO.Socket, string>();
-    export let disconnect: Function;
-
-
-    export async function start(isRelease: boolean) {
-        await preliminaryFunctions();
-        initialize(isRelease);
-    }
-
-    async function preliminaryFunctions() {
-    }
-    function initialize(isRelease: boolean) {
-        const endpoint = io();
-        endpoint.on("connection", function (socket: Socket) {
-            _socket = socket;
-
-            socket.use((_packet, next) => {
-                const userEmail = socketMap.get(socket);
-                if (userEmail) {
-                    timeMap[userEmail] = Date.now();
-                }
-                next();
-            });
-
-            // convenience function to log server messages on the client
-            function log(message?: any, ...optionalParams: any[]) {
-                socket.emit('log', ['Message from server:', message, ...optionalParams]);
-            }
-
-            socket.on('message', function (message, room) {
-                console.log('Client said: ', message);
-                socket.in(room).emit('message', message);
-            });
-
-            socket.on('create or join', function (room) {
-                console.log('Received request to create or join room ' + room);
-
-                const clientsInRoom = socket.adapter.rooms[room];
-                const numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
-                console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
-
-                if (numClients === 0) {
-                    socket.join(room);
-                    console.log('Client ID ' + socket.id + ' created room ' + room);
-                    socket.emit('created', room, socket.id);
-
-                } else if (numClients === 1) {
-                    console.log('Client ID ' + socket.id + ' joined room ' + room);
-                    socket.in(room).emit('join', room);
-                    socket.join(room);
-                    socket.emit('joined', room, socket.id);
-                    socket.in(room).emit('ready');
-                } else { // max two clients
-                    socket.emit('full', room);
-                }
-            });
-
-            socket.on('ipaddr', function () {
-                const ifaces = networkInterfaces();
-                for (const dev in ifaces) {
-                    ifaces[dev].forEach(function (details) {
-                        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
-                            socket.emit('ipaddr', details.address);
-                        }
-                    });
-                }
-            });
-
-            socket.on('bye', function () {
-                console.log('received bye');
-            });
-
-            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, () => doDelete(false));
-            }
-
-            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.AddServerHandler(socket, MessageStore.GesturePoints, content => processGesturePoints(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.MobileInkOverlayTrigger, content => processOverlayTrigger(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.UpdateMobileInkOverlayPosition, content => processUpdateOverlayPosition(socket, content));
-            Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
-            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
-
-            /**
-             * Whenever we receive the go-ahead, invoke the import script and pass in
-             * as an emitter and a terminator the functions that simply broadcast a result
-             * or indicate termination to the client via the web socket
-             */
-            Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => {
-                executeImport(
-                    deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError),
-                    results => Utils.Emit(socket, MessageStore.BuxtonImportComplete, results)
-                );
-            });
-
-            disconnect = () => {
-                socket.broadcast.emit("connection_terminated", Date.now());
-                socket.disconnect(true);
-            };
-        });
-
-        const socketPort = isRelease ? Number(process.env.socketPort) : 4321;
-        endpoint.listen(socketPort);
-        logPort("websocket", socketPort);
-    }
-
-    function processGesturePoints(socket: Socket, content: GestureContent) {
-        socket.broadcast.emit("receiveGesturePoints", content);
-    }
-
-    function processOverlayTrigger(socket: Socket, content: MobileInkOverlayContent) {
-        socket.broadcast.emit("receiveOverlayTrigger", content);
-    }
-
-    function processUpdateOverlayPosition(socket: Socket, content: UpdateMobileInkOverlayPositionContent) {
-        socket.broadcast.emit("receiveUpdateOverlayPosition", content);
-    }
-
-    function processMobileDocumentUpload(socket: Socket, content: MobileDocumentUploadContent) {
-        socket.broadcast.emit("receiveMobileDocumentUpload", content);
-    }
-
-    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 doDelete(onlyFields = true) {
-        const target: string[] = [];
-        onlyFields && target.push(DocumentsCollection);
-        await Database.Instance.dropSchema(...target);
-        if (process.env.DISABLE_SEARCH !== "true") {
-            await Search.clear();
-        }
-    }
-
-    function barReceived(socket: SocketIO.Socket, userEmail: string) {
-        clients[userEmail] = new Client(userEmail.toString());
-        console.log(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) {  // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR
-            Search.updateDocument({ id: newValue.id, data: (newValue as any).data });
-        }
-    }
-
-    function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
-        Database.Instance.getDocument(id, callback);
-    }
-
-    function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
-        Database.Instance.getDocuments(ids, callback);
-    }
-
-    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"],
-        "script": ["_t", value => value.script.originalScript],
-        "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);
-        const docfield = diff.diff.$set || diff.diff.$unset;
-        if (!docfield) {
-            return;
-        }
-        const update: any = { id: diff.id };
-        let dynfield = false;
-        for (let key in docfield) {
-            if (!key.startsWith("fields.")) continue;
-            dynfield = true;
-            const val = docfield[key];
-            key = key.substring(7);
-            Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
-            const term = ToSearchTerm(val);
-            if (term !== undefined) {
-                const { suffix, value } = term;
-                update[key + suffix] = { set: value };
-            }
-        }
-        if (dynfield) {
-            Search.updateDocument(update);
-        }
-    }
-
-    function DeleteField(socket: Socket, id: string) {
-        Database.Instance.delete({ _id: id }).then(() => {
-            socket.broadcast.emit(MessageStore.DeleteField.Message, id);
-        });
-
-        Search.deleteDocuments([id]);
-    }
-
-    function DeleteFields(socket: Socket, ids: string[]) {
-        Database.Instance.delete({ _id: { $in: ids } }).then(() => {
-            socket.broadcast.emit(MessageStore.DeleteFields.Message, ids);
-        });
-        Search.deleteDocuments(ids);
-    }
-
-    function CreateField(newValue: any) {
-        Database.Instance.insert(newValue);
-    }
-
-}
-
diff --git a/src/server/apis/google/CredentialsLoader.ts b/src/server/apis/google/CredentialsLoader.ts
new file mode 100644
index 000000000..e3f4d167b
--- /dev/null
+++ b/src/server/apis/google/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/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts
index 48a8da89f..2e4811c86 100644
--- a/src/server/apis/google/GoogleApiServerUtils.ts
+++ b/src/server/apis/google/GoogleApiServerUtils.ts
@@ -3,9 +3,9 @@ import { OAuth2Client, Credentials, OAuth2ClientOptions } from "google-auth-libr
 import { Opt } from "../../../new_fields/Doc";
 import { GaxiosResponse } from "gaxios";
 import request = require('request-promise');
-import * as qs from 'query-string';
+import * as qs from "query-string";
 import { Database } from "../../database";
-import { GoogleCredentialsLoader } from "../../credentials/CredentialsLoader";
+import { GoogleCredentialsLoader } from "./CredentialsLoader";
 
 /**
  * Scopes give Google users fine granularity of control
@@ -224,7 +224,7 @@ export namespace GoogleApiServerUtils {
             });
         });
         const enriched = injectUserInfo(credentials);
-        await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched);
+        await Database.Auxiliary.GoogleAccessToken.Write(userId, enriched);
         return enriched;
     }
 
@@ -280,7 +280,7 @@ export namespace GoogleApiServerUtils {
      * and a flag indicating whether or not they were refreshed during retrieval
      */
     export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<EnrichedCredentials>, refreshed: boolean }> {
-        let credentials = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId);
+        let credentials = await Database.Auxiliary.GoogleAccessToken.Fetch(userId);
         let refreshed = false;
         if (!credentials) {
             return { credentials: undefined, refreshed };
@@ -318,7 +318,7 @@ export namespace GoogleApiServerUtils {
         });
         // 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);
+        await Database.Auxiliary.GoogleAccessToken.Update(userId, access_token, expiry_date);
         // update the relevant properties
         credentials.access_token = access_token;
         credentials.expiry_date = expiry_date;
diff --git a/src/server/apis/google/google_project_credentials.json b/src/server/apis/google/google_project_credentials.json
new file mode 100644
index 000000000..955c5a3c1
--- /dev/null
+++ b/src/server/apis/google/google_project_credentials.json
@@ -0,0 +1,11 @@
+{
+    "installed": {
+        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
+        "project_id": "quickstart-1565056383187",
+        "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": "w8KIFSc0MQpmUYHed4qEzn8b",
+        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
+    }
+}
\ No newline at end of file
diff --git a/src/server/apis/youtube/youtubeApiSample.js b/src/server/apis/youtube/youtubeApiSample.js
index 50b3c7b38..d535bd9ff 100644
--- a/src/server/apis/youtube/youtubeApiSample.js
+++ b/src/server/apis/youtube/youtubeApiSample.js
@@ -1,6 +1,8 @@
 const fs = require('fs');
 const readline = require('readline');
-const { google } = require('googleapis');
+const {
+    google
+} = require('googleapis');
 const OAuth2 = google.auth.OAuth2;
 
 
@@ -19,21 +21,27 @@ module.exports.readApiKey = (callback) => {
         }
         callback(content);
     });
-}
+};
 
 module.exports.authorizedGetChannel = (apiKey) => {
     //this didnt get called
     // Authorize a client with the loaded credentials, then call the YouTube API.
     authorize(JSON.parse(apiKey), getChannel);
-}
+};
 
 module.exports.authorizedGetVideos = (apiKey, userInput, callBack) => {
-    authorize(JSON.parse(apiKey), getVideos, { userInput: userInput, callBack: callBack });
-}
+    authorize(JSON.parse(apiKey), getVideos, {
+        userInput: userInput,
+        callBack: callBack
+    });
+};
 
 module.exports.authorizedGetVideoDetails = (apiKey, videoIds, callBack) => {
-    authorize(JSON.parse(apiKey), getVideoDetails, { videoIds: videoIds, callBack: callBack });
-}
+    authorize(JSON.parse(apiKey), getVideoDetails, {
+        videoIds: videoIds,
+        callBack: callBack
+    });
+};
 
 
 /**
diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts
new file mode 100644
index 000000000..00f1fe44e
--- /dev/null
+++ b/src/server/authentication/AuthenticationManager.ts
@@ -0,0 +1,268 @@
+import { default as User, DashUserModel } from "./DashUserModel";
+import { Request, Response, NextFunction } from "express";
+import * as passport from "passport";
+import { IVerifyOptions } from "passport-local";
+import "./Passport";
+import flash = require("express-flash");
+import * as async from 'async';
+import * as nodemailer from 'nodemailer';
+import c = require("crypto");
+import { Utils } from "../../Utils";
+import { MailOptions } from "nodemailer/lib/stream-transport";
+
+/**
+ * GET /signup
+ * Directs user to the signup page
+ * modeled by signup.pug in views
+ */
+export let getSignup = (req: Request, res: Response) => {
+    if (req.user) {
+        return res.redirect("/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) {
+        return res.redirect("/signup");
+    }
+
+    const email = req.body.email as String;
+    const password = req.body.password;
+
+    const model = {
+        email,
+        password,
+        userDocumentId: Utils.GenerateGuid()
+    } as Partial<DashUserModel>;
+
+    const user = new User(model);
+
+    User.findOne({ email }, (err, existingUser) => {
+        if (err) { return next(err); }
+        if (existingUser) {
+            return res.redirect("/login");
+        }
+        user.save((err: any) => {
+            if (err) { return next(err); }
+            req.logIn(user, (err) => {
+                if (err) { return next(err); }
+                tryRedirectToTarget(req, res);
+            });
+        });
+    });
+
+};
+
+const tryRedirectToTarget = (req: Request, res: Response) => {
+    if (req.session && req.session.target) {
+        const target = req.session.target;
+        req.session.target = undefined;
+        res.redirect(target);
+    } else {
+        res.redirect("/home");
+    }
+};
+
+
+/**
+ * GET /login
+ * Login page.
+ */
+export let getLogin = (req: Request, res: Response) => {
+    if (req.user) {
+        req.session!.target = undefined;
+        return res.redirect("/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("/signup");
+    }
+
+    passport.authenticate("local", (err: Error, user: DashUserModel, _info: IVerifyOptions) => {
+        if (err) { next(err); return; }
+        if (!user) {
+            return res.redirect("/signup");
+        }
+        req.logIn(user, (err) => {
+            if (err) { next(err); return; }
+            tryRedirectToTarget(req, res);
+        });
+    })(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("/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) {
+            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
+                    res.redirect("/forgotPassword");
+                    return;
+                }
+                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 + '/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) {
+                // 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("/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("/forgotPassword");
+        }
+        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) {
+                        res.redirect("/login");
+                        return;
+                    }
+                    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'
+            } as MailOptions;
+            smtpTransport.sendMail(mailOptions, function (err) {
+                done(null, err);
+            });
+        }
+    ], function (err) {
+        res.redirect("/login");
+    });
+};
\ No newline at end of file
diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts
new file mode 100644
index 000000000..51d920a8f
--- /dev/null
+++ b/src/server/authentication/DashUserModel.ts
@@ -0,0 +1,86 @@
+//@ts-ignore
+import * as bcrypt from "bcrypt-nodejs";
+//@ts-ignore
+import * as mongoose from 'mongoose';
+
+export type DashUserModel = mongoose.Document & {
+    email: String,
+    password: string,
+    passwordResetToken?: string,
+    passwordResetExpires?: Date,
+
+    userDocumentId: string;
+
+    profile: {
+        name: string,
+        gender: string,
+        location: string,
+        website: string,
+        picture: string
+    },
+
+    comparePassword: comparePasswordFunction,
+};
+
+type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void;
+
+export type AuthToken = {
+    accessToken: string,
+    kind: string
+};
+
+const userSchema = new mongoose.Schema({
+    email: String,
+    password: String,
+    passwordResetToken: String,
+    passwordResetExpires: Date,
+
+    userDocumentId: String,
+
+    facebook: String,
+    twitter: String,
+    google: String,
+
+    profile: {
+        name: String,
+        gender: String,
+        location: String,
+        website: String,
+        picture: String
+    }
+}, { timestamps: true });
+
+/**
+ * Password hash middleware.
+ */
+userSchema.pre("save", function save(next) {
+    const user = this as DashUserModel;
+    if (!user.isModified("password")) {
+        return next();
+    }
+    bcrypt.genSalt(10, (err: any, salt: string) => {
+        if (err) {
+            return next(err);
+        }
+        bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash: string) => {
+            if (err) {
+                return next(err);
+            }
+            user.password = hash;
+            next();
+        });
+    });
+});
+
+const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) {
+    // Choose one of the following bodies for authentication logic.
+    // secure (expected, default)
+    bcrypt.compare(candidatePassword, this.password, cb);
+    // bypass password (debugging)
+    // cb(undefined, true);
+};
+
+userSchema.methods.comparePassword = comparePassword;
+
+const User = mongoose.model("User", userSchema);
+export default User;
\ No newline at end of file
diff --git a/src/server/authentication/Passport.ts b/src/server/authentication/Passport.ts
new file mode 100644
index 000000000..9b0069414
--- /dev/null
+++ b/src/server/authentication/Passport.ts
@@ -0,0 +1,29 @@
+import * as passport from 'passport';
+import * as passportLocal from 'passport-local';
+import { default as User } from './DashUserModel';
+
+const LocalStrategy = passportLocal.Strategy;
+
+passport.serializeUser<any, any>((user, done) => {
+    done(undefined, user.id);
+});
+
+passport.deserializeUser<any, any>((id, done) => {
+    User.findById(id, (err, user) => {
+        done(err, user);
+    });
+});
+
+// AUTHENTICATE JUST WITH EMAIL AND PASSWORD
+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
+        user.comparePassword(password, (error: Error, isMatch: boolean) => {
+            if (error) return done(error);
+            if (!isMatch) return done(undefined, false, { message: "Invalid email or password" }); // invalid password
+            // valid authentication HERE
+            return done(undefined, user);
+        });
+    });
+}));
\ No newline at end of file
diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts
deleted file mode 100644
index 286209b20..000000000
--- a/src/server/authentication/config/passport.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import * as passport from 'passport';
-import * as passportLocal from 'passport-local';
-import { default as User } from '../models/user_model';
-
-const LocalStrategy = passportLocal.Strategy;
-
-passport.serializeUser<any, any>((user, done) => {
-    done(undefined, user.id);
-});
-
-passport.deserializeUser<any, any>((id, done) => {
-    User.findById(id, (err, user) => {
-        done(err, user);
-    });
-});
-
-// AUTHENTICATE JUST WITH EMAIL AND PASSWORD
-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
-        user.comparePassword(password, (error: Error, isMatch: boolean) => {
-            if (error) return done(error);
-            if (!isMatch) return done(undefined, false, { message: "Invalid email or password" }); // invalid password
-            // valid authentication HERE
-            return done(undefined, user);
-        });
-    });
-}));
\ No newline at end of file
diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts
deleted file mode 100644
index f0086d4ea..000000000
--- a/src/server/authentication/controllers/user_controller.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-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 flash = require("express-flash");
-import * as async from 'async';
-import * as nodemailer from 'nodemailer';
-import c = require("crypto");
-import { Utils } from "../../../Utils";
-import { MailOptions } from "nodemailer/lib/stream-transport";
-
-/**
- * GET /signup
- * Directs user to the signup page
- * modeled by signup.pug in views
- */
-export let getSignup = (req: Request, res: Response) => {
-    if (req.user) {
-        return res.redirect("/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) {
-        return res.redirect("/signup");
-    }
-
-    const email = req.body.email as String;
-    const password = req.body.password;
-
-    const model = {
-        email,
-        password,
-        userDocumentId: Utils.GenerateGuid()
-    } as Partial<DashUserModel>;
-
-    const user = new User(model);
-
-    User.findOne({ email }, (err, existingUser) => {
-        if (err) { return next(err); }
-        if (existingUser) {
-            return res.redirect("/login");
-        }
-        user.save((err: any) => {
-            if (err) { return next(err); }
-            req.logIn(user, (err) => {
-                if (err) { return next(err); }
-                tryRedirectToTarget(req, res);
-            });
-        });
-    });
-
-};
-
-const tryRedirectToTarget = (req: Request, res: Response) => {
-    if (req.session && req.session.target) {
-        const target = req.session.target;
-        req.session.target = undefined;
-        res.redirect(target);
-    } else {
-        res.redirect("/home");
-    }
-};
-
-
-/**
- * GET /login
- * Login page.
- */
-export let getLogin = (req: Request, res: Response) => {
-    if (req.user) {
-        req.session!.target = undefined;
-        return res.redirect("/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("/signup");
-    }
-
-    passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => {
-        if (err) { next(err); return; }
-        if (!user) {
-            return res.redirect("/signup");
-        }
-        req.logIn(user, (err) => {
-            if (err) { next(err); return; }
-            tryRedirectToTarget(req, res);
-        });
-    })(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("/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) {
-            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
-                    res.redirect("/forgotPassword");
-                    return;
-                }
-                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 + '/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) {
-                // 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("/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("/forgotPassword");
-        }
-        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) {
-                        res.redirect("/login");
-                        return;
-                    }
-                    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'
-            } as MailOptions;
-            smtpTransport.sendMail(mailOptions, function (err) {
-                done(null, err);
-            });
-        }
-    ], function (err) {
-        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
deleted file mode 100644
index b6ff10bab..000000000
--- a/src/server/authentication/models/current_user_utils.ts
+++ /dev/null
@@ -1,738 +0,0 @@
-import { action, computed, observable, reaction } from "mobx";
-import * as rp from 'request-promise';
-import { Utils } from "../../../Utils";
-import { DocServer } from "../../../client/DocServer";
-import { Docs, DocumentOptions } from "../../../client/documents/Documents";
-import { UndoManager } from "../../../client/util/UndoManager";
-import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc";
-import { List } from "../../../new_fields/List";
-import { listSpec } from "../../../new_fields/Schema";
-import { ScriptField, ComputedField } from "../../../new_fields/ScriptField";
-import { Cast, PromiseValue, StrCast, NumCast } from "../../../new_fields/Types";
-import { nullAudio, ImageField } from "../../../new_fields/URLField";
-import { DragManager } from "../../../client/util/DragManager";
-import { InkingControl } from "../../../client/views/InkingControl";
-import { Scripting, CompileScript } from "../../../client/util/Scripting";
-import { CollectionViewType } from "../../../client/views/collections/CollectionView";
-import { makeTemplate } from "../../../client/util/DropConverter";
-import { RichTextField } from "../../../new_fields/RichTextField";
-import { PrefetchProxy } from "../../../new_fields/Proxy";
-import { FormattedTextBox } from "../../../client/views/nodes/formattedText/FormattedTextBox";
-import { MainView } from "../../../client/views/MainView";
-import { DocumentType } from "../../../client/documents/DocumentTypes";
-import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
-import { DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView";
-
-export class CurrentUserUtils {
-    private static curr_id: string;
-    //TODO tfs: these should be temporary...
-    private static mainDocId: string | undefined;
-
-    public static get id() { return this.curr_id; }
-    public static get MainDocId() { return this.mainDocId; }
-    public static set MainDocId(id: string | undefined) { this.mainDocId = id; }
-    @computed public static get UserDocument() { return Doc.UserDoc(); }
-    @computed public static get ActivePen() { return Doc.UserDoc().activePen instanceof Doc && (Doc.UserDoc().activePen as Doc).inkPen as Doc; }
-
-    @observable public static GuestTarget: Doc | undefined;
-    @observable public static GuestWorkspace: Doc | undefined;
-    @observable public static GuestMobile: Doc | undefined;
-
-    // sets up the default User Templates - slideView, queryView, descriptionView
-    static setupUserTemplateButtons(doc: Doc) {
-        if (doc["template-button-query"] === undefined) {
-            const queryTemplate = Docs.Create.MulticolumnDocument(
-                [
-                    Docs.Create.QueryDocument({ title: "query", _height: 200 }),
-                    Docs.Create.FreeformDocument([], { title: "data", _height: 100, _LODdisable: true })
-                ],
-                { _width: 400, _height: 300, title: "queryView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
-            );
-            queryTemplate.isTemplateDoc = makeTemplate(queryTemplate);
-            doc["template-button-query"] = CurrentUserUtils.ficon({
-                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
-                dragFactory: new PrefetchProxy(queryTemplate) as any as Doc,
-                removeDropProperties: new List<string>(["dropAction"]), title: "query view", icon: "question-circle"
-            });
-        }
-
-        if (doc["template-button-slides"] === undefined) {
-            const slideTemplate = Docs.Create.MultirowDocument(
-                [
-                    Docs.Create.MulticolumnDocument([], { title: "data", _height: 200 }),
-                    Docs.Create.TextDocument("", { title: "text", _height: 100 })
-                ],
-                { _width: 400, _height: 300, title: "slideView", _chromeStatus: "disabled", _xMargin: 3, _yMargin: 3, hideFilterView: true }
-            );
-            slideTemplate.isTemplateDoc = makeTemplate(slideTemplate);
-            doc["template-button-slides"] = CurrentUserUtils.ficon({
-                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
-                dragFactory: new PrefetchProxy(slideTemplate) as any as Doc,
-                removeDropProperties: new List<string>(["dropAction"]), title: "presentation slide", icon: "address-card"
-            });
-        }
-
-        if (doc["template-button-description"] === undefined) {
-            const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created
-            Doc.GetProto(descriptionTemplate).layout =
-                "<div><FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" +
-                "<FormattedTextBox {...props} height='calc(100% - {this._headerHeight||75}px)' fieldKey={'text'}/></div>";
-            descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView");
-
-            doc["template-button-description"] = CurrentUserUtils.ficon({
-                onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'),
-                dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc,
-                removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize"
-            });
-        }
-
-        if (doc["template-button-switch"] === undefined) {
-            const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create;
-
-            const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _LODdisable: true, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40 });
-            const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1 });
-            const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, _LODdisable: true });
-            const labelTemplate = {
-                doc: {
-                    type: "doc", content: [{
-                        type: "paragraph",
-                        content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }]
-                    }]
-                },
-                selection: { type: "text", anchor: 1, head: 1 },
-                storedMarks: []
-            };
-            Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS");
-            Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'");
-            // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'");
-            // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true");
-            Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]");
-            // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false");
-            const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, });
-            box.isTemplateDoc = makeTemplate(box, true, "switch");
-
-            doc["template-button-switch"] = CurrentUserUtils.ficon({
-                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
-                dragFactory: new PrefetchProxy(box) as any as Doc,
-                removeDropProperties: new List<string>(["dropAction"]), title: "data switch", icon: "toggle-on"
-            });
-        }
-
-        if (doc["template-button-detail"] === undefined) {
-            const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create;
-
-            const openInTarget = ScriptField.MakeScript("openOnRight(self.doubleClickView)");
-            const carousel = CarouselDocument([], {
-                title: "data", _height: 350, _itemIndex: 0, "_carousel-caption-xMargin": 10, "_carousel-caption-yMargin": 10,
-                onChildDoubleClick: openInTarget, backgroundColor: "#9b9b9b3F"
-            });
-
-            const details = TextDocument("", { title: "details", _height: 350, _autoHeight: true });
-            const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 100, _autoHeight: true });
-            const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 350, _autoHeight: true });
-
-            const buxtonFieldKeys = ["year", "originalPrice", "degreesOfFreedom", "company", "attribute", "primaryKey", "secondaryKey", "dimensions"];
-            const detailedTemplate = {
-                doc: {
-                    type: "doc", content: buxtonFieldKeys.map(fieldKey => ({
-                        type: "paragraph",
-                        content: [{ type: "dashField", attrs: { fieldKey } }]
-                    }))
-                },
-                selection: { type: "text", anchor: 1, head: 1 },
-                storedMarks: []
-            };
-            details.text = new RichTextField(JSON.stringify(detailedTemplate), buxtonFieldKeys.join(" "));
-
-            const shared = { _chromeStatus: "disabled", _autoHeight: true, _xMargin: 0 };
-            const detailViewOpts = { title: "detailView", _width: 300, _fontFamily: "Arial", _fontSize: 12 };
-            const descriptionWrapperOpts = { title: "descriptions", _height: 300, columnWidth: -1, treeViewHideTitle: true, _pivotField: "title" };
-
-            const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts });
-            descriptionWrapper.sectionHeaders = new List<SchemaHeaderField>([
-                new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false),
-                new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true),
-                new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true),
-            ]);
-            const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts });
-            detailView.isTemplateDoc = makeTemplate(detailView);
-
-            details.title = "Details";
-            short.title = "A Short Description";
-            long.title = "Long Description";
-
-            doc["template-button-detail"] = CurrentUserUtils.ficon({
-                onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'),
-                dragFactory: new PrefetchProxy(detailView) as any as Doc,
-                removeDropProperties: new List<string>(["dropAction"]), title: "detail view", icon: "window-maximize"
-            });
-        }
-
-        if (doc["template-buttons"] === undefined) {
-            doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc,
-            doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc], {
-                title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title",
-                _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
-                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
-            }));
-        } else {
-            const curButnTypes = Cast(doc["template-buttons"], Doc, null);
-            const requiredTypes = [doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc,
-            doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc];
-            DocListCastAsync(curButnTypes.data).then(async curBtns => {
-                await Promise.all(curBtns!);
-                requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype));
-            });
-        }
-        return doc["template-buttons"] as Doc;
-    }
-
-    // setup the different note type skins
-    static setupNoteTemplates(doc: Doc) {
-        if (doc["template-note-Note"] === undefined) {
-            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Note", isTemplateDoc: true, backgroundColor: "yellow" });
-            noteView.isTemplateDoc = makeTemplate(noteView, true, "Note");
-            doc["template-note-Note"] = new PrefetchProxy(noteView);
-        }
-        if (doc["template-note-Idea"] === undefined) {
-            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Idea", backgroundColor: "pink" });
-            noteView.isTemplateDoc = makeTemplate(noteView, true, "Idea");
-            doc["template-note-Idea"] = new PrefetchProxy(noteView);
-        }
-        if (doc["template-note-Topic"] === undefined) {
-            const noteView = Docs.Create.TextDocument("", { title: "text", style: "Topic", backgroundColor: "lightBlue" });
-            noteView.isTemplateDoc = makeTemplate(noteView, true, "Topic");
-            doc["template-note-Topic"] = new PrefetchProxy(noteView);
-        }
-        if (doc["template-note-Todo"] === undefined) {
-            const noteView = Docs.Create.TextDocument("", {
-                title: "text", style: "Todo", backgroundColor: "orange", _autoHeight: false, _height: 100, _showCaption: "caption",
-                layout: FormattedTextBox.LayoutString("Todo"), caption: RichTextField.DashField("taskStatus")
-            });
-            noteView.isTemplateDoc = makeTemplate(noteView, true, "Todo");
-            doc["template-note-Todo"] = new PrefetchProxy(noteView);
-        }
-        const taskStatusValues = [
-            { title: "todo", _backgroundColor: "blue", color: "white" },
-            { title: "in progress", _backgroundColor: "yellow", color: "black" },
-            { title: "completed", _backgroundColor: "green", color: "white" }
-        ];
-        if (doc.fieldTypes === undefined) {
-            doc.fieldTypes = Docs.Create.TreeDocument([], { title: "field enumerations" });
-            Doc.addFieldEnumerations(Doc.GetProto(doc["template-note-Todo"] as any as Doc), "taskStatus", taskStatusValues);
-        }
-
-        if (doc["template-notes"] === undefined) {
-            doc["template-notes"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-note-Note"] as any as Doc,
-            doc["template-note-Idea"] as any as Doc, doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc],
-                { title: "Note Layouts", _height: 75 }));
-        } else {
-            const curNoteTypes = Cast(doc["template-notes"], Doc, null);
-            const requiredTypes = [doc["template-note-Note"] as any as Doc, doc["template-note-Idea"] as any as Doc,
-            doc["template-note-Topic"] as any as Doc, doc["template-note-Todo"] as any as Doc];
-            DocListCastAsync(curNoteTypes.data).then(async curNotes => {
-                await Promise.all(curNotes!);
-                requiredTypes.map(ntype => Doc.AddDocToList(curNoteTypes, "data", ntype));
-            });
-        }
-
-        return doc["template-notes"] as Doc;
-    }
-
-    // creates Note templates, and initial "user" templates
-    static setupDocTemplates(doc: Doc) {
-        const noteTemplates = CurrentUserUtils.setupNoteTemplates(doc);
-        const userTemplateBtns = CurrentUserUtils.setupUserTemplateButtons(doc);
-        const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
-        if (doc.templateDocs === undefined) {
-            doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
-                title: "template layouts", _xPadding: 0,
-                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
-            }));
-        }
-    }
-
-    // setup templates for different document types when they are iconified from Document Decorations
-    static setupDefaultIconTemplates(doc: Doc) {
-        if (doc["template-icon-view"] === undefined) {
-            const iconView = Docs.Create.TextDocument("", {
-                title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
-            });
-            Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', "");
-            iconView.isTemplateDoc = makeTemplate(iconView);
-            doc["template-icon-view"] = new PrefetchProxy(iconView);
-        }
-        if (doc["template-icon-view-rtf"] === undefined) {
-            const iconRtfView = Docs.Create.LabelDocument({
-                title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset",
-                _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
-            });
-            iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF);
-            doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView);
-        }
-        if (doc["template-icon-view-img"] === undefined) {
-            const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", {
-                title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)")
-            });
-            iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG);
-            doc["template-icon-view-img"] = new PrefetchProxy(iconImageView);
-        }
-        if (doc["template-icon-view-col"] === undefined) {
-            const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") });
-            iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL);
-            doc["template-icon-view-col"] = new PrefetchProxy(iconColView);
-        }
-        if (doc["template-icons"] === undefined) {
-            doc["template-icons"] = new PrefetchProxy(Docs.Create.TreeDocument([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
-            doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 }));
-        } else {
-            const templateIconsDoc = Cast(doc["template-icons"], Doc, null);
-            const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc,
-            doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc];
-            DocListCastAsync(templateIconsDoc.data).then(async curIcons => {
-                await Promise.all(curIcons!);
-                requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype));
-            });
-        }
-        return doc["template-icons"] as Doc;
-    }
-
-    static creatorBtnDescriptors(doc: Doc): {
-        title: string, label: string, icon: string, drag?: string, ignoreClick?: boolean,
-        click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc
-    }[] {
-        if (doc.emptyPresentation === undefined) {
-            doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(),
-                { title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" });
-        }
-        if (doc.emptyCollection === undefined) {
-            doc.emptyCollection = Docs.Create.FreeformDocument([],
-                { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" });
-        }
-        if (doc.emptyDocHolder === undefined) {
-            doc.emptyDocHolder = Docs.Create.DocumentDocument(
-                ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any,
-                { _width: 250, _height: 250, title: "container" });
-        }
-        if (doc.emptyWebpage === undefined) {
-            doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600, UseCors: true });
-        }
-        return [
-            { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc },
-            { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc },
-            { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' },
-            { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' },
-            { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' },
-            { title: "Drag a audio recorder", label: "Audio", icon: "microphone", ignoreClick: true, drag: `Docs.Create.AudioDocument("${nullAudio}", { _width: 200, title: "ready to record audio" })` },
-            { title: "Drag a clickable button", label: "Btn", icon: "bolt", ignoreClick: true, drag: 'Docs.Create.ButtonDocument({ _width: 150, _height: 50, _xPadding:10, _yPadding: 10, title: "Button" })' },
-            { title: "Drag a presentation view", label: "Prezi", icon: "tv", click: 'openOnRight(Doc.UserDoc().activePresentation = getCopy(this.dragFactory, true))', drag: `Doc.UserDoc().activePresentation = getCopy(this.dragFactory,true)`, dragFactory: doc.emptyPresentation as Doc },
-            { title: "Drag a search box", label: "Query", icon: "search", ignoreClick: true, drag: 'Docs.Create.QueryDocument({ _width: 200, title: "an image of a cat" })' },
-            { title: "Drag a scripting box", label: "Script", icon: "terminal", ignoreClick: true, drag: 'Docs.Create.ScriptingDocument(undefined, { _width: 200, _height: 250 title: "untitled script" })' },
-            { title: "Drag an import folder", label: "Load", icon: "cloud-upload-alt", ignoreClick: true, drag: 'Docs.Create.DirectoryImportDocument({ title: "Directory Import", _width: 400, _height: 400 })' },
-            { title: "Drag a mobile view", label: "Phone", icon: "phone", ignoreClick: true, drag: 'Doc.UserDoc().activeMobile' },
-            { title: "Drag an instance of the device collection", label: "Buxton", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.Buxton()' },
-            // { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            // { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
-            { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc },
-            { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' },
-            { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' },
-        ];
-
-    }
-
-    // setup the "creator" buttons for the sidebar-- eg. the default set of draggable document creation tools
-    static async setupCreatorButtons(doc: Doc) {
-        let alreadyCreatedButtons: string[] = [];
-        const dragCreatorSet = await Cast(doc.myItemCreators, Doc, null);
-        if (dragCreatorSet) {
-            const dragCreators = await Cast(dragCreatorSet.data, listSpec(Doc));
-            if (dragCreators) {
-                const dragDocs = await Promise.all(dragCreators);
-                alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title));
-            }
-        }
-        const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title));
-        const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({
-            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100,
-            icon,
-            title,
-            label,
-            ignoreClick,
-            dropAction: "copy",
-            onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined,
-            onClick: click ? ScriptField.MakeScript(click) : undefined,
-            ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined,
-            activePen,
-            backgroundColor,
-            removeDropProperties: new List<string>(["dropAction"]),
-            dragFactory,
-        }));
-
-        if (dragCreatorSet === undefined) {
-            doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, {
-                title: "Basic Item Creators", _showTitle: "title", _xMargin: 0,
-                _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
-                dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
-            }));
-        } else {
-            creatorBtns.forEach(nb => Doc.AddDocToList(doc.myItemCreators as Doc, "data", nb));
-        }
-        return doc.myItemCreators as Doc;
-    }
-
-    static setupMobileButtons(doc: Doc, buttons?: string[]) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, click?: string, ischecked?: string, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
-            { title: "record", icon: "microphone", ignoreClick: true, click: "FILL" },
-            { title: "use pen", icon: "pen-nib", click: 'activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", click: 'activateBrush(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,20,this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc },
-            { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc },
-            // { title: "draw", icon: "pen-nib", click: 'switchMobileView(setupMobileInkingDoc, renderMobileInking, onSwitchMobileInking);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "red", activePen: doc },
-            { title: "upload", icon: "upload", click: 'switchMobileView(setupMobileUploadDoc, renderMobileUpload, onSwitchMobileUpload);', backgroundColor: "orange" },
-            // { title: "upload", icon: "upload", click: 'uploadImageMobile();', backgroundColor: "cyan" },
-        ];
-        return docProtoData.filter(d => !buttons || !buttons.includes(d.title)).map(data => Docs.Create.FontIconDocument({
-            _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, dropAction: data.click ? "copy" : undefined, title: data.title, icon: data.icon, ignoreClick: data.ignoreClick,
-            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen,
-            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
-        }));
-    }
-
-    static setupThumbButtons(doc: Doc) {
-        const docProtoData: { title: string, icon: string, drag?: string, ignoreClick?: boolean, pointerDown?: string, pointerUp?: string, ischecked?: string, clipboard?: Doc, activePen?: Doc, backgroundColor?: string, dragFactory?: Doc }[] = [
-            { title: "use pen", icon: "pen-nib", pointerUp: "resetPen()", pointerDown: 'setPen(2, this.backgroundColor)', backgroundColor: "blue", ischecked: `sameDocs(this.activePen.inkPen,  this)`, activePen: doc },
-            { title: "use highlighter", icon: "highlighter", pointerUp: "resetPen()", pointerDown: 'setPen(20, this.backgroundColor)', backgroundColor: "yellow", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "notepad", icon: "clipboard", pointerUp: "GestureOverlay.Instance.closeFloatingDoc()", pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300 }), backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "interpret text", icon: "font", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('inktotext')", backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-            { title: "ignore gestures", icon: "signature", pointerUp: "setToolglass('none')", pointerDown: "setToolglass('ignoregesture')", backgroundColor: "green", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc },
-        ];
-        return docProtoData.map(data => Docs.Create.FontIconDocument({
-            _nativeWidth: 10, _nativeHeight: 10, _width: 10, _height: 10, title: data.title, icon: data.icon,
-            dropAction: data.pointerDown ? "copy" : undefined, ignoreClick: data.ignoreClick,
-            onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined,
-            clipboard: data.clipboard,
-            onPointerUp: data.pointerUp ? ScriptField.MakeScript(data.pointerUp) : undefined, onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined,
-            ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, activePen: data.activePen, pointerHack: true,
-            backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), dragFactory: data.dragFactory,
-        }));
-    }
-
-    static setupThumbDoc(userDoc: Doc) {
-        if (!userDoc.thumbDoc) {
-            const thumbDoc = Docs.Create.LinearDocument(CurrentUserUtils.setupThumbButtons(userDoc), {
-                _width: 100, _height: 50, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons",
-                _autoHeight: true, _yMargin: 5, linearViewIsExpanded: true, backgroundColor: "white"
-            });
-            thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], {
-                _width: 300, _height: 25, _autoHeight: true, _chromeStatus: "disabled", linearViewIsExpanded: true, flexDirection: "column"
-            });
-            userDoc.thumbDoc = thumbDoc;
-        }
-        return Cast(userDoc.thumbDoc, Doc);
-    }
-
-    static setupMobileDoc(userDoc: Doc) {
-        return userDoc.activeMoble ?? Docs.Create.MasonryDocument(CurrentUserUtils.setupMobileButtons(userDoc), {
-            columnWidth: 100, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", title: "buttons", _autoHeight: true, _yMargin: 5
-        });
-    }
-
-    static setupMobileInkingDoc(userDoc: Doc) {
-        return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white" });
-    }
-
-    static setupMobileUploadDoc(userDoc: Doc) {
-        // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" })
-        const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", {
-            title: "Upload Images From the Web", _chromeStatus: "enabled", lockedPosition: true
-        });
-        const uploadDoc = Docs.Create.StackingDocument([], {
-            title: "Mobile Upload Collection", backgroundColor: "white", lockedPosition: true
-        });
-        return Docs.Create.StackingDocument([webDoc, uploadDoc], {
-            _width: screen.width, lockedPosition: true, _chromeStatus: "disabled", title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray"
-        });
-    }
-
-    // setup the Creator button which will display the creator panel.  This panel will include the drag creators and the color picker. 
-    // when clicked, this panel will be displayed in the target container (ie, sidebarContainer)  
-    static async setupToolsBtnPanel(doc: Doc, sidebarContainer: Doc) {
-        // setup a masonry view of all he creators
-        const creatorBtns = await CurrentUserUtils.setupCreatorButtons(doc);
-        const templateBtns = CurrentUserUtils.setupUserTemplateButtons(doc);
-
-        if (doc.myCreators === undefined) {
-            doc.myCreators = new PrefetchProxy(Docs.Create.StackingDocument([creatorBtns, templateBtns], {
-                title: "all Creators", _yMargin: 0, _autoHeight: true, _xMargin: 0,
-                _width: 500, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled",
-            }));
-        }
-        // setup a color picker
-        if (doc.myColorPicker === undefined) {
-            const color = Docs.Create.ColorDocument({
-                title: "color picker", _width: 300, dropAction: "alias", forceActive: true, removeDropProperties: new List<string>(["dropAction", "forceActive"])
-            });
-            doc.myColorPicker = new PrefetchProxy(color);
-        }
-
-        if (doc["tabs-button-tools"] === undefined) {
-            doc["tabs-button-tools"] = new PrefetchProxy(Docs.Create.ButtonDocument({
-                _width: 35, _height: 25, title: "Tools", _fontSize: 10,
-                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
-                sourcePanel: new PrefetchProxy(Docs.Create.StackingDocument([doc.myCreators as Doc, doc.myColorPicker as Doc], {
-                    _width: 500, lockedPosition: true, _chromeStatus: "disabled", title: "tools stack", forceActive: true
-                })) as any as Doc,
-                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
-                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel"),
-            }));
-        }
-        (doc["tabs-button-tools"] as Doc).sourcePanel; // prefetch sourcePanel
-        return doc["tabs-button-tools"] as Doc;
-    }
-
-    static setupWorkspaces(doc: Doc) {
-        // setup workspaces library item
-        if (doc.myWorkspaces === undefined) {
-            doc.myWorkspaces = new PrefetchProxy(Docs.Create.TreeDocument([], {
-                title: "WORKSPACES", _height: 100, forceActive: true, boxShadow: "0 0", lockedPosition: true,
-            }));
-        }
-        const newWorkspace = ScriptField.MakeScript(`createNewWorkspace()`);
-        (doc.myWorkspaces as Doc).contextMenuScripts = new List<ScriptField>([newWorkspace!]);
-        (doc.myWorkspaces as Doc).contextMenuLabels = new List<string>(["Create New Workspace"]);
-
-        return doc.myWorkspaces as Doc;
-    }
-    static setupCatalog(doc: Doc) {
-        if (doc.myCatalog === undefined) {
-            doc.myCatalog = new PrefetchProxy(Docs.Create.TreeDocument([], {
-                title: "CATALOG", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true,
-            }));
-        }
-        return doc.myCatalog as Doc;
-    }
-    static setupRecentlyClosed(doc: Doc) {
-        // setup Recently Closed library item
-        if (doc.myRecentlyClosed === undefined) {
-            doc.myRecentlyClosed = new PrefetchProxy(Docs.Create.TreeDocument([], {
-                title: "RECENTLY CLOSED", _height: 75, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true,
-            }));
-        }
-        // this is equivalent to using PrefetchProxies to make sure the recentlyClosed doc is ready
-        PromiseValue(Cast(doc.myRecentlyClosed, Doc)).then(recent => recent && PromiseValue(recent.data).then(DocListCast));
-        const clearAll = ScriptField.MakeScript(`self.data = new List([])`);
-        (doc.myRecentlyClosed as Doc).contextMenuScripts = new List<ScriptField>([clearAll!]);
-        (doc.myRecentlyClosed as Doc).contextMenuLabels = new List<string>(["Clear All"]);
-
-        return doc.myRecentlyClosed as Doc;
-    }
-    // setup the Library button which will display the library panel.  This panel includes a collection of workspaces, documents, and recently closed views
-    static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) {
-        const workspaces = CurrentUserUtils.setupWorkspaces(doc);
-        const documents = CurrentUserUtils.setupCatalog(doc);
-        const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc);
-
-        if (doc["tabs-button-library"] === undefined) {
-            doc["tabs-button-library"] = new PrefetchProxy(Docs.Create.ButtonDocument({
-                _width: 50, _height: 25, title: "Library", _fontSize: 10,
-                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
-                sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], {
-                    title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true
-                })) as any as Doc,
-                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
-                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;")
-            }));
-        }
-        return doc["tabs-button-library"] as Doc;
-    }
-
-    // setup the Search button which will display the search panel.  
-    static setupSearchBtnPanel(doc: Doc, sidebarContainer: Doc) {
-        if (doc["tabs-button-search"] === undefined) {
-            doc["tabs-button-search"] = new PrefetchProxy(Docs.Create.ButtonDocument({
-                _width: 50, _height: 25, title: "Search", _fontSize: 10,
-                letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)",
-                sourcePanel: new PrefetchProxy(Docs.Create.QueryDocument({ title: "search stack", })) as any as Doc,
-                searchFileTypes: new List<string>([DocumentType.RTF, DocumentType.IMG, DocumentType.PDF, DocumentType.VID, DocumentType.WEB, DocumentType.SCRIPTING]),
-                targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc,
-                lockedPosition: true,
-                onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel")
-            }));
-        }
-        return doc["tabs-button-search"] as Doc;
-    }
-
-    static setupSidebarContainer(doc: Doc) {
-        if (doc["tabs-panelContainer"] === undefined) {
-            const sidebarContainer = new Doc();
-            sidebarContainer._chromeStatus = "disabled";
-            sidebarContainer.onClick = ScriptField.MakeScript("freezeSidebar()");
-            doc["tabs-panelContainer"] = new PrefetchProxy(sidebarContainer);
-        }
-        return doc["tabs-panelContainer"] as Doc;
-    }
-
-    // setup the list of sidebar mode buttons which determine what is displayed in the sidebar
-    static async setupSidebarButtons(doc: Doc) {
-        const sidebarContainer = CurrentUserUtils.setupSidebarContainer(doc);
-        const toolsBtn = await CurrentUserUtils.setupToolsBtnPanel(doc, sidebarContainer);
-        const libraryBtn = CurrentUserUtils.setupLibraryPanel(doc, sidebarContainer);
-        const searchBtn = CurrentUserUtils.setupSearchBtnPanel(doc, sidebarContainer);
-
-        // Finally, setup the list of buttons to display in the sidebar
-        if (doc["tabs-buttons"] === undefined) {
-            doc["tabs-buttons"] = new PrefetchProxy(Docs.Create.StackingDocument([searchBtn, libraryBtn, toolsBtn], {
-                _width: 500, _height: 80, boxShadow: "0 0", _pivotField: "title", hideHeadings: true, ignoreClick: true, _chromeStatus: "view-mode",
-                title: "sidebar btn row stack", backgroundColor: "dimGray",
-            }));
-            (toolsBtn.onClick as ScriptField).script.run({ this: toolsBtn });
-        }
-    }
-
-    static blist = (opts: DocumentOptions, docs: Doc[]) => new PrefetchProxy(Docs.Create.LinearDocument(docs, {
-        ...opts,
-        _gridGap: 5, _xMargin: 5, _yMargin: 5, _height: 42, _width: 100, boxShadow: "0 0", forceActive: true,
-        dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }),
-        backgroundColor: "black", treeViewPreventOpen: true, lockedPosition: true, _chromeStatus: "disabled", linearViewIsExpanded: true
-    })) as any as Doc
-
-    static ficon = (opts: DocumentOptions) => new PrefetchProxy(Docs.Create.FontIconDocument({
-        ...opts,
-        dropAction: "alias", removeDropProperties: new List<string>(["dropAction"]), _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100
-    })) as any as Doc
-
-    /// sets up the default list of buttons to be shown in the expanding button menu at the bottom of the Dash window
-    static setupDockedButtons(doc: Doc) {
-        if (doc["dockedBtn-pen"] === undefined) {
-            doc["dockedBtn-pen"] = CurrentUserUtils.ficon({
-                onClick: ScriptField.MakeScript("activatePen(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this,2, this.backgroundColor)"),
-                author: "systemTemplates", title: "ink mode", icon: "pen-nib", ischecked: ComputedField.MakeFunction(`sameDocs(this.activePen.inkPen,  this)`), activePen: doc
-            });
-        }
-        if (doc["dockedBtn-undo"] === undefined) {
-            doc["dockedBtn-undo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("undo()"), title: "undo button", icon: "undo-alt" });
-        }
-        if (doc["dockedBtn-redo"] === undefined) {
-            doc["dockedBtn-redo"] = CurrentUserUtils.ficon({ onClick: ScriptField.MakeScript("redo()"), title: "redo button", icon: "redo-alt" });
-        }
-        if (doc.dockedBtns === undefined) {
-            doc.dockedBtns = CurrentUserUtils.blist({ title: "docked buttons", ignoreClick: true }, [doc["dockedBtn-undo"] as Doc, doc["dockedBtn-redo"] as Doc, doc["dockedBtn-pen"] as Doc]);
-        }
-    }
-    // sets up the default set of documents to be shown in the Overlay layer
-    static setupOverlays(doc: Doc) {
-        if (doc.myOverlayDocuments === undefined) {
-            doc.myOverlayDocuments = new PrefetchProxy(Docs.Create.FreeformDocument([], { title: "overlay documents", backgroundColor: "#aca3a6" }));
-        }
-    }
-
-    // the initial presentation Doc to use
-    static setupDefaultPresentation(doc: Doc) {
-        if (doc.activePresentation === undefined) {
-            doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), {
-                title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias",
-                _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0"
-            });
-        }
-    }
-
-    static setupRightSidebar(doc: Doc) {
-        if (doc.rightSidebarCollection === undefined) {
-            doc.rightSidebarCollection = new PrefetchProxy(Docs.Create.StackingDocument([], { title: "Right Sidebar" }));
-        }
-    }
-
-    static setupClickEditorTemplates(doc: Doc) {
-        if (doc["clickFuncs-child"] === undefined) {
-            const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
-                "docCast(thisContainer.target).then((target) => {" +
-                "     target && docCast(this.source).then((source) => { " +
-                "           target.proto.data = new List([source || this]); " +
-                "      }); " +
-                "})",
-                { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" });
-
-            const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript(
-                "openOnRight(self.doubleClickView)",
-                { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" });
-
-            doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" });
-        }
-        // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved.
-        PromiseValue(Cast(doc["clickFuncs-child"], Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
-
-        if (doc.clickFuncs === undefined) {
-            const onClick = Docs.Create.ScriptingDocument(undefined, {
-                title: "onClick", "onClick-rawScript": "console.log('click')",
-                isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200
-            }, "onClick");
-            const onDoubleClick = Docs.Create.ScriptingDocument(undefined, {
-                title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')",
-                isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200
-            }, "onDoubleClick");
-            const onCheckedClick = Docs.Create.ScriptingDocument(undefined, {
-                title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200
-            }, "onCheckedClick");
-            doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" });
-        }
-        PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast));
-
-        return doc.clickFuncs as Doc;
-    }
-
-    static async updateUserDocument(doc: Doc) {
-        new InkingControl();
-        doc.title = Doc.CurrentUserEmail;
-        doc.activePen = doc;
-        doc.inkColor = StrCast(doc.backgroundColor, "");
-        doc.fontSize = NumCast(doc.fontSize, 12);
-        doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // 
-        doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // 
-        Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]);
-        this.setupDefaultIconTemplates(doc);  // creates a set of icon templates triggered by the document deoration icon
-        this.setupDocTemplates(doc); // sets up the template menu of templates
-        this.setupRightSidebar(doc);  // sets up the right sidebar collection for mobile upload documents and sharing
-        this.setupOverlays(doc);  // documents in overlay layer 
-        this.setupDockedButtons(doc);  // the bottom bar of font icons
-        this.setupDefaultPresentation(doc); // presentation that's initially triggered
-        await this.setupSidebarButtons(doc); // the pop-out left sidebar of tools/panels
-        doc.globalLinkDatabase = Docs.Prototypes.MainLinkDocument();
-
-        // setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet
-        doc["dockedBtn-undo"] && reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(doc["dockedBtn-undo"] as Doc).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true });
-        doc["dockedBtn-redo"] && reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(doc["dockedBtn-redo"] as Doc).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true });
-
-        return doc;
-    }
-    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;
-            } else {
-                throw new Error("There should be a user! Why does Dash think there isn't one?");
-            }
-        });
-    }
-
-    public static async loadUserDocument({ id, email }: { id: string, email: string }) {
-        this.curr_id = id;
-        Doc.CurrentUserEmail = email;
-        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))));
-            } else {
-                throw new Error("There should be a user id! Why does Dash think there isn't one?");
-            }
-        });
-    }
-}
-
-Scripting.addGlobal(function setupMobileInkingDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileInkingDoc(userDoc); });
-Scripting.addGlobal(function setupMobileUploadDoc(userDoc: Doc) { return CurrentUserUtils.setupMobileUploadDoc(userDoc); });
-Scripting.addGlobal(function createNewWorkspace() { return MainView.Instance.createNewWorkspace(); });
\ No newline at end of file
diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts
deleted file mode 100644
index a0b688328..000000000
--- a/src/server/authentication/models/user_model.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-//@ts-ignore
-import * as bcrypt from "bcrypt-nodejs";
-//@ts-ignore
-import * as mongoose from 'mongoose';
-
-export type DashUserModel = mongoose.Document & {
-    email: String,
-    password: string,
-    passwordResetToken?: string,
-    passwordResetExpires?: Date,
-
-    userDocumentId: string;
-
-    profile: {
-        name: string,
-        gender: string,
-        location: string,
-        website: string,
-        picture: string
-    },
-
-    comparePassword: comparePasswordFunction,
-};
-
-type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void;
-
-export type AuthToken = {
-    accessToken: string,
-    kind: string
-};
-
-const userSchema = new mongoose.Schema({
-    email: String,
-    password: String,
-    passwordResetToken: String,
-    passwordResetExpires: Date,
-
-    userDocumentId: String,
-
-    facebook: String,
-    twitter: String,
-    google: String,
-
-    profile: {
-        name: String,
-        gender: String,
-        location: String,
-        website: String,
-        picture: String
-    }
-}, { timestamps: true });
-
-/**
- * Password hash middleware.
- */
-userSchema.pre("save", function save(next) {
-    const user = this as DashUserModel;
-    if (!user.isModified("password")) {
-        return next();
-    }
-    bcrypt.genSalt(10, (err, salt) => {
-        if (err) {
-            return next(err);
-        }
-        bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => {
-            if (err) {
-                return next(err);
-            }
-            user.password = hash;
-            next();
-        });
-    });
-});
-
-const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) {
-    // Choose one of the following bodies for authentication logic.
-    // secure (expected, default)
-    bcrypt.compare(candidatePassword, this.password, cb);
-    // bypass password (debugging)
-    // cb(undefined, true);
-};
-
-userSchema.methods.comparePassword = comparePassword;
-
-const User = mongoose.model("User", userSchema);
-export default User;
\ No newline at end of file
diff --git a/src/server/credentials/CredentialsLoader.ts b/src/server/credentials/CredentialsLoader.ts
deleted file mode 100644
index e3f4d167b..000000000
--- a/src/server/credentials/CredentialsLoader.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-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_project_credentials.json b/src/server/credentials/google_project_credentials.json
deleted file mode 100644
index 955c5a3c1..000000000
--- a/src/server/credentials/google_project_credentials.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-    "installed": {
-        "client_id": "343179513178-ud6tvmh275r2fq93u9eesrnc66t6akh9.apps.googleusercontent.com",
-        "project_id": "quickstart-1565056383187",
-        "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": "w8KIFSc0MQpmUYHed4qEzn8b",
-        "redirect_uris": ["urn:ietf:wg:oauth:2.0:oob", "http://localhost"]
-    }
-}
\ No newline at end of file
diff --git a/src/server/database.ts b/src/server/database.ts
index 580f7f919..ed9a246e3 100644
--- a/src/server/database.ts
+++ b/src/server/database.ts
@@ -293,13 +293,26 @@ export namespace Database {
 
     export const Instance = getDatabase();
 
+    /**
+     * Provides definitions and apis for working with
+     * portions of the database not dedicated to storing documents
+     * or Dash-internal user data.
+     */
     export namespace Auxiliary {
 
+        /**
+         * All the auxiliary MongoDB collections (schemas)
+         */
         export enum AuxiliaryCollections {
             GooglePhotosUploadHistory = "uploadedFromGooglePhotos",
-            GoogleAuthentication = "googleAuthentication"
+            GoogleAccess = "googleAuthentication"
         }
 
+        /**
+         * Searches for the @param query in the specified @param collection,
+         * and returns at most the first @param cap results. If @param removeId is true,
+         * as it is by default, each object will be stripped of its database id.
+         */
         const SanitizedCappedQuery = async (query: { [key: string]: any }, collection: string, cap: number, removeId = true) => {
             const cursor = await Instance.query(query, undefined, collection);
             const results = await cursor.toArray();
@@ -310,52 +323,89 @@ export namespace Database {
             }) : slice;
         };
 
+        /**
+         * Searches for the @param query in the specified @param collection,
+         * and returns at most the first result. If @param removeId is true,
+         * as it is by default, each object will be stripped of its database id. 
+         * Worth the special case since it converts the Array return type to a single
+         * object of the specified type.
+         */
         const SanitizedSingletonQuery = async <T>(query: { [key: string]: any }, collection: string, removeId = true): Promise<Opt<T>> => {
             const results = await SanitizedCappedQuery(query, collection, 1, removeId);
             return results.length ? results[0] : undefined;
         };
 
+        /**
+         * Checks to see if an image with the given @param contentSize 
+         * already exists in the aux database, i.e. has already been downloaded from Google Photos.
+         */
         export const QueryUploadHistory = async (contentSize: number) => {
             return SanitizedSingletonQuery<Upload.ImageInformation>({ contentSize }, AuxiliaryCollections.GooglePhotosUploadHistory);
         };
 
-        export namespace GoogleAuthenticationToken {
+        /**
+         * Records the uploading of the image with the given @param information,
+         * using the given content size as a seed for the database id.
+         */
+        export const LogUpload = async (information: Upload.ImageInformation) => {
+            const bundle = {
+                _id: Utils.GenerateDeterministicGuid(String(information.contentSize)),
+                ...information
+            };
+            return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory);
+        };
+
+        /**
+         * Manages the storage, retrieval and updating of the access token that
+         * facilitates interactions with all their APIs for a given account.
+         */
+        export namespace GoogleAccessToken {
 
+            /**
+             * Format stored in database.
+             */
             type StoredCredentials = GoogleApiServerUtils.EnrichedCredentials & { _id: string };
 
+            /**
+             * Retrieves the credentials associaed with @param userId
+             * and optionally removes their database id according to @param removeId. 
+             */
             export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => {
-                return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAuthentication, removeId);
+                return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAccess, removeId);
             };
 
+            /**
+             * Writes the @param enrichedCredentials to the database, associated
+             * with @param userId for later retrieval and updating. 
+             */
             export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => {
-                return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAuthentication);
+                return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAccess);
             };
 
+            /**
+             * Updates the @param access_token and @param expiry_date fields
+             * in the stored credentials associated with @param userId.
+             */
             export const Update = async (userId: string, access_token: string, expiry_date: number) => {
                 const entry = await Fetch(userId, false);
                 if (entry) {
                     const parameters = { $set: { access_token, expiry_date } };
-                    return Instance.update(entry._id, parameters, emptyFunction, true, AuxiliaryCollections.GoogleAuthentication);
+                    return Instance.update(entry._id, parameters, emptyFunction, true, AuxiliaryCollections.GoogleAccess);
                 }
             };
 
+            /**
+             * Revokes the credentials associated with @param userId. 
+             */
             export const Revoke = async (userId: string) => {
                 const entry = await Fetch(userId, false);
                 if (entry) {
-                    Instance.delete({ _id: entry._id }, AuxiliaryCollections.GoogleAuthentication);
+                    Instance.delete({ _id: entry._id }, AuxiliaryCollections.GoogleAccess);
                 }
             };
 
         }
 
-        export const LogUpload = async (information: Upload.ImageInformation) => {
-            const bundle = {
-                _id: Utils.GenerateDeterministicGuid(String(information.contentSize)),
-                ...information
-            };
-            return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory);
-        };
-
     }
 
 }
diff --git a/src/server/index.ts b/src/server/index.ts
index f26c8a6ab..af8f95a5e 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -11,9 +11,9 @@ 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 { WebSocket } from './websocket';
 import DownloadManager from './ApiManagers/DownloadManager';
-import { GoogleCredentialsLoader } from './credentials/CredentialsLoader';
+import { GoogleCredentialsLoader } from './apis/google/CredentialsLoader';
 import DeleteManager from "./ApiManagers/DeleteManager";
 import PDFManager from "./ApiManagers/PDFManager";
 import UploadManager from "./ApiManagers/UploadManager";
@@ -25,7 +25,6 @@ import { yellow } from "colors";
 import { DashSessionAgent } from "./DashSession/DashSessionAgent";
 import SessionManager from "./ApiManagers/SessionManager";
 import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent";
-import { Utils } from "../Utils";
 
 export const onWindows = process.platform === "win32";
 export let sessionAgent: AppliedSessionAgent;
@@ -125,7 +124,7 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }:
 
     // 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.start(isRelease);
+    WebSocket.initialize(isRelease);
 }
 
 
diff --git a/src/server/server_Initialization.ts b/src/server/server_Initialization.ts
index add607761..4b3094616 100644
--- a/src/server/server_Initialization.ts
+++ b/src/server/server_Initialization.ts
@@ -7,7 +7,7 @@ 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';
+import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/AuthenticationManager';
 const MongoStore = require('connect-mongo')(session);
 import RouteManager from './RouteManager';
 import * as webpack from 'webpack';
@@ -94,6 +94,8 @@ function determineEnvironment() {
     const label = isRelease ? "release" : "development";
     console.log(`\nrunning server in ${color(label)} mode`);
 
+    // swilkins: I don't think we need to read from ClientUtils.RELEASE anymore. Should be able to invoke process.env.RELEASE
+    // on the client side, thanks to dotenv in webpack.config.js
     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");
diff --git a/src/server/websocket.ts b/src/server/websocket.ts
new file mode 100644
index 000000000..7278bdc32
--- /dev/null
+++ b/src/server/websocket.ts
@@ -0,0 +1,297 @@
+import { Utils } from "../Utils";
+import { MessageStore, Transferable, Types, Diff, YoutubeQueryInput, YoutubeQueryTypes, GestureContent, MobileInkOverlayContent, UpdateMobileInkOverlayPositionContent, MobileDocumentUploadContent, RoomMessage } 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 "./apis/google/CredentialsLoader";
+import { logPort } from "./ActionUtilities";
+import { timeMap } from "./ApiManagers/UserManager";
+import { green } from "colors";
+import { networkInterfaces } from "os";
+import executeImport from "../scraping/buxton/final/BuxtonImporter";
+import { DocumentsCollection } from "./IDatabase";
+
+export namespace WebSocket {
+
+    export let _socket: Socket;
+    const clients: { [key: string]: Client } = {};
+    export const socketMap = new Map<SocketIO.Socket, string>();
+    export let disconnect: Function;
+
+    export function initialize(isRelease: boolean) {
+        const endpoint = io();
+        endpoint.on("connection", function (socket: Socket) {
+            _socket = socket;
+
+            socket.use((_packet, next) => {
+                const userEmail = socketMap.get(socket);
+                if (userEmail) {
+                    timeMap[userEmail] = Date.now();
+                }
+                next();
+            });
+
+            // convenience function to log server messages on the client
+            function log(message?: any, ...optionalParams: any[]) {
+                socket.emit('log', ['Message from server:', message, ...optionalParams]);
+            }
+
+            socket.on('message', function (message, room) {
+                console.log('Client said: ', message);
+                socket.in(room).emit('message', message);
+            });
+
+            socket.on('create or join', function (room) {
+                console.log('Received request to create or join room ' + room);
+
+                const clientsInRoom = socket.adapter.rooms[room];
+                const numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0;
+                console.log('Room ' + room + ' now has ' + numClients + ' client(s)');
+
+                if (numClients === 0) {
+                    socket.join(room);
+                    console.log('Client ID ' + socket.id + ' created room ' + room);
+                    socket.emit('created', room, socket.id);
+
+                } else if (numClients === 1) {
+                    console.log('Client ID ' + socket.id + ' joined room ' + room);
+                    socket.in(room).emit('join', room);
+                    socket.join(room);
+                    socket.emit('joined', room, socket.id);
+                    socket.in(room).emit('ready');
+                } else { // max two clients
+                    socket.emit('full', room);
+                }
+            });
+
+            socket.on('ipaddr', function () {
+                const ifaces = networkInterfaces();
+                for (const dev in ifaces) {
+                    ifaces[dev].forEach(function (details) {
+                        if (details.family === 'IPv4' && details.address !== '127.0.0.1') {
+                            socket.emit('ipaddr', details.address);
+                        }
+                    });
+                }
+            });
+
+            socket.on('bye', function () {
+                console.log('received bye');
+            });
+
+            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, () => doDelete(false));
+            }
+
+            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.AddServerHandler(socket, MessageStore.GesturePoints, content => processGesturePoints(socket, content));
+            Utils.AddServerHandler(socket, MessageStore.MobileInkOverlayTrigger, content => processOverlayTrigger(socket, content));
+            Utils.AddServerHandler(socket, MessageStore.UpdateMobileInkOverlayPosition, content => processUpdateOverlayPosition(socket, content));
+            Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content));
+            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField);
+            Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields);
+
+            /**
+             * Whenever we receive the go-ahead, invoke the import script and pass in
+             * as an emitter and a terminator the functions that simply broadcast a result
+             * or indicate termination to the client via the web socket
+             */
+            Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => {
+                executeImport(
+                    deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError),
+                    results => Utils.Emit(socket, MessageStore.BuxtonImportComplete, results)
+                );
+            });
+
+            disconnect = () => {
+                socket.broadcast.emit("connection_terminated", Date.now());
+                socket.disconnect(true);
+            };
+        });
+
+        const socketPort = isRelease ? Number(process.env.socketPort) : 4321;
+        endpoint.listen(socketPort);
+        logPort("websocket", socketPort);
+    }
+
+    function processGesturePoints(socket: Socket, content: GestureContent) {
+        socket.broadcast.emit("receiveGesturePoints", content);
+    }
+
+    function processOverlayTrigger(socket: Socket, content: MobileInkOverlayContent) {
+        socket.broadcast.emit("receiveOverlayTrigger", content);
+    }
+
+    function processUpdateOverlayPosition(socket: Socket, content: UpdateMobileInkOverlayPositionContent) {
+        socket.broadcast.emit("receiveUpdateOverlayPosition", content);
+    }
+
+    function processMobileDocumentUpload(socket: Socket, content: MobileDocumentUploadContent) {
+        socket.broadcast.emit("receiveMobileDocumentUpload", content);
+    }
+
+    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 doDelete(onlyFields = true) {
+        const target: string[] = [];
+        onlyFields && target.push(DocumentsCollection);
+        await Database.Instance.dropSchema(...target);
+        if (process.env.DISABLE_SEARCH !== "true") {
+            await Search.clear();
+        }
+    }
+
+    function barReceived(socket: SocketIO.Socket, userEmail: string) {
+        clients[userEmail] = new Client(userEmail.toString());
+        console.log(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) {  // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR
+            Search.updateDocument({ id: newValue.id, data: (newValue as any).data });
+        }
+    }
+
+    function GetRefField([id, callback]: [string, (result?: Transferable) => void]) {
+        Database.Instance.getDocument(id, callback);
+    }
+
+    function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) {
+        Database.Instance.getDocuments(ids, callback);
+    }
+
+    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"],
+        "script": ["_t", value => value.script.originalScript],
+        "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);
+        const docfield = diff.diff.$set || diff.diff.$unset;
+        if (!docfield) {
+            return;
+        }
+        const update: any = { id: diff.id };
+        let dynfield = false;
+        for (let key in docfield) {
+            if (!key.startsWith("fields.")) continue;
+            dynfield = true;
+            const val = docfield[key];
+            key = key.substring(7);
+            Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null });
+            const term = ToSearchTerm(val);
+            if (term !== undefined) {
+                const { suffix, value } = term;
+                update[key + suffix] = { set: value };
+            }
+        }
+        if (dynfield) {
+            Search.updateDocument(update);
+        }
+    }
+
+    function DeleteField(socket: Socket, id: string) {
+        Database.Instance.delete({ _id: id }).then(() => {
+            socket.broadcast.emit(MessageStore.DeleteField.Message, id);
+        });
+
+        Search.deleteDocuments([id]);
+    }
+
+    function DeleteFields(socket: Socket, ids: string[]) {
+        Database.Instance.delete({ _id: { $in: ids } }).then(() => {
+            socket.broadcast.emit(MessageStore.DeleteFields.Message, ids);
+        });
+        Search.deleteDocuments(ids);
+    }
+
+    function CreateField(newValue: any) {
+        Database.Instance.insert(newValue);
+    }
+
+}
+
diff --git a/webpack.config.js b/webpack.config.js
index 6265883fd..67d492e1f 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -16,22 +16,21 @@ const plugins = [
     new webpack.HotModuleReplacementPlugin(),
 ];
 
-const dotenv = require('dotenv');
-
-function transferEnvironmentVariables() {
+(function transferEnvironmentVariables() {
     const prefix = "_CLIENT_";
-    const env = dotenv.config().parsed;
-    if (env) {
-        plugins.push(new webpack.DefinePlugin(Object.keys(env).reduce((mapping, envKey) => {
-            if (envKey.startsWith(prefix)) {
-                mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(env[envKey]);
-            }
-            return mapping;
-        }, {})));
+    const {
+        parsed
+    } = require('dotenv').config();
+    if (!parsed) {
+        return;
     }
-}
-
-transferEnvironmentVariables();
+    plugins.push(new webpack.DefinePlugin(Object.keys(parsed).reduce((mapping, envKey) => {
+        if (envKey.startsWith(prefix)) {
+            mapping[`process.env.${envKey.replace(prefix, "")}`] = JSON.stringify(parsed[envKey]);
+        }
+        return mapping;
+    }, {})));
+})();
 
 module.exports = {
     mode: 'development',
-- 
cgit v1.2.3-70-g09d2

