diff options
Diffstat (limited to 'src/server')
-rw-r--r-- | src/server/ApiManagers/UserManager.ts | 77 | ||||
-rw-r--r-- | src/server/RouteManager.ts | 58 | ||||
-rw-r--r-- | src/server/authentication/AuthenticationManager.ts | 31 | ||||
-rw-r--r-- | src/server/authentication/DashUserModel.ts | 106 | ||||
-rw-r--r-- | src/server/index.ts | 108 | ||||
-rw-r--r-- | src/server/websocket.ts | 194 |
6 files changed, 296 insertions, 278 deletions
diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index 7be8a1e9f..53e55c1c3 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -1,10 +1,10 @@ -import ApiManager, { Registration } from "./ApiManager"; -import { Method } from "../RouteManager"; -import { Database } from "../database"; -import { msToTime } from "../ActionUtilities"; -import * as bcrypt from "bcrypt-nodejs"; -import { Opt } from "../../fields/Doc"; -import { WebSocket } from "../websocket"; +import ApiManager, { Registration } from './ApiManager'; +import { Method } from '../RouteManager'; +import { Database } from '../database'; +import { msToTime } from '../ActionUtilities'; +import * as bcrypt from 'bcrypt-nodejs'; +import { Opt } from '../../fields/Doc'; +import { WebSocket } from '../websocket'; export const timeMap: { [id: string]: number } = {}; interface ActivityUnit { @@ -13,28 +13,26 @@ interface ActivityUnit { } export default class UserManager extends ApiManager { - protected initialize(register: Registration): void { - register({ method: Method.GET, - subscription: "/getUsers", + subscription: '/getUsers', secureHandler: async ({ res }) => { - const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, "users"); + const cursor = await Database.Instance.query({}, { email: 1, linkDatabaseId: 1, sharingDocumentId: 1 }, 'users'); const results = await cursor.toArray(); res.send(results.map((user: any) => ({ email: user.email, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }))); - } + }, }); register({ method: Method.POST, - subscription: "/setCacheDocumentIds", + subscription: '/setCacheDocumentIds', secureHandler: async ({ user, req, res }) => { const result: any = {}; user.cacheDocumentIds = req.body.cacheDocumentIds; user.save(err => { if (err) { - result.error = [{ msg: "Error while caching documents" }]; + result.error = [{ msg: 'Error while caching documents' }]; } }); @@ -42,32 +40,35 @@ export default class UserManager extends ApiManager { // console.log(e); // }); res.send(result); - } + }, }); register({ method: Method.GET, - subscription: "/getUserDocumentIds", - secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }) + subscription: '/getUserDocumentIds', + secureHandler: ({ res, user }) => res.send({ userDocumentId: user.userDocumentId, linkDatabaseId: user.linkDatabaseId, sharingDocumentId: user.sharingDocumentId }), + publicHandler: ({ res }) => res.send({ userDocumentId: '__guest__', linkDatabaseId: 3, sharingDocumentId: 2 }), }); register({ method: Method.GET, - subscription: "/getSharingDocumentId", - secureHandler: ({ res, user }) => res.send(user.sharingDocumentId) + subscription: '/getSharingDocumentId', + secureHandler: ({ res, user }) => res.send(user.sharingDocumentId), + publicHandler: ({ res }) => res.send(2), }); register({ method: Method.GET, - subscription: "/getLinkDatabaseId", - secureHandler: ({ res, user }) => res.send(user.linkDatabaseId) + subscription: '/getLinkDatabaseId', + secureHandler: ({ res, user }) => res.send(user.linkDatabaseId), + publicHandler: ({ res }) => res.send(3), }); register({ method: Method.GET, - subscription: "/getCurrentUser", + subscription: '/getCurrentUser', secureHandler: ({ res, user: { _id, email, cacheDocumentIds } }) => res.send(JSON.stringify({ id: _id, email, cacheDocumentIds })), - publicHandler: ({ res }) => res.send(JSON.stringify({ id: "__guest__", email: "" })) + publicHandler: ({ res }) => res.send(JSON.stringify({ id: '__guest__', email: 'guest' })), }); register({ @@ -80,7 +81,7 @@ export default class UserManager extends ApiManager { const validated = await new Promise<Opt<boolean>>(resolve => { bcrypt.compare(curr_pass, user.password, (err, passwords_match) => { if (err || !passwords_match) { - result.error = [{ msg: "Incorrect current password" }]; + result.error = [{ msg: 'Incorrect current password' }]; res.send(result); resolve(undefined); } else { @@ -93,10 +94,10 @@ export default class UserManager extends ApiManager { return; } - req.assert("new_pass", "Password must be at least 4 characters long").len({ min: 4 }); - req.assert("new_confirm", "Passwords do not match").equals(new_pass); + req.assert('new_pass', 'Password must be at least 4 characters long').len({ min: 4 }); + req.assert('new_confirm', 'Passwords do not match').equals(new_pass); if (curr_pass === new_pass) { - result.error = [{ msg: "Current and new password are the same" }]; + result.error = [{ msg: 'Current and new password are the same' }]; } // was there error in validating new passwords? if (req.validationErrors()) { @@ -113,17 +114,17 @@ export default class UserManager extends ApiManager { user.save(err => { if (err) { - result.error = [{ msg: "Error while saving new password" }]; + result.error = [{ msg: 'Error while saving new password' }]; } }); res.send(result); - } + }, }); register({ method: Method.GET, - subscription: "/activity", + subscription: '/activity', secureHandler: ({ res }) => { const now = Date.now(); @@ -135,25 +136,23 @@ export default class UserManager extends ApiManager { const socketPair = Array.from(WebSocket.socketMap).find(pair => pair[1] === user); if (socketPair && !socketPair[0].disconnected) { const duration = now - time; - const target = (duration / 1000) < (60 * 5) ? activeTimes : inactiveTimes; + const target = duration / 1000 < 60 * 5 ? activeTimes : inactiveTimes; target.push({ user, duration }); } } - const process = (target: { user: string, duration: number }[]) => { + const process = (target: { user: string; duration: number }[]) => { const comparator = (first: ActivityUnit, second: ActivityUnit) => first.duration - second.duration; const sorted = target.sort(comparator); return sorted.map(({ user, duration }) => `${user} (${msToTime(duration)})`); }; - res.render("user_activity.pug", { - title: "User Activity", + res.render('user_activity.pug', { + title: 'User Activity', active: process(activeTimes), - inactive: process(inactiveTimes) + inactive: process(inactiveTimes), }); - } + }, }); - } - -}
\ No newline at end of file +} diff --git a/src/server/RouteManager.ts b/src/server/RouteManager.ts index aa9bfcfa7..5683cd539 100644 --- a/src/server/RouteManager.ts +++ b/src/server/RouteManager.ts @@ -1,12 +1,12 @@ import { cyan, green, red } from 'colors'; import { Express, Request, Response } from 'express'; -import { AdminPriviliges } from "."; -import { DashUserModel } from "./authentication/DashUserModel"; -import RouteSubscriber from "./RouteSubscriber"; +import { AdminPriviliges } from '.'; +import { DashUserModel } from './authentication/DashUserModel'; +import RouteSubscriber from './RouteSubscriber'; export enum Method { GET, - POST + POST, } export interface CoreArguments { @@ -33,13 +33,13 @@ const registered = new Map<string, Set<Method>>(); enum RegistrationError { Malformed, - Duplicate + Duplicate, } export default class RouteManager { private server: Express; private _isRelease: boolean; - private failedRegistrations: { route: string, reason: RegistrationError }[] = []; + private failedRegistrations: { route: string; reason: RegistrationError }[] = []; public get isRelease() { return this._isRelease; @@ -74,39 +74,42 @@ export default class RouteManager { } process.exit(1); } else { - console.log(green("all server routes have been successfully registered:")); - Array.from(registered.keys()).sort().forEach(route => console.log(cyan(route))); + console.log(green('all server routes have been successfully registered:')); + Array.from(registered.keys()) + .sort() + .forEach(route => console.log(cyan(route))); console.log(); } - } + }; static routes: string[] = []; /** - * - * @param initializer + * + * @param initializer */ addSupervisedRoute = (initializer: RouteInitializer): void => { const { method, subscription, secureHandler, publicHandler, errorHandler, requireAdminInRelease: requireAdmin } = initializer; - typeof (initializer.subscription) === "string" && RouteManager.routes.push(initializer.subscription); + typeof initializer.subscription === 'string' && RouteManager.routes.push(initializer.subscription); initializer.subscription instanceof RouteSubscriber && RouteManager.routes.push(initializer.subscription.root); - initializer.subscription instanceof Array && initializer.subscription.map(sub => { - typeof (sub) === "string" && RouteManager.routes.push(sub); - sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root); - }); + initializer.subscription instanceof Array && + initializer.subscription.map(sub => { + typeof sub === 'string' && RouteManager.routes.push(sub); + sub instanceof RouteSubscriber && RouteManager.routes.push(sub.root); + }); const isRelease = this._isRelease; const supervised = async (req: Request, res: Response) => { let user = req.user as Partial<DashUserModel> | undefined; const { originalUrl: target } = req; - if (process.env.DB === "MEM" && !user) { - user = { id: "guest", email: "", userDocumentId: "guestDocId" }; + if (process.env.DB === 'MEM' && !user) { + user = { id: 'guest', email: 'guest', userDocumentId: '__guest__' }; } const core = { req, res, isRelease }; const tryExecute = async (toExecute: (args: any) => any | Promise<any>, args: any) => { try { await toExecute(args); } catch (e) { - console.log(red(target), user && ("email" in user) ? "<user logged out>" : undefined); + console.log(red(target), user && 'email' in user ? '<user logged out>' : undefined); if (errorHandler) { errorHandler({ ...core, error: e }); } else { @@ -119,7 +122,7 @@ export default class RouteManager { if (AdminPriviliges.get(user.id)) { AdminPriviliges.delete(user.id); } else { - return res.redirect(`/admin/${req.originalUrl.substring(1).replace("/", ":")}`); + return res.redirect(`/admin/${req.originalUrl.substring(1).replace('/', ':')}`); } } await tryExecute(secureHandler, { ...core, user }); @@ -128,10 +131,10 @@ export default class RouteManager { if (publicHandler) { await tryExecute(publicHandler, core); if (!res.headersSent) { - res.redirect("/login"); + // res.redirect("/login"); } } else { - res.redirect("/login"); + res.redirect('/login'); } } setTimeout(() => { @@ -144,7 +147,7 @@ export default class RouteManager { }; const subscribe = (subscriber: RouteSubscriber | string) => { let route: string; - if (typeof subscriber === "string") { + if (typeof subscriber === 'string') { route = subscriber; } else { route = subscriber.build; @@ -152,7 +155,7 @@ export default class RouteManager { if (!/^\/$|^\/[A-Za-z\*]+(\/\:[A-Za-z?_\*]+)*$/g.test(route)) { this.failedRegistrations.push({ reason: RegistrationError.Malformed, - route + route, }); } else { const existing = registered.get(route); @@ -160,7 +163,7 @@ export default class RouteManager { if (existing.has(method)) { this.failedRegistrations.push({ reason: RegistrationError.Duplicate, - route + route, }); return; } @@ -184,15 +187,14 @@ export default class RouteManager { } else { subscribe(subscription); } - } - + }; } export const STATUS = { OK: 200, BAD_REQUEST: 400, EXECUTION_ERROR: 500, - PERMISSION_DENIED: 403 + PERMISSION_DENIED: 403, }; export function _error(res: Response, message: string, error?: any) { diff --git a/src/server/authentication/AuthenticationManager.ts b/src/server/authentication/AuthenticationManager.ts index 3622be4c5..52d876e95 100644 --- a/src/server/authentication/AuthenticationManager.ts +++ b/src/server/authentication/AuthenticationManager.ts @@ -1,4 +1,4 @@ -import { default as User, DashUserModel } from './DashUserModel'; +import { default as User, DashUserModel, initializeGuest } from './DashUserModel'; import { Request, Response, NextFunction } from 'express'; import * as passport from 'passport'; import { IVerifyOptions } from 'passport-local'; @@ -30,6 +30,7 @@ export let getSignup = (req: Request, res: Response) => { * Create a new local account. */ export let postSignup = (req: Request, res: Response, next: NextFunction) => { + const email = req.body.email as String; 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); @@ -41,15 +42,14 @@ export let postSignup = (req: Request, res: Response, next: NextFunction) => { return res.redirect('/signup'); } - const email = req.body.email as String; const password = req.body.password; const model = { email, password, - userDocumentId: Utils.GenerateGuid(), - sharingDocumentId: Utils.GenerateGuid(), - linkDatabaseId: Utils.GenerateGuid(), + userDocumentId: email === 'guest' ? '__guest__' : Utils.GenerateGuid(), + sharingDocumentId: email === 'guest' ? 2 : Utils.GenerateGuid(), + linkDatabaseId: email === 'guest' ? 3 : Utils.GenerateGuid(), cacheDocumentIds: '', } as Partial<DashUserModel>; @@ -106,18 +106,22 @@ export let getLogin = (req: Request, res: Response) => { * 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 (req.body.email === '') { + User.findOne({ email: 'guest' }, (err: any, user: DashUserModel) => !user && initializeGuest()); + req.body.email = 'guest'; + req.body.password = 'guest'; + } else { + req.assert('email', 'Email is not valid').isEmail(); + req.assert('password', 'Password cannot be blank').notEmpty(); + req.sanitize('email').normalizeEmail({ gmail_remove_dots: false }); + } - if (errors) { + if (req.validationErrors()) { 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) => { + const callback = (err: Error, user: DashUserModel, _info: IVerifyOptions) => { if (err) { next(err); return; @@ -132,7 +136,8 @@ export let postLogin = (req: Request, res: Response, next: NextFunction) => { } tryRedirectToTarget(req, res); }); - })(req, res, next); + }; + setTimeout(() => passport.authenticate('local', callback)(req, res, next), 500); }; /** diff --git a/src/server/authentication/DashUserModel.ts b/src/server/authentication/DashUserModel.ts index bee28b96d..a1883beab 100644 --- a/src/server/authentication/DashUserModel.ts +++ b/src/server/authentication/DashUserModel.ts @@ -1,13 +1,13 @@ //@ts-ignore -import * as bcrypt from "bcrypt-nodejs"; +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, + email: String; + password: string; + passwordResetToken?: string; + passwordResetExpires?: Date; userDocumentId: string; sharingDocumentId: string; @@ -15,66 +15,74 @@ export type DashUserModel = mongoose.Document & { cacheDocumentIds: string; profile: { - name: string, - gender: string, - location: string, - website: string, - picture: string - }, + name: string; + gender: string; + location: string; + website: string; + picture: string; + }; - comparePassword: comparePasswordFunction, + comparePassword: comparePasswordFunction; }; type comparePasswordFunction = (candidatePassword: string, cb: (err: any, isMatch: any) => {}) => void; export type AuthToken = { - accessToken: string, - kind: string + accessToken: string; + kind: string; }; -const userSchema = new mongoose.Schema({ - email: String, - password: String, - passwordResetToken: String, - passwordResetExpires: Date, +const userSchema = new mongoose.Schema( + { + email: String, + password: String, + passwordResetToken: String, + passwordResetExpires: Date, - userDocumentId: String, // id that identifies a document which hosts all of a user's account data - sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users - linkDatabaseId: String, - cacheDocumentIds: String, // set of document ids to retreive on startup + userDocumentId: String, // id that identifies a document which hosts all of a user's account data + sharingDocumentId: String, // id that identifies a document that stores documents shared to a user, their user color, and any additional info needed to communicate between users + linkDatabaseId: String, + cacheDocumentIds: String, // set of document ids to retreive on startup - facebook: String, - twitter: String, - google: String, + facebook: String, + twitter: String, + google: String, - profile: { - name: String, - gender: String, - location: String, - website: String, - picture: String - } -}, { timestamps: true }); + profile: { + name: String, + gender: String, + location: String, + website: String, + picture: String, + }, + }, + { timestamps: true } +); /** * Password hash middleware. */ -userSchema.pre("save", function save(next) { +userSchema.pre('save', function save(next) { const user = this as DashUserModel; - if (!user.isModified("password")) { + 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); + bcrypt.hash( + user.password, + salt, + () => void {}, + (err: mongoose.Error, hash: string) => { + if (err) { + return next(err); + } + user.password = hash; + next(); } - user.password = hash; - next(); - }); + ); }); }); @@ -88,5 +96,15 @@ const comparePassword: comparePasswordFunction = function (this: DashUserModel, userSchema.methods.comparePassword = comparePassword; -const User = mongoose.model("User", userSchema); -export default User;
\ No newline at end of file +const User = mongoose.model('User', userSchema); +export function initializeGuest() { + new User({ + email: 'guest', + password: 'guest', + userDocumentId: '__guest__', + sharingDocumentId: '2', + linkDatabaseId: '3', + cacheDocumentIds: '', + }).save(); +} +export default User; diff --git a/src/server/index.ts b/src/server/index.ts index f8c32103b..6e6bde3cb 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,35 +1,35 @@ require('dotenv').config(); -import { yellow } from "colors"; +import { yellow } from 'colors'; import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; import * as qs from 'query-string'; -import { log_execution } from "./ActionUtilities"; -import DeleteManager from "./ApiManagers/DeleteManager"; +import { log_execution } from './ActionUtilities'; +import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; -import GeneralGoogleManager from "./ApiManagers/GeneralGoogleManager"; -import GooglePhotosManager from "./ApiManagers/GooglePhotosManager"; -import PDFManager from "./ApiManagers/PDFManager"; +import GeneralGoogleManager from './ApiManagers/GeneralGoogleManager'; +import GooglePhotosManager from './ApiManagers/GooglePhotosManager'; +import PDFManager from './ApiManagers/PDFManager'; import { SearchManager } from './ApiManagers/SearchManager'; -import SessionManager from "./ApiManagers/SessionManager"; -import UploadManager from "./ApiManagers/UploadManager"; +import SessionManager from './ApiManagers/SessionManager'; +import UploadManager from './ApiManagers/UploadManager'; import UserManager from './ApiManagers/UserManager'; import UtilManager from './ApiManagers/UtilManager'; import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; -import { GoogleApiServerUtils } from "./apis/google/GoogleApiServerUtils"; -import { DashSessionAgent } from "./DashSession/DashSessionAgent"; -import { AppliedSessionAgent } from "./DashSession/Session/agents/applied_session_agent"; +import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; +import { DashSessionAgent } from './DashSession/DashSessionAgent'; +import { AppliedSessionAgent } from './DashSession/Session/agents/applied_session_agent'; import { DashUploadUtils } from './DashUploadUtils'; import { Database } from './database'; -import { Logger } from "./ProcessFactory"; +import { Logger } from './ProcessFactory'; import RouteManager, { Method, PublicHandler } from './RouteManager'; import RouteSubscriber from './RouteSubscriber'; import initializeServer, { resolvedPorts } from './server_Initialization'; export const AdminPriviliges: Map<string, boolean> = new Map(); -export const onWindows = process.platform === "win32"; +export const onWindows = process.platform === 'win32'; export let sessionAgent: AppliedSessionAgent; -export const publicDirectory = path.resolve(__dirname, "public"); -export const filesDirectory = path.resolve(publicDirectory, "files"); +export const publicDirectory = path.resolve(__dirname, 'public'); +export const filesDirectory = path.resolve(publicDirectory, 'files'); /** * These are the functions run before the server starts @@ -43,11 +43,11 @@ async function preliminaryFunctions() { await GoogleCredentialsLoader.loadCredentials(); SSL.loadCredentials(); GoogleApiServerUtils.processProjectCredentials(); - if (process.env.DB !== "MEM") { + if (process.env.DB !== 'MEM') { await log_execution({ - startMessage: "attempting to initialize mongodb connection", - endMessage: "connection outcome determined", - action: Database.tryInitializeConnection + startMessage: 'attempting to initialize mongodb connection', + endMessage: 'connection outcome determined', + action: Database.tryInitializeConnection, }); } } @@ -56,27 +56,16 @@ async function preliminaryFunctions() { * Either clustered together as an API manager * or individually referenced below, by the completion * of this function's execution, all routes will - * be registered on the server + * be registered on the server * @param router the instance of the route manager * that will manage the registration of new routes * with the server */ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: RouteManager) { - const managers = [ - new SessionManager(), - new UserManager(), - new UploadManager(), - new DownloadManager(), - new SearchManager(), - new PDFManager(), - new DeleteManager(), - new UtilManager(), - new GeneralGoogleManager(), - new GooglePhotosManager(), - ]; + const managers = [new SessionManager(), new UserManager(), new UploadManager(), new DownloadManager(), new SearchManager(), new PDFManager(), new DeleteManager(), new UtilManager(), new GeneralGoogleManager(), new GooglePhotosManager()]; // initialize API Managers - console.log(yellow("\nregistering server routes...")); + console.log(yellow('\nregistering server routes...')); managers.forEach(manager => manager.register(addSupervisedRoute)); /** @@ -84,88 +73,87 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: */ addSupervisedRoute({ method: Method.GET, - subscription: "/", - secureHandler: ({ res }) => res.redirect("/home") + subscription: '/', + secureHandler: ({ res }) => res.redirect('/home'), }); - addSupervisedRoute({ method: Method.GET, - subscription: "/serverHeartbeat", - secureHandler: ({ res }) => res.send(true) + subscription: '/serverHeartbeat', + secureHandler: ({ res }) => res.send(true), }); addSupervisedRoute({ method: Method.GET, - subscription: "/resolvedPorts", - secureHandler: ({ res }) => res.send(resolvedPorts) + subscription: '/resolvedPorts', + secureHandler: ({ res }) => res.send(resolvedPorts), + publicHandler: ({ res }) => res.send(resolvedPorts), }); const serve: PublicHandler = ({ req, res }) => { - const detector = new mobileDetect(req.headers['user-agent'] || ""); + const detector = new mobileDetect(req.headers['user-agent'] || ''); const filename = detector.mobile() !== null ? 'mobile/image.html' : 'index.html'; res.sendFile(path.join(__dirname, '../../deploy/' + filename)); }; /** - * Serves a simple password input box for any + * Serves a simple password input box for any */ addSupervisedRoute({ method: Method.GET, - subscription: new RouteSubscriber("admin").add("previous_target"), + subscription: new RouteSubscriber('admin').add('previous_target'), secureHandler: ({ res, isRelease }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect("/home"); + return res.redirect('/home'); } - res.render("admin.pug", { title: "Enter Administrator Password" }); - } + res.render('admin.pug', { title: 'Enter Administrator Password' }); + }, }); addSupervisedRoute({ method: Method.POST, - subscription: new RouteSubscriber("admin").add("previous_target"), + subscription: new RouteSubscriber('admin').add('previous_target'), secureHandler: async ({ req, res, isRelease, user: { id } }) => { const { PASSWORD } = process.env; if (!(isRelease && PASSWORD)) { - return res.redirect("/home"); + return res.redirect('/home'); } const { password } = req.body; const { previous_target } = req.params; let redirect: string; if (password === PASSWORD) { AdminPriviliges.set(id, true); - redirect = `/${previous_target.replace(":", "/")}`; + redirect = `/${previous_target.replace(':', '/')}`; } else { redirect = `/admin/${previous_target}`; } res.redirect(redirect); - } + }, }); addSupervisedRoute({ method: Method.GET, - subscription: ["/home", new RouteSubscriber("doc").add("docId")], + subscription: ['/home', new RouteSubscriber('doc').add('docId')], secureHandler: serve, publicHandler: ({ req, res, ...remaining }) => { const { originalUrl: target } = req; - const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === "true"; - const docAccess = target.startsWith("/doc/"); + const sharing = qs.parse(qs.extract(req.originalUrl), { sort: false }).sharing === 'true'; + const docAccess = target.startsWith('/doc/'); // since this is the public handler, there's no meaning of '/home' to speak of // since there's no user logged in, so the only viable operation // for a guest is to look at a shared document - if (sharing && docAccess) { + if (docAccess) { serve({ req, res, ...remaining }); } else { - res.redirect("/login"); + res.redirect('/login'); } - } + }, }); logRegistrationOutcome(); } - /** * This function can be used in two different ways. If not in release mode, * this is simply the logic that is invoked to start the server. In release mode, @@ -174,9 +162,9 @@ function routeSetter({ isRelease, addSupervisedRoute, logRegistrationOutcome }: */ export async function launchServer() { await log_execution({ - startMessage: "\nstarting execution of preliminary functions", - endMessage: "completed preliminary functions\n", - action: preliminaryFunctions + startMessage: '\nstarting execution of preliminary functions', + endMessage: 'completed preliminary functions\n', + action: preliminaryFunctions, }); await initializeServer(routeSetter); } diff --git a/src/server/websocket.ts b/src/server/websocket.ts index 1b7f5919f..9b91a35a6 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -1,24 +1,24 @@ -import * as express from "express"; -import { blue, green } from "colors"; -import { createServer, Server } from "https"; -import { networkInterfaces } from "os"; +import { blue } from 'colors'; +import * as express from 'express'; +import { createServer, Server } from 'https'; +import { networkInterfaces } from 'os'; import * as sio from 'socket.io'; -import { Socket } from "socket.io"; -import { Utils } from "../Utils"; +import { Socket } from 'socket.io'; +import { Opt } from '../fields/Doc'; +import { Utils } from '../Utils'; import { logPort } from './ActionUtilities'; -import { timeMap } from "./ApiManagers/UserManager"; -import { GoogleCredentialsLoader, SSL } from "./apis/google/CredentialsLoader"; -import YoutubeApi from "./apis/youtube/youtubeApiSample"; -import { Client } from "./Client"; -import { Database } from "./database"; -import { DocumentsCollection } from "./IDatabase"; -import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from "./Message"; -import { Search } from "./Search"; +import { timeMap } from './ApiManagers/UserManager'; +import { GoogleCredentialsLoader, SSL } from './apis/google/CredentialsLoader'; +import YoutubeApi from './apis/youtube/youtubeApiSample'; +import { initializeGuest } from './authentication/DashUserModel'; +import { Client } from './Client'; +import { Database } from './database'; +import { DocumentsCollection } from './IDatabase'; +import { Diff, GestureContent, MessageStore, MobileDocumentUploadContent, MobileInkOverlayContent, Transferable, Types, UpdateMobileInkOverlayPositionContent, YoutubeQueryInput, YoutubeQueryTypes } from './Message'; +import { Search } from './Search'; import { resolvedPorts } from './server_Initialization'; -import { Opt } from "../fields/Doc"; export namespace WebSocket { - export let _socket: Socket; const clients: { [key: string]: Client } = {}; export const socketMap = new Map<SocketIO.Socket, string>(); @@ -32,14 +32,14 @@ export namespace WebSocket { resolvedPorts.socket = Number(socketPort); } let socketEndpoint: Opt<Server>; - await new Promise<void>(resolve => socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve)); + await new Promise<void>(resolve => (socketEndpoint = createServer(SSL.Credentials, app).listen(resolvedPorts.socket, resolve))); io = sio(socketEndpoint!, SSL.Credentials as any); } else { io = sio().listen(resolvedPorts.socket); } - logPort("websocket", resolvedPorts.socket); + logPort('websocket', resolvedPorts.socket); - io.on("connection", function (socket: Socket) { + io.on('connection', function (socket: Socket) { _socket = socket; socket.use((_packet, next) => { const userEmail = socketMap.get(socket); @@ -70,14 +70,14 @@ export namespace WebSocket { 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 + } else { + // max two clients socket.emit('full', room); } }); @@ -97,10 +97,10 @@ export namespace WebSocket { console.log('received bye'); }); - Utils.Emit(socket, MessageStore.Foo, "handshooken"); + 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.AddServerHandler(socket, MessageStore.SetField, args => setField(socket, args)); Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); if (isRelease) { @@ -126,26 +126,26 @@ export namespace WebSocket { */ disconnect = () => { - socket.broadcast.emit("connection_terminated", Date.now()); + socket.broadcast.emit('connection_terminated', Date.now()); socket.disconnect(true); }; }); } function processGesturePoints(socket: Socket, content: GestureContent) { - socket.broadcast.emit("receiveGesturePoints", content); + socket.broadcast.emit('receiveGesturePoints', content); } function processOverlayTrigger(socket: Socket, content: MobileInkOverlayContent) { - socket.broadcast.emit("receiveOverlayTrigger", content); + socket.broadcast.emit('receiveOverlayTrigger', content); } function processUpdateOverlayPosition(socket: Socket, content: UpdateMobileInkOverlayPositionContent) { - socket.broadcast.emit("receiveUpdateOverlayPosition", content); + socket.broadcast.emit('receiveUpdateOverlayPosition', content); } function processMobileDocumentUpload(socket: Socket, content: MobileDocumentUploadContent) { - socket.broadcast.emit("receiveMobileDocumentUpload", content); + socket.broadcast.emit('receiveMobileDocumentUpload', content); } function HandleYoutubeQuery([query, callback]: [YoutubeQueryInput, (result?: any[]) => void]) { @@ -165,27 +165,22 @@ export namespace WebSocket { const target: string[] = []; onlyFields && target.push(DocumentsCollection); await Database.Instance.dropSchema(...target); - if (process.env.DISABLE_SEARCH !== "true") { + if (process.env.DISABLE_SEARCH !== 'true') { await Search.clear(); } + initializeGuest(); } function barReceived(socket: SocketIO.Socket, userEmail: string) { clients[userEmail] = new Client(userEmail.toString()); const currentdate = new Date(); - const datetime = currentdate.getDate() + "/" - + (currentdate.getMonth() + 1) + "/" - + currentdate.getFullYear() + " @ " - + currentdate.getHours() + ":" - + currentdate.getMinutes() + ":" - + currentdate.getSeconds(); + const datetime = currentdate.getDate() + '/' + (currentdate.getMonth() + 1) + '/' + currentdate.getFullYear() + ' @ ' + currentdate.getHours() + ':' + currentdate.getMinutes() + ':' + currentdate.getSeconds(); console.log(blue(`user ${userEmail} has connected to the web socket at: ${datetime}`)); - socketMap.set(socket, userEmail + " at " + datetime); + socketMap.set(socket, userEmail + ' at ' + datetime); } function getField([id, callback]: [string, (result?: Transferable) => void]) { - Database.Instance.getDocument(id, (result?: Transferable) => - callback(result ? result : undefined)); + Database.Instance.getDocument(id, (result?: Transferable) => callback(result ? result : undefined)); } function getFields([ids, callback]: [string[], (result: Transferable[]) => void]) { @@ -193,9 +188,9 @@ export namespace WebSocket { } function setField(socket: Socket, newValue: Transferable) { - Database.Instance.update(newValue.id, newValue, () => - socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients - if (newValue.type === Types.Text) { // if the newValue has sring type, then it's suitable for searching -- pass it to SOLR + Database.Instance.update(newValue.id, newValue, () => socket.broadcast.emit(MessageStore.SetField.Message, newValue)); // broadcast set value to all other clients + 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: { set: (newValue as any).data } }); } } @@ -213,34 +208,36 @@ export namespace WebSocket { 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"], - "map": ["_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); + 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'], + map: ['_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; - }] + return results.length ? results : null; + }, + ], }; - function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { - + function ToSearchTerm(val: any): { suffix: string; value: any } | undefined { if (val === null || val === undefined) { return; } @@ -252,69 +249,79 @@ export namespace WebSocket { } if (Array.isArray(suffix)) { const accessor = suffix[1]; - if (typeof accessor === "function") { + 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]; + return typeof value === 'string' ? value : value[0]; } function addToListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { - diff.diff.$set = diff.diff.$addToSet; delete diff.diff.$addToSet;// convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones + diff.diff.$set = diff.diff.$addToSet; + delete diff.diff.$addToSet; // convert add to set to a query of the current fields, and then a set of the composition of the new fields with the old ones const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const newListItems = diff.diff.$set[updatefield]?.fields; if (!newListItems) { - console.log("Error: addToListField - no new list items"); + console.log('Error: addToListField - no new list items'); return; } - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((item: any) => item !== undefined) || []; - diff.diff.$set[updatefield].fields = [...curList, ...newListItems];//, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; + const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((item: any) => item !== undefined) || []; + diff.diff.$set[updatefield].fields = [...curList, ...newListItems]; //, ...newListItems.filter((newItem: any) => newItem === null || !curList.some((curItem: any) => curItem.fieldId ? curItem.fieldId === newItem.fieldId : curItem.heading ? curItem.heading === newItem.heading : curItem === newItem))]; const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; - Database.Instance.update(diff.id, diff.diff, + Database.Instance.update( + diff.id, + diff.diff, () => { if (sendBack) { - console.log("Warning: list modified during update. Composite list is being returned."); + console.log('Warning: list modified during update. Composite list is being returned.'); const id = socket.id; - socket.id = ""; + socket.id = ''; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); dispatchNextOp(diff.id); - }, false); + }, + false + ); } function remFromListField(socket: Socket, diff: Diff, curListItems?: Transferable): void { - diff.diff.$set = diff.diff.$remFromSet; delete diff.diff.$remFromSet; + diff.diff.$set = diff.diff.$remFromSet; + delete diff.diff.$remFromSet; const updatefield = Array.from(Object.keys(diff.diff.$set))[0]; const remListItems = diff.diff.$set[updatefield].fields; - const curList = (curListItems as any)?.fields?.[updatefield.replace("fields.", "")]?.fields.filter((f: any) => f !== null) || []; - diff.diff.$set[updatefield].fields = curList?.filter((curItem: any) => !remListItems.some((remItem: any) => remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)); + const curList = (curListItems as any)?.fields?.[updatefield.replace('fields.', '')]?.fields.filter((f: any) => f !== null) || []; + diff.diff.$set[updatefield].fields = curList?.filter( + (curItem: any) => !remListItems.some((remItem: any) => (remItem.fieldId ? remItem.fieldId === curItem.fieldId : remItem.heading ? remItem.heading === curItem.heading : remItem === curItem)) + ); const sendBack = diff.diff.length !== diff.diff.$set[updatefield].fields.length; delete diff.diff.length; - Database.Instance.update(diff.id, diff.diff, + Database.Instance.update( + diff.id, + diff.diff, () => { if (sendBack) { - console.log("SEND BACK"); + console.log('SEND BACK'); const id = socket.id; - socket.id = ""; + socket.id = ''; socket.broadcast.emit(MessageStore.UpdateField.Message, diff); socket.id = id; } else socket.broadcast.emit(MessageStore.UpdateField.Message, diff); dispatchNextOp(diff.id); - }, false); + }, + false + ); } - const pendingOps = new Map<string, { diff: Diff, socket: Socket }[]>(); + const pendingOps = new Map<string, { diff: Diff; socket: Socket }[]>(); function dispatchNextOp(id: string) { const next = pendingOps.get(id)!.shift(); @@ -341,7 +348,7 @@ export namespace WebSocket { function UpdateField(socket: Socket, diff: Diff) { if (CurUser !== socketMap.get(socket)) { CurUser = socketMap.get(socket); - console.log("Switch User: " + CurUser); + console.log('Switch User: ' + CurUser); } if (pendingOps.has(diff.id)) { pendingOps.get(diff.id)!.push({ diff, socket }); @@ -357,24 +364,25 @@ export namespace WebSocket { return GetRefFieldLocal([diff.id, (result?: Transferable) => SetField(socket, diff, result)]); } function SetField(socket: Socket, diff: Diff, curListItems?: Transferable) { - Database.Instance.update(diff.id, diff.diff, - () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); + 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) { const update: any = { id: diff.id }; let dynfield = false; for (let key in docfield) { - if (!key.startsWith("fields.")) continue; + 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 }; }); + 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 (key.endsWith('lastModified')) { - update["lastModified" + suffix] = value; + update['lastModified' + suffix] = value; } } } @@ -403,6 +411,4 @@ export namespace WebSocket { function CreateField(newValue: any) { Database.Instance.insert(newValue); } - } - |