diff options
Diffstat (limited to 'src/server')
-rw-r--r-- | src/server/Client.ts | 12 | ||||
-rw-r--r-- | src/server/Message.ts | 127 | ||||
-rw-r--r-- | src/server/RouteStore.ts | 30 | ||||
-rw-r--r-- | src/server/Search.ts | 49 | ||||
-rw-r--r-- | src/server/ServerUtil.ts | 71 | ||||
-rw-r--r-- | src/server/authentication/config/passport.ts | 13 | ||||
-rw-r--r-- | src/server/authentication/controllers/user.ts | 107 | ||||
-rw-r--r-- | src/server/authentication/controllers/user_controller.ts | 266 | ||||
-rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 137 | ||||
-rw-r--r-- | src/server/authentication/models/user_model.ts (renamed from src/server/authentication/models/User.ts) | 35 | ||||
-rw-r--r-- | src/server/database.ts | 123 | ||||
-rw-r--r-- | src/server/index.ts | 426 | ||||
-rw-r--r-- | src/server/public/files/.gitignore | 1 |
13 files changed, 922 insertions, 475 deletions
diff --git a/src/server/Client.ts b/src/server/Client.ts index 6b8841658..e6f953712 100644 --- a/src/server/Client.ts +++ b/src/server/Client.ts @@ -1,15 +1,11 @@ import { computed } from "mobx"; export class Client { - constructor(guid: string) { - this.guid = guid - } + private _guid: string; - private guid: string; - - @computed - public get GUID(): string { - return this.guid + constructor(guid: string) { + this._guid = guid; } + @computed public get GUID(): string { return this._guid; } }
\ No newline at end of file diff --git a/src/server/Message.ts b/src/server/Message.ts index 8a00f6b59..e9a8b0f0c 100644 --- a/src/server/Message.ts +++ b/src/server/Message.ts @@ -1,125 +1,48 @@ import { Utils } from "../Utils"; export class Message<T> { - private name: string; - private guid: string; - - get Name(): string { - return this.name; - } - - get Message(): string { - return this.guid - } + private _name: string; + private _guid: string; constructor(name: string) { - this.name = name; - this.guid = Utils.GenerateDeterministicGuid(name) - } - - GetValue() { - return this.Name; - } -} - -class TestMessageArgs { - hello: string = ""; -} - -export class SetFieldArgs { - field: string; - value: any; - - constructor(f: string, v: any) { - this.field = f - this.value = v + this._name = name; + this._guid = Utils.GenerateDeterministicGuid(name); } -} -export class GetFieldArgs { - field: string; - - constructor(f: string) { - this.field = f - } + get Name(): string { return this._name; } + get Message(): string { return this._guid; } } export enum Types { - Number, List, Key, Image, Web, Document, Text, RichText, DocumentReference, Html, Video, Audio, Ink, PDF -} - -export class DocumentTransfer implements Transferable { - readonly type = Types.Document - _id: string - - constructor(readonly obj: { type: Types, data: [string, string][], _id: string }) { - this._id = obj._id - } -} - -export class ImageTransfer implements Transferable { - readonly type = Types.Image - - constructor(readonly _id: string) { } -} - -export class KeyTransfer implements Transferable { - name: string - readonly _id: string - readonly type = Types.Key - - constructor(i: string, n: string) { - this.name = n - this._id = i - } -} - -export class ListTransfer implements Transferable { - type = Types.List; - - constructor(readonly _id: string) { } -} - -export class NumberTransfer implements Transferable { - readonly type = Types.Number - - constructor(readonly value: number, readonly _id: string) { } + Number, List, Key, Image, Web, Document, Text, Icon, RichText, DocumentReference, + Html, Video, Audio, Ink, PDF, Tuple, HistogramOp, Boolean, Script, Templates } -export class TextTransfer implements Transferable { - value: string - readonly _id: string - readonly type = Types.Text - - constructor(t: string, i: string) { - this.value = t - this._id = i - } +export interface Transferable { + readonly id: string; + readonly type: Types; + readonly data?: any; } -export class RichTextTransfer implements Transferable { - value: string - readonly _id: string - readonly type = Types.Text - - constructor(t: string, i: string) { - this.value = t - this._id = i - } +export interface Reference { + readonly id: string; } -export interface Transferable { - readonly _id: string - readonly type: Types +export interface Diff extends Reference { + readonly diff: any; } export namespace MessageStore { export const Foo = new Message<string>("Foo"); export const Bar = new Message<string>("Bar"); - export const AddDocument = new Message<DocumentTransfer>("Add Document"); - export const SetField = new Message<{ _id: string, data: any, type: Types }>("Set Field") - export const GetField = new Message<string>("Get Field") - export const GetFields = new Message<string[]>("Get Fields") + export const SetField = new Message<Transferable>("Set Field"); // send Transferable (no reply) + export const GetField = new Message<string>("Get Field"); // send string 'id' get Transferable back + export const GetFields = new Message<string[]>("Get Fields"); // send string[] of 'id' get Transferable[] back export const GetDocument = new Message<string>("Get Document"); export const DeleteAll = new Message<any>("Delete All"); -}
\ No newline at end of file + + export const GetRefField = new Message<string>("Get Ref Field"); + export const GetRefFields = new Message<string[]>("Get Ref Fields"); + export const UpdateField = new Message<Diff>("Update Ref Field"); + export const CreateField = new Message<Reference>("Create Ref Field"); +} diff --git a/src/server/RouteStore.ts b/src/server/RouteStore.ts new file mode 100644 index 000000000..fdf5b6a5c --- /dev/null +++ b/src/server/RouteStore.ts @@ -0,0 +1,30 @@ +// PREPEND ALL ROUTES WITH FORWARD SLASHES! + +export enum RouteStore { + // GENERAL + root = "/", + home = "/home", + corsProxy = "/corsProxy", + delete = "/delete", + deleteAll = "/deleteAll", + + // UPLOAD AND STATIC FILE SERVING + public = "/public", + upload = "/upload", + images = "/images", + + // USER AND WORKSPACES + getCurrUser = "/getCurrentUser", + getUserDocumentId = "/getUserDocumentId", + updateCursor = "/updateCursor", + + openDocumentWithId = "/doc/:docId", + + // AUTHENTICATION + signup = "/signup", + login = "/login", + logout = "/logout", + forgot = "/forgotpassword", + reset = "/reset/:token", + +}
\ No newline at end of file diff --git a/src/server/Search.ts b/src/server/Search.ts new file mode 100644 index 000000000..5ca5578a7 --- /dev/null +++ b/src/server/Search.ts @@ -0,0 +1,49 @@ +import * as rp from 'request-promise'; +import { Database } from './database'; +import { thisExpression } from 'babel-types'; + +export class Search { + public static Instance = new Search(); + private url = 'http://localhost:8983/solr/'; + + public async updateDocument(document: any) { + try { + const res = await rp.post(this.url + "dash/update", { + headers: { 'content-type': 'application/json' }, + body: JSON.stringify([document]) + }); + return res; + } catch (e) { + console.warn("Search error: " + e + document); + } + } + + public async search(query: string) { + try { + const searchResults = JSON.parse(await rp.get(this.url + "dash/select", { + qs: { + q: query, + fl: "id" + } + })); + const fields = searchResults.response.docs; + const ids = fields.map((field: any) => field.id); + return ids; + } catch { + return []; + } + } + + public async clear() { + try { + return await rp.post(this.url + "dash/update", { + body: { + delete: { + query: "*:*" + } + }, + json: true + }); + } catch { } + } +}
\ No newline at end of file diff --git a/src/server/ServerUtil.ts b/src/server/ServerUtil.ts deleted file mode 100644 index 5331c9e30..000000000 --- a/src/server/ServerUtil.ts +++ /dev/null @@ -1,71 +0,0 @@ - -import { Field } from './../fields/Field'; -import { TextField } from './../fields/TextField'; -import { NumberField } from './../fields/NumberField'; -import { RichTextField } from './../fields/RichTextField'; -import { Key } from './../fields/Key'; -import { ImageField } from './../fields/ImageField'; -import { ListField } from './../fields/ListField'; -import { Document } from './../fields/Document'; -import { Server } from './../client/Server'; -import { Types } from './Message'; -import { Utils } from '../Utils'; -import { HtmlField } from '../fields/HtmlField'; -import { WebField } from '../fields/WebField'; -import { AudioField } from '../fields/AudioField'; -import { VideoField } from '../fields/VideoField'; -import {InkField} from '../fields/InkField'; -import {PDFField} from '../fields/PDFField'; - - - - -export class ServerUtils { - public static FromJson(json: any): Field { - let obj = json - let data: any = obj.data - let id: string = obj._id - let type: Types = obj.type - - if (!(data !== undefined && id && type !== undefined)) { - console.log("how did you manage to get an object that doesn't have a data or an id?") - return new TextField("Something to fill the space", Utils.GenerateGuid()); - } - - switch (type) { - case Types.Number: - return new NumberField(data, id, false) - case Types.Text: - return new TextField(data, id, false) - case Types.Html: - return new HtmlField(data, id, false) - case Types.Web: - return new WebField(new URL(data), id, false) - case Types.RichText: - return new RichTextField(data, id, false) - case Types.Key: - return new Key(data, id, false) - case Types.Image: - return new ImageField(new URL(data), id, false) - case Types.PDF: - return new PDFField(new URL(data), id, false) - case Types.List: - return ListField.FromJson(id, data) - case Types.Audio: - return new AudioField(new URL(data), id, false) - case Types.Video: - return new VideoField(new URL(data), id, false) - case Types.Ink: - return InkField.FromJson(id, data); - case Types.Document: - let doc: Document = new Document(id, false) - let fields: [string, string][] = data as [string, string][] - fields.forEach(element => { - doc._proxies.set(element[0], element[1]); - }); - return doc - default: - throw Error("Error, unrecognized field type received from server. If you just created a new field type, be sure to add it here"); - } - } -}
\ No newline at end of file diff --git a/src/server/authentication/config/passport.ts b/src/server/authentication/config/passport.ts index 05f6c3133..d42741410 100644 --- a/src/server/authentication/config/passport.ts +++ b/src/server/authentication/config/passport.ts @@ -1,9 +1,10 @@ -import * as passport from 'passport' +import * as passport from 'passport'; import * as passportLocal from 'passport-local'; import * as mongodb from 'mongodb'; import * as _ from "lodash"; -import { default as User } from '../models/User'; +import { default as User } from '../models/user_model'; import { Request, Response, NextFunction } from "express"; +import { RouteStore } from '../../RouteStore'; const LocalStrategy = passportLocal.Strategy; @@ -18,10 +19,10 @@ passport.deserializeUser<any, any>((id, done) => { }); // AUTHENTICATE JUST WITH EMAIL AND PASSWORD -passport.use(new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { +passport.use(new LocalStrategy({ usernameField: 'email', passReqToCallback: true }, (req, email, password, done) => { User.findOne({ email: email.toLowerCase() }, (error: any, user: any) => { if (error) return done(error); - if (!user) return done(undefined, false, { message: "Invalid email or password" }) // invalid email + 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 @@ -35,8 +36,8 @@ export let isAuthenticated = (req: Request, res: Response, next: NextFunction) = if (req.isAuthenticated()) { return next(); } - return res.redirect("/login"); -} + return res.redirect(RouteStore.login); +}; export let isAuthorized = (req: Request, res: Response, next: NextFunction) => { const provider = req.path.split("/").slice(-1)[0]; diff --git a/src/server/authentication/controllers/user.ts b/src/server/authentication/controllers/user.ts deleted file mode 100644 index f74ff9039..000000000 --- a/src/server/authentication/controllers/user.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { default as User, UserModel, AuthToken } from "../models/User"; -import { Request, Response, NextFunction } from "express"; -import * as passport from "passport"; -import { IVerifyOptions } from "passport-local"; -import "../config/passport"; -import * as request from "express-validator"; -const flash = require("express-flash"); -import * as session from "express-session"; -import * as pug from 'pug'; - -/** - * GET /signup - * Signup page. - */ -export let getSignup = (req: Request, res: Response) => { - if (req.user) { - return res.redirect("/"); - } - res.render("signup.pug", { - title: "Sign Up" - }); -}; - -/** - * POST /signup - * Create a new local account. - */ -export let postSignup = (req: Request, res: Response, next: NextFunction) => { - req.assert("email", "Email is not valid").isEmail(); - req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); - req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); - req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); - - const errors = req.validationErrors(); - - if (errors) { - req.flash("errors", "Unable to facilitate sign up. Please try again."); - return res.redirect("/signup"); - } - - const user = new User({ - email: req.body.email, - password: req.body.password - }); - - User.findOne({ email: req.body.email }, (err, existingUser) => { - if (err) { return next(err); } - if (existingUser) { - req.flash("errors", "Account with that email address already exists."); - return res.redirect("/signup"); - } - user.save((err) => { - if (err) { return next(err); } - req.logIn(user, (err) => { - if (err) { - return next(err); - } - res.redirect("/"); - }); - }); - }); -}; - - -/** - * GET /login - * Login page. - */ -export let getLogin = (req: Request, res: Response) => { - if (req.user) { - return res.redirect("/"); - } - res.send("<p>dear lord please render</p>"); - // res.render("account/login", { - // title: "Login" - // }); -}; - -/** - * POST /login - * Sign in using email and password. - */ -export let postLogin = (req: Request, res: Response, next: NextFunction) => { - req.assert("email", "Email is not valid").isEmail(); - req.assert("password", "Password cannot be blank").notEmpty(); - req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); - - const errors = req.validationErrors(); - - if (errors) { - req.flash("errors", "Unable to login at this time. Please try again."); - return res.redirect("/login"); - } - - passport.authenticate("local", (err: Error, user: UserModel, info: IVerifyOptions) => { - if (err) { return next(err); } - if (!user) { - req.flash("errors", info.message); - return res.redirect("/login"); - } - req.logIn(user, (err) => { - if (err) { return next(err); } - req.flash("success", "Success! You are logged in."); - res.redirect("/"); - }); - })(req, res, next); -};
\ No newline at end of file diff --git a/src/server/authentication/controllers/user_controller.ts b/src/server/authentication/controllers/user_controller.ts new file mode 100644 index 000000000..1dacdf3fa --- /dev/null +++ b/src/server/authentication/controllers/user_controller.ts @@ -0,0 +1,266 @@ +import { default as User, DashUserModel, AuthToken } from "../models/user_model"; +import { Request, Response, NextFunction } from "express"; +import * as passport from "passport"; +import { IVerifyOptions } from "passport-local"; +import "../config/passport"; +import * as request from "express-validator"; +import flash = require("express-flash"); +import * as session from "express-session"; +import * as pug from 'pug'; +import * as async from 'async'; +import * as nodemailer from 'nodemailer'; +import c = require("crypto"); +import { RouteStore } from "../../RouteStore"; +import { Utils } from "../../../Utils"; + +/** + * GET /signup + * Directs user to the signup page + * modeled by signup.pug in views + */ +export let getSignup = (req: Request, res: Response) => { + if (req.user) { + let user = req.user; + return res.redirect(RouteStore.home); + } + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); +}; + +/** + * POST /signup + * Create a new local account. + */ +export let postSignup = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); + req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + res.render("signup.pug", { + title: "Sign Up", + user: req.user, + }); + return res.redirect(RouteStore.signup); + } + + const email = req.body.email; + const password = req.body.password; + + const user = new User({ + email, + password, + userDocumentId: Utils.GenerateGuid() + }); + + User.findOne({ email }, (err, existingUser) => { + if (err) { return next(err); } + if (existingUser) { + return res.redirect(RouteStore.login); + } + user.save((err) => { + if (err) { return next(err); } + req.logIn(user, (err) => { + if (err) { + return next(err); + } + res.redirect(RouteStore.home); + }); + }); + }); + +}; + + +/** + * GET /login + * Login page. + */ +export let getLogin = (req: Request, res: Response) => { + if (req.user) { + return res.redirect(RouteStore.home); + } + res.render("login.pug", { + title: "Log In", + user: req.user + }); +}; + +/** + * POST /login + * Sign in using email and password. + * On failure, redirect to signup page + */ +export let postLogin = (req: Request, res: Response, next: NextFunction) => { + req.assert("email", "Email is not valid").isEmail(); + req.assert("password", "Password cannot be blank").notEmpty(); + req.sanitize("email").normalizeEmail({ gmail_remove_dots: false }); + + const errors = req.validationErrors(); + + if (errors) { + req.flash("errors", "Unable to login at this time. Please try again."); + return res.redirect(RouteStore.signup); + } + + passport.authenticate("local", (err: Error, user: DashUserModel, info: IVerifyOptions) => { + if (err) { next(err); return; } + if (!user) { + return res.redirect(RouteStore.signup); + } + req.logIn(user, (err) => { + if (err) { next(err); return; } + res.redirect(RouteStore.home); + }); + })(req, res, next); +}; + +/** + * GET /logout + * Invokes the logout function on the request + * and destroys the user's current session. + */ +export let getLogout = (req: Request, res: Response) => { + req.logout(); + const sess = req.session; + if (sess) { + sess.destroy((err) => { if (err) { console.log(err); } }); + } + res.redirect(RouteStore.login); +}; + +export let getForgot = function (req: Request, res: Response) { + res.render("forgot.pug", { + title: "Recover Password", + user: req.user, + }); +}; + +export let postForgot = function (req: Request, res: Response, next: NextFunction) { + const email = req.body.email; + async.waterfall([ + function (done: any) { + let token: string; + c.randomBytes(20, function (err: any, buffer: Buffer) { + if (err) { + done(null); + return; + } + done(null, buffer.toString('hex')); + }); + }, + function (token: string, done: any) { + User.findOne({ email }, function (err, user: DashUserModel) { + if (!user) { + // NO ACCOUNT WITH SUBMITTED EMAIL + res.redirect(RouteStore.forgot); + 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 + '/reset/' + token + '\n\n' + + 'If you did not request this, please ignore this email and your password will remain unchanged.\n' + }; + smtpTransport.sendMail(mailOptions, function (err) { + // req.flash('info', 'An e-mail has been sent to ' + user.email + ' with further instructions.'); + done(null, err, 'done'); + }); + } + ], function (err) { + if (err) return next(err); + res.redirect(RouteStore.forgot); + }); +}; + +export let getReset = function (req: Request, res: Response) { + User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { + if (!user || err) { + return res.redirect(RouteStore.forgot); + } + res.render("reset.pug", { + title: "Reset Password", + user: req.user, + }); + }); +}; + +export let postReset = function (req: Request, res: Response) { + async.waterfall([ + function (done: any) { + User.findOne({ passwordResetToken: req.params.token, passwordResetExpires: { $gt: Date.now() } }, function (err, user: DashUserModel) { + if (!user || err) { + return res.redirect('back'); + } + + req.assert("password", "Password must be at least 4 characters long").len({ min: 4 }); + req.assert("confirmPassword", "Passwords do not match").equals(req.body.password); + + if (req.validationErrors()) { + return res.redirect('back'); + } + + user.password = req.body.password; + user.passwordResetToken = undefined; + user.passwordResetExpires = undefined; + + user.save(function (err) { + if (err) { + res.redirect(RouteStore.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' + }; + smtpTransport.sendMail(mailOptions, function (err) { + done(null, err); + }); + } + ], function (err) { + res.redirect(RouteStore.login); + }); +};
\ No newline at end of file diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts new file mode 100644 index 000000000..aef2d3f4a --- /dev/null +++ b/src/server/authentication/models/current_user_utils.ts @@ -0,0 +1,137 @@ +import { computed, observable, action, runInAction } from "mobx"; +import * as rp from 'request-promise'; +import { Docs } from "../../../client/documents/Documents"; +import { Attribute, AttributeGroup, Catalog, Schema, AggregateFunction } from "../../../client/northstar/model/idea/idea"; +import { ArrayUtil } from "../../../client/northstar/utils/ArrayUtil"; +import { RouteStore } from "../../RouteStore"; +import { DocServer } from "../../../client/DocServer"; +import { Doc, Opt, Field } from "../../../new_fields/Doc"; +import { List } from "../../../new_fields/List"; +import { CollectionViewType } from "../../../client/views/collections/CollectionBaseView"; +import { CollectionTreeView } from "../../../client/views/collections/CollectionTreeView"; +import { CollectionView } from "../../../client/views/collections/CollectionView"; +import { NorthstarSettings, Gateway } from "../../../client/northstar/manager/Gateway"; +import { AttributeTransformationModel } from "../../../client/northstar/core/attribute/AttributeTransformationModel"; +import { ColumnAttributeModel } from "../../../client/northstar/core/attribute/AttributeModel"; +import { HistogramOperation } from "../../../client/northstar/operations/HistogramOperation"; +import { Cast, PromiseValue } from "../../../new_fields/Types"; +import { listSpec } from "../../../new_fields/Schema"; + +export class CurrentUserUtils { + private static curr_email: string; + private static curr_id: string; + @observable private static user_document: Doc; + //TODO tfs: these should be temporary... + private static mainDocId: string | undefined; + + public static get email() { return this.curr_email; } + public static get id() { return this.curr_id; } + @computed public static get UserDocument() { return this.user_document; } + public static get MainDocId() { return this.mainDocId; } + public static set MainDocId(id: string | undefined) { this.mainDocId = id; } + + private static createUserDocument(id: string): Doc { + let doc = new Doc(id, true); + doc.viewType = CollectionViewType.Tree; + doc.layout = CollectionView.LayoutString(); + doc.title = this.email; + doc.data = new List<Doc>(); + doc.excludeFromLibrary = true; + doc.optionalRightCollection = Docs.SchemaDocument(["title"], [], { title: "Pending documents" }); + // doc.library = Docs.TreeDocument([doc], { title: `Library: ${CurrentUserUtils.email}` }); + // (doc.library as Doc).excludeFromLibrary = true; + return doc; + } + + public static async loadCurrentUser(): Promise<any> { + let userPromise = rp.get(DocServer.prepend(RouteStore.getCurrUser)).then(response => { + if (response) { + let obj = JSON.parse(response); + CurrentUserUtils.curr_id = obj.id as string; + CurrentUserUtils.curr_email = obj.email as string; + } else { + throw new Error("There should be a user! Why does Dash think there isn't one?"); + } + }); + let userDocPromise = await rp.get(DocServer.prepend(RouteStore.getUserDocumentId)).then(id => { + if (id) { + return DocServer.GetRefField(id).then(field => + runInAction(() => this.user_document = field instanceof Doc ? field : this.createUserDocument(id))); + } else { + throw new Error("There should be a user id! Why does Dash think there isn't one?"); + } + }); + try { + const getEnvironment = await fetch("/assets/env.json", { redirect: "follow", method: "GET", credentials: "include" }); + NorthstarSettings.Instance.UpdateEnvironment(await getEnvironment.json()); + await Gateway.Instance.ClearCatalog(); + const extraSchemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); + let extras = await Promise.all(extraSchemas.map(sc => Gateway.Instance.GetSchema("", sc))); + let catprom = CurrentUserUtils.SetNorthstarCatalog(await Gateway.Instance.GetCatalog(), extras); + if (catprom) await Promise.all(catprom); + } catch (e) { + + } + return Promise.all([userPromise, userDocPromise]); + } + + /* Northstar catalog ... really just for testing so this should eventually go away */ + // --------------- Northstar hooks ------------- / + static _northstarSchemas: Doc[] = []; + @observable private static _northstarCatalog?: Catalog; + @computed public static get NorthstarDBCatalog() { return this._northstarCatalog; } + + @action static SetNorthstarCatalog(ctlog: Catalog, extras: Catalog[]) { + CurrentUserUtils.NorthstarDBCatalog = ctlog; + if (ctlog && ctlog.schemas) { + extras.map(ex => ctlog.schemas!.push(ex)); + return ctlog.schemas.map(async schema => { + let schemaDocuments: Doc[] = []; + let attributesToBecomeDocs = CurrentUserUtils.GetAllNorthstarColumnAttributes(schema); + await Promise.all(attributesToBecomeDocs.reduce((promises, attr) => { + promises.push(DocServer.GetRefField(attr.displayName! + ".alias").then(action((field: Opt<Field>) => { + if (field instanceof Doc) { + schemaDocuments.push(field); + } else { + var atmod = new ColumnAttributeModel(attr); + let histoOp = new HistogramOperation(schema.displayName!, + new AttributeTransformationModel(atmod, AggregateFunction.None), + new AttributeTransformationModel(atmod, AggregateFunction.Count), + new AttributeTransformationModel(atmod, AggregateFunction.Count)); + schemaDocuments.push(Docs.HistogramDocument(histoOp, { width: 200, height: 200, title: attr.displayName! })); + } + }))); + return promises; + }, [] as Promise<void>[])); + return CurrentUserUtils._northstarSchemas.push(Docs.TreeDocument(schemaDocuments, { width: 50, height: 100, title: schema.displayName! })); + }); + } + } + public static set NorthstarDBCatalog(ctlog: Catalog | undefined) { this._northstarCatalog = ctlog; } + + public static AddNorthstarSchema(schema: Schema, schemaDoc: Doc) { + if (this._northstarCatalog && CurrentUserUtils._northstarSchemas) { + this._northstarCatalog.schemas!.push(schema); + CurrentUserUtils._northstarSchemas.push(schemaDoc); + let schemas = Cast(CurrentUserUtils.UserDocument.DBSchemas, listSpec("string"), []); + schemas.push(schema.displayName!); + CurrentUserUtils.UserDocument.DBSchemas = new List<string>(schemas); + } + } + public static GetNorthstarSchema(name: string): Schema | undefined { + return !this._northstarCatalog || !this._northstarCatalog.schemas ? undefined : + ArrayUtil.FirstOrDefault<Schema>(this._northstarCatalog.schemas, (s: Schema) => s.displayName === name); + } + public static GetAllNorthstarColumnAttributes(schema: Schema) { + const recurs = (attrs: Attribute[], g?: AttributeGroup) => { + if (g && g.attributes) { + attrs.push.apply(attrs, g.attributes); + if (g.attributeGroups) { + g.attributeGroups.forEach(ng => recurs(attrs, ng)); + } + } + return attrs; + }; + return recurs([] as Attribute[], schema ? schema.rootAttributeGroup : undefined); + } +}
\ No newline at end of file diff --git a/src/server/authentication/models/User.ts b/src/server/authentication/models/user_model.ts index 9752c4260..ee85e1c05 100644 --- a/src/server/authentication/models/User.ts +++ b/src/server/authentication/models/user_model.ts @@ -1,9 +1,8 @@ //@ts-ignore import * as bcrypt from "bcrypt-nodejs"; -import * as crypto from "crypto"; //@ts-ignore import * as mongoose from "mongoose"; -var url = 'mongodb://localhost:27017/Dash' +var url = 'mongodb://localhost:27017/Dash'; mongoose.connect(url, { useNewUrlParser: true }); @@ -16,12 +15,13 @@ mongoose.connection.on('error', function (error) { mongoose.connection.on('disconnected', function () { console.log('connection closed'); }); -export type UserModel = mongoose.Document & { +export type DashUserModel = mongoose.Document & { email: string, password: string, - passwordResetToken: string, - passwordResetExpires: Date, - tokens: AuthToken[], + passwordResetToken?: string, + passwordResetExpires?: Date, + + userDocumentId: string; profile: { name: string, @@ -47,10 +47,11 @@ const userSchema = new mongoose.Schema({ passwordResetToken: String, passwordResetExpires: Date, + userDocumentId: String, + facebook: String, twitter: String, google: String, - tokens: Array, profile: { name: String, @@ -65,22 +66,26 @@ const userSchema = new mongoose.Schema({ * Password hash middleware. */ userSchema.pre("save", function save(next) { - const user = this as UserModel; - if (!user.isModified("password")) { return next(); } + const user = this as DashUserModel; + if (!user.isModified("password")) { + return next(); + } bcrypt.genSalt(10, (err, salt) => { - if (err) { return next(err); } + if (err) { + return next(err); + } bcrypt.hash(user.password, salt, () => void {}, (err: mongoose.Error, hash) => { - if (err) { return next(err); } + if (err) { + return next(err); + } user.password = hash; next(); }); }); }); -const comparePassword: comparePasswordFunction = function (this: UserModel, candidatePassword, cb) { - bcrypt.compare(candidatePassword, this.password, (err: mongoose.Error, isMatch: boolean) => { - cb(err, isMatch); - }); +const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { + bcrypt.compare(candidatePassword, this.password, cb); }; userSchema.methods.comparePassword = comparePassword; diff --git a/src/server/database.ts b/src/server/database.ts index 07c5819ab..69005d2d3 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -1,81 +1,100 @@ -import { action, configure } from 'mobx'; import * as mongodb from 'mongodb'; -import { ObjectID } from 'mongodb'; import { Transferable } from './Message'; -import { Utils } from '../Utils'; export class Database { - public static Instance = new Database() + public static DocumentsCollection = 'documents'; + public static Instance = new Database(); private MongoClient = mongodb.MongoClient; private url = 'mongodb://localhost:27017/Dash'; + private currentWrites: { [id: string]: Promise<void> } = {}; private db?: mongodb.Db; constructor() { - this.MongoClient.connect(this.url, (err, client) => { - this.db = client.db() - }) + this.MongoClient.connect(this.url, (err, client) => this.db = client.db()); } - public update(id: string, value: any) { + public update(id: string, value: any, callback: () => void, upsert = true, collectionName = Database.DocumentsCollection) { if (this.db) { - let collection = this.db.collection('documents'); - collection.update({ _id: id }, { $set: value }, { - upsert: true - }); + let collection = this.db.collection(collectionName); + const prom = this.currentWrites[id]; + let newProm: Promise<void>; + const run = (): Promise<void> => { + return new Promise<void>(resolve => { + collection.updateOne({ _id: id }, value, { upsert } + , (err, res) => { + if (this.currentWrites[id] === newProm) { + delete this.currentWrites[id]; + } + resolve(); + callback(); + }); + }); + }; + newProm = prom ? prom.then(run) : run(); + this.currentWrites[id] = newProm; } } - public delete(id: string) { - if (this.db) { - let collection = this.db.collection('documents'); - collection.remove({ _id: id }); - } + public delete(id: string, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).remove({ id: id }); } - public deleteAll() { - if (this.db) { - let collection = this.db.collection('documents'); - collection.deleteMany({}); - } + public deleteAll(collectionName = Database.DocumentsCollection): Promise<any> { + return new Promise(res => + this.db && this.db.collection(collectionName).deleteMany({}, res)); } - public insert(kvpairs: any) { - if (this.db) { - let collection = this.db.collection('documents'); - collection.insertOne(kvpairs, (err: any, res: any) => { - if (err) { - // console.log(err) - return - } - }); + public insert(value: any, collectionName = Database.DocumentsCollection) { + if (!this.db) { return; } + if ("id" in value) { + value._id = value.id; + delete value.id; } + const id = value._id; + const collection = this.db.collection(collectionName); + const prom = this.currentWrites[id]; + let newProm: Promise<void>; + const run = (): Promise<void> => { + return new Promise<void>(resolve => { + collection.insertOne(value, (err, res) => { + if (this.currentWrites[id] === newProm) { + delete this.currentWrites[id]; + } + resolve(); + }); + }); + }; + newProm = prom ? prom.then(run) : run(); + this.currentWrites[id] = newProm; } - public getDocument(id: string, fn: (res: any) => void) { - var result: JSON; - if (this.db) { - let collection = this.db.collection('documents'); - collection.findOne({ _id: id }, (err: any, res: any) => { - result = res - if (!result) { - fn(undefined) - } - fn(result) - }) - }; + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).findOne({ _id: id }, (err, result) => { + if (result) { + result.id = result._id; + delete result._id; + fn(result); + } else { + fn(undefined); + } + }); } - public getDocuments(ids: string[], fn: (res: any) => void) { - if (this.db) { - let collection = this.db.collection('documents'); - let cursor = collection.find({ _id: { "$in": ids } }) - cursor.toArray((err, docs) => { - fn(docs); - }) - }; + public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) { + this.db && this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => { + if (err) { + console.log(err.message); + console.log(err.errmsg); + } + fn(docs.map(doc => { + doc.id = doc._id; + delete doc._id; + return doc; + })); + }); } public print() { - console.log("db says hi!") + console.log("db says hi!"); } } diff --git a/src/server/index.ts b/src/server/index.ts index 0d0b65b22..da6bc0165 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,68 +1,66 @@ -import * as express from 'express' -const app = express() -import * as webpack from 'webpack' -import * as wdm from 'webpack-dev-middleware'; -import * as whm from 'webpack-hot-middleware'; -import * as path from 'path' -import * as formidable from 'formidable' +import * as bodyParser from 'body-parser'; +import { exec } from 'child_process'; +import * as cookieParser from 'cookie-parser'; +import * as express from 'express'; +import * as session from 'express-session'; +import * as expressValidator from 'express-validator'; +import * as formidable from 'formidable'; +import * as fs from 'fs'; +import * as mobileDetect from 'mobile-detect'; +import { ObservableMap } from 'mobx'; import * as passport from 'passport'; -import { MessageStore, Message, SetFieldArgs, GetFieldArgs, Transferable } from "./Message"; -import { Client } from './Client'; +import * as path from 'path'; +import * as request from 'request'; +import * as rp from 'request-promise'; +import * as io from 'socket.io'; import { Socket } from 'socket.io'; +import * as webpack from 'webpack'; +import * as wdm from 'webpack-dev-middleware'; +import * as whm from 'webpack-hot-middleware'; import { Utils } from '../Utils'; -import { ObservableMap } from 'mobx'; -import { FieldId, Field } from '../fields/Field'; +import { getForgot, getLogin, getLogout, getReset, getSignup, postForgot, postLogin, postReset, postSignup } from './authentication/controllers/user_controller'; +import { DashUserModel } from './authentication/models/user_model'; +import { Client } from './Client'; import { Database } from './database'; -import { ServerUtils } from './ServerUtil'; -import { ObjectID } from 'mongodb'; -import { Document } from '../fields/Document'; -import * as io from 'socket.io' -import * as passportConfig from './authentication/config/passport'; -import { getLogin, postLogin, getSignup, postSignup } from './authentication/controllers/user'; +import { MessageStore, Transferable, Types, Diff } from "./Message"; +import { RouteStore } from './RouteStore'; +const app = express(); const config = require('../../webpack.config'); const compiler = webpack(config); const port = 1050; // default port to listen -const serverPort = 1234; -import * as expressValidator from 'express-validator'; +const serverPort = 4321; import expressFlash = require('express-flash'); -import * as bodyParser from 'body-parser'; -import * as session from 'express-session'; +import flash = require('connect-flash'); import c = require("crypto"); +import { Search } from './Search'; +import { debug } from 'util'; +import _ = require('lodash'); const MongoStore = require('connect-mongo')(session); const mongoose = require('mongoose'); -const bluebird = require('bluebird'); -import { performance } from 'perf_hooks' -import * as fs from 'fs'; -import * as request from 'request' -const download = (url: string, dest: fs.PathLike) => { - request.get(url).pipe(fs.createWriteStream(dest)); -} +const download = (url: string, dest: fs.PathLike) => request.get(url).pipe(fs.createWriteStream(dest)); const mongoUrl = 'mongodb://localhost:27017/Dash'; -// mongoose.Promise = bluebird; -mongoose.connect(mongoUrl)//.then( -// () => { /** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }, -// ).catch((err: any) => { -// console.log("MongoDB connection error. Please make sure MongoDB is running. " + err); -// process.exit(); -// }); -mongoose.connection.on('connected', function () { - console.log("connected"); -}) +mongoose.connect(mongoUrl); +mongoose.connection.on('connected', () => console.log("connected")); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); -app.use(expressValidator()); -app.use(expressFlash()); -app.use(require('express-session')({ - secret: `${c.randomBytes(64)}`, +// SESSION MANAGEMENT AND AUTHENTICATION MIDDLEWARE +// ORDER OF IMPORTS MATTERS + +app.use(cookieParser()); +app.use(session({ + secret: "64d6866242d3b5a5503c675b32c9605e4e90478e9b77bcf2bc", resave: true, + cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 }, saveUninitialized: true, - store: new MongoStore({ - url: 'mongodb://localhost:27017/Dash' - }) + store: new MongoStore({ url: 'mongodb://localhost:27017/Dash' }) })); + +app.use(flash()); +app.use(expressFlash()); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); +app.use(expressValidator()); app.use(passport.initialize()); app.use(passport.session()); app.use((req, res, next) => { @@ -70,111 +68,311 @@ app.use((req, res, next) => { next(); }); -app.get("/signup", getSignup); -app.post("/signup", postSignup); -app.get("/login", getLogin); -app.post("/login", postLogin); - -// IMAGE UPLOADING HANDLER -app.post("/upload", (req, res, err) => { - let form = new formidable.IncomingForm() - form.uploadDir = __dirname + "/public/files/" - form.keepExtensions = true - // let path = req.body.path; - console.log("upload") - form.parse(req, (err, fields, files) => { - console.log("parsing") - let names: any[] = []; - for (const name in files) { - let file = files[name]; - names.push(`/files/` + path.basename(file.path)); +app.get("/hello", (req, res) => res.send("<p>Hello</p>")); + +enum Method { + GET, + POST +} + +/** + * Please invoke this function when adding a new route to Dash's server. + * It ensures that any requests leading to or containing user-sensitive information + * does not execute unless Passport authentication detects a user logged in. + * @param method whether or not the request is a GET or a POST + * @param handler the action to invoke, recieving a DashUserModel and, as expected, the Express.Request and Express.Response + * @param onRejection an optional callback invoked on return if no user is found to be logged in + * @param subscribers the forward slash prepended path names (reference and add to RouteStore.ts) that will all invoke the given @param handler + */ +function addSecureRoute(method: Method, + handler: (user: DashUserModel, res: express.Response, req: express.Request) => void, + onRejection: (res: express.Response) => any = (res) => res.redirect(RouteStore.logout), + ...subscribers: string[] +) { + let abstracted = (req: express.Request, res: express.Response) => { + if (req.user) { + handler(req.user, res, req); + } else { + onRejection(res); + } + }; + subscribers.forEach(route => { + switch (method) { + case Method.GET: + app.get(route, abstracted); + break; + case Method.POST: + app.post(route, abstracted); + break; } - res.send(names); }); -}) +} -app.use(express.static(__dirname + '/public')); -app.use('/images', express.static(__dirname + '/public')) +// STATIC FILE SERVING +app.use(express.static(__dirname + RouteStore.public)); +app.use(RouteStore.images, express.static(__dirname + RouteStore.public)); -let FieldStore: ObservableMap<FieldId, Field> = new ObservableMap(); +app.get("/pull", (req, res) => + exec('"C:\\Program Files\\Git\\git-bash.exe" -c "git pull"', (err, stdout, stderr) => { + if (err) { + res.send(err.message); + return; + } + res.redirect("/"); + })); -// define a route handler for the default home page -app.get("/", (req, res) => { - res.sendFile(path.join(__dirname, '../../deploy/index.html')); -}); +// SEARCH -app.get("/hello", (req, res) => { - res.send("<p>Hello</p>"); -}) +// GETTERS -app.use("/corsProxy", (req, res) => { - req.pipe(request(req.url.substring(1))).pipe(res); +app.get("/search", async (req, res) => { + let query = req.query.query || "hello"; + let results = await Search.Instance.search(query); + res.send(results); }); -app.get("/delete", (req, res) => { - deleteAll(); - res.redirect("/"); -}); +// anyone attempting to navigate to localhost at this port will +// first have to login +addSecureRoute( + Method.GET, + (user, res) => res.redirect(RouteStore.home), + undefined, + RouteStore.root +); + +addSecureRoute( + Method.GET, + (user, res, req) => { + let detector = new mobileDetect(req.headers['user-agent'] || ""); + let filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; + res.sendFile(path.join(__dirname, '../../deploy/' + filename)); + }, + undefined, + RouteStore.home, + RouteStore.openDocumentWithId +); -app.use(wdm(compiler, { - publicPath: config.output.publicPath -})) +addSecureRoute( + Method.GET, + (user, res) => res.send(user.userDocumentId || ""), + undefined, + RouteStore.getUserDocumentId, +); -app.use(whm(compiler)) +addSecureRoute( + Method.GET, + (user, res) => res.send(JSON.stringify({ id: user.id, email: user.email })), + undefined, + RouteStore.getCurrUser +); + +// SETTERS + +addSecureRoute( + Method.POST, + (user, res, req) => { + let form = new formidable.IncomingForm(); + form.uploadDir = __dirname + "/public/files/"; + form.keepExtensions = true; + // let path = req.body.path; + console.log("upload"); + form.parse(req, (err, fields, files) => { + console.log("parsing"); + let names: string[] = []; + for (const name in files) { + names.push(`/files/` + path.basename(files[name].path)); + } + res.send(names); + }); + }, + undefined, + RouteStore.upload +); + +// AUTHENTICATION + +// Sign Up +app.get(RouteStore.signup, getSignup); +app.post(RouteStore.signup, postSignup); + +// Log In +app.get(RouteStore.login, getLogin); +app.post(RouteStore.login, postLogin); + +// Log Out +app.get(RouteStore.logout, getLogout); + +// FORGOT PASSWORD EMAIL HANDLING +app.get(RouteStore.forgot, getForgot); +app.post(RouteStore.forgot, postForgot); + +// RESET PASSWORD EMAIL HANDLING +app.get(RouteStore.reset, getReset); +app.post(RouteStore.reset, postReset); + +app.use(RouteStore.corsProxy, (req, res) => + req.pipe(request(req.url.substring(1))).pipe(res)); + +app.get(RouteStore.delete, (req, res) => + deleteFields().then(() => res.redirect(RouteStore.home))); + +app.get(RouteStore.deleteAll, (req, res) => + deleteAll().then(() => res.redirect(RouteStore.home))); + +app.use(wdm(compiler, { publicPath: config.output.publicPath })); + +app.use(whm(compiler)); // start the Express server -app.listen(port, () => { - console.log(`server started at http://localhost:${port}`); -}) +app.listen(port, () => + console.log(`server started at http://localhost:${port}`)); const server = io(); interface Map { [key: string]: Client; } -let clients: Map = {} +let clients: Map = {}; server.on("connection", function (socket: Socket) { - console.log("a user has connected") + console.log("a user has connected"); - Utils.Emit(socket, MessageStore.Foo, "handshooken") + Utils.Emit(socket, MessageStore.Foo, "handshooken"); - Utils.AddServerHandler(socket, MessageStore.Bar, barReceived) - Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)) - Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField) - Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields) - Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteAll) -}) + Utils.AddServerHandler(socket, MessageStore.Bar, barReceived); + Utils.AddServerHandler(socket, MessageStore.SetField, (args) => setField(socket, args)); + Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); + Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); + Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); -function deleteAll() { - Database.Instance.deleteAll(); + Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); + Utils.AddServerHandler(socket, MessageStore.UpdateField, diff => UpdateField(socket, diff)); + Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); + Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); +}); + +async function deleteFields() { + await Database.Instance.deleteAll(); + await Search.Instance.clear(); + await Database.Instance.deleteAll('newDocuments'); +} + +async function deleteAll() { + await Database.Instance.deleteAll(); + await Database.Instance.deleteAll('newDocuments'); + await Database.Instance.deleteAll('sessions'); + await Database.Instance.deleteAll('users'); + await Search.Instance.clear(); } function barReceived(guid: String) { clients[guid.toString()] = new Client(guid.toString()); } -function addDocument(document: Document) { +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 getField([id, callback]: [string, (result: any) => void]) { - Database.Instance.getDocument(id, (result: any) => { - if (result) { - callback(result) +function setField(socket: Socket, newValue: Transferable) { + Database.Instance.update(newValue.id, newValue, () => + socket.broadcast.emit(MessageStore.SetField.Message, newValue)); + if (newValue.type === Types.Text) { + Search.Instance.updateDocument({ id: newValue.id, data: (newValue as any).data }); + console.log("set field"); + } +} + +function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { + Database.Instance.getDocument(id, callback, "newDocuments"); +} + +function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { + Database.Instance.getDocuments(ids, callback, "newDocuments"); +} + + +const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { + "number": "_n", + "string": "_t", + // "boolean": "_b", + "image": ["_t", "url"], + "video": ["_t", "url"], + "pdf": ["_t", "url"], + "audio": ["_t", "url"], + "web": ["_t", "url"], + "date": ["_d", value => new Date(value.date).toISOString()], + "proxy": ["_i", "fieldId"], + "list": ["_l", list => { + const results = []; + for (const value of list.fields) { + const term = ToSearchTerm(value); + if (term) { + results.push(term.value); + } } - else { - callback(undefined) + 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 getFields([ids, callback]: [string[], (result: any) => void]) { - Database.Instance.getDocuments(ids, callback); +function getSuffix(value: string | [string, any]): string { + return typeof value === "string" ? value : value[0]; } -function setField(socket: Socket, newValue: Transferable) { - Database.Instance.update(newValue._id, newValue) - socket.broadcast.emit(MessageStore.SetField.Message, newValue) +function UpdateField(socket: Socket, diff: Diff) { + Database.Instance.update(diff.id, diff.diff, + () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); + const docfield = diff.diff.$set; + if (!docfield) { + return; + } + const update: any = { id: diff.id }; + let dynfield = false; + for (let key in docfield) { + if (!key.startsWith("fields.")) continue; + dynfield = true; + let val = docfield[key]; + key = key.substring(7); + Object.values(suffixMap).forEach(suf => update[key + getSuffix(suf)] = { set: null }); + let term = ToSearchTerm(val); + if (term !== undefined) { + let { suffix, value } = term; + update[key + suffix] = { set: value }; + } + } + if (dynfield) { + Search.Instance.updateDocument(update); + } +} + +function CreateField(newValue: any) { + Database.Instance.insert(newValue, "newDocuments"); } server.listen(serverPort); diff --git a/src/server/public/files/.gitignore b/src/server/public/files/.gitignore new file mode 100644 index 000000000..f59ec20aa --- /dev/null +++ b/src/server/public/files/.gitignore @@ -0,0 +1 @@ +*
\ No newline at end of file |