diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/ApiManagers/DeleteManager.ts | 85 | ||||
| -rw-r--r-- | src/server/ApiManagers/GeneralGoogleManager.ts | 19 | ||||
| -rw-r--r-- | src/server/ApiManagers/GooglePhotosManager.ts | 2 | ||||
| -rw-r--r-- | src/server/ApiManagers/SessionManager.ts | 2 | ||||
| -rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 2 | ||||
| -rw-r--r-- | src/server/ApiManagers/UserManager.ts | 2 | ||||
| -rw-r--r-- | src/server/DashSession/DashSessionAgent.ts | 2 | ||||
| -rw-r--r-- | src/server/DashUploadUtils.ts | 7 | ||||
| -rw-r--r-- | src/server/GarbageCollector.ts | 6 | ||||
| -rw-r--r-- | src/server/IDatabase.ts | 4 | ||||
| -rw-r--r-- | src/server/MemoryDatabase.ts | 23 | ||||
| -rw-r--r-- | src/server/Websocket/Websocket.ts | 44 | ||||
| -rw-r--r-- | src/server/apis/google/GoogleApiServerUtils.ts | 47 | ||||
| -rw-r--r-- | src/server/authentication/models/current_user_utils.ts | 185 | ||||
| -rw-r--r-- | src/server/authentication/models/user_model.ts | 4 | ||||
| -rw-r--r-- | src/server/database.ts | 116 | ||||
| -rw-r--r-- | src/server/remapUrl.ts | 4 |
17 files changed, 289 insertions, 265 deletions
diff --git a/src/server/ApiManagers/DeleteManager.ts b/src/server/ApiManagers/DeleteManager.ts index 9e70af2eb..bd80d6500 100644 --- a/src/server/ApiManagers/DeleteManager.ts +++ b/src/server/ApiManagers/DeleteManager.ts @@ -1,12 +1,12 @@ import ApiManager, { Registration } from "./ApiManager"; -import { Method, _permission_denied, PublicHandler } from "../RouteManager"; +import { Method, _permission_denied } from "../RouteManager"; import { WebSocket } from "../Websocket/Websocket"; import { Database } from "../database"; import rimraf = require("rimraf"); -import { pathToDirectory, Directory } from "./UploadManager"; import { filesDirectory } from ".."; import { DashUploadUtils } from "../DashUploadUtils"; import { mkdirSync } from "fs"; +import RouteSubscriber from "../RouteSubscriber"; export default class DeleteManager extends ApiManager { @@ -14,68 +14,39 @@ export default class DeleteManager extends ApiManager { register({ method: Method.GET, - subscription: "/delete", - secureHandler: async ({ res, isRelease }) => { + subscription: new RouteSubscriber("delete").add("target?"), + secureHandler: async ({ req, res, isRelease }) => { if (isRelease) { - return _permission_denied(res, deletionPermissionError); + return _permission_denied(res, "Cannot perform a delete operation outside of the development environment!"); } - await WebSocket.deleteFields(); - res.redirect("/home"); - } - }); - register({ - method: Method.GET, - subscription: "/deleteAll", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); + const { target } = req.params; + const { doDelete } = WebSocket; + + if (!target) { + await doDelete(); + } else { + let all = false; + switch (target) { + case "all": + all = true; + case "database": + await doDelete(false); + if (!all) break; + case "files": + rimraf.sync(filesDirectory); + mkdirSync(filesDirectory); + await DashUploadUtils.buildFileDirectories(); + break; + default: + await Database.Instance.dropSchema(target); + } } - await WebSocket.deleteAll(); - res.redirect("/home"); - } - }); - register({ - method: Method.GET, - subscription: "/deleteAssets", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - rimraf.sync(filesDirectory); - mkdirSync(filesDirectory); - await DashUploadUtils.buildFileDirectories(); - res.redirect("/delete"); - } - }); - - register({ - method: Method.GET, - subscription: "/deleteWithAux", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - await Database.Auxiliary.DeleteAll(); - res.redirect("/delete"); - } - }); - - register({ - method: Method.GET, - subscription: "/deleteWithGoogleCredentials", - secureHandler: async ({ res, isRelease }) => { - if (isRelease) { - return _permission_denied(res, deletionPermissionError); - } - await Database.Auxiliary.GoogleAuthenticationToken.DeleteAll(); - res.redirect("/delete"); + res.redirect("/home"); } }); } -} - -const deletionPermissionError = "Cannot perform a delete operation outside of the development environment!"; +}
\ No newline at end of file diff --git a/src/server/ApiManagers/GeneralGoogleManager.ts b/src/server/ApiManagers/GeneralGoogleManager.ts index a5240edbc..17968cc7d 100644 --- a/src/server/ApiManagers/GeneralGoogleManager.ts +++ b/src/server/ApiManagers/GeneralGoogleManager.ts @@ -1,10 +1,8 @@ import ApiManager, { Registration } from "./ApiManager"; import { Method, _permission_denied } from "../RouteManager"; import { GoogleApiServerUtils } from "../apis/google/GoogleApiServerUtils"; -import { Database } from "../database"; import RouteSubscriber from "../RouteSubscriber"; - -const deletionPermissionError = "Cannot perform specialized delete outside of the development environment!"; +import { Database } from "../database"; const EndpointHandlerMap = new Map<GoogleApiServerUtils.Action, GoogleApiServerUtils.ApiRouter>([ ["create", (api, params) => api.create(params)], @@ -20,11 +18,11 @@ export default class GeneralGoogleManager extends ApiManager { method: Method.GET, subscription: "/readGoogleAccessToken", secureHandler: async ({ user, res }) => { - const token = await GoogleApiServerUtils.retrieveAccessToken(user.id); - if (!token) { + const { credentials } = (await GoogleApiServerUtils.retrieveCredentials(user.id)); + if (!credentials?.access_token) { return res.send(GoogleApiServerUtils.generateAuthenticationUrl()); } - return res.send(token); + return res.send(credentials); } }); @@ -37,6 +35,15 @@ export default class GeneralGoogleManager extends ApiManager { }); register({ + method: Method.GET, + subscription: "/revokeGoogleAccessToken", + secureHandler: async ({ user, res }) => { + await Database.Auxiliary.GoogleAuthenticationToken.Revoke(user.id); + res.send(); + } + }); + + register({ method: Method.POST, subscription: new RouteSubscriber("googleDocs").add("sector", "action"), secureHandler: async ({ req, res, user }) => { diff --git a/src/server/ApiManagers/GooglePhotosManager.ts b/src/server/ApiManagers/GooglePhotosManager.ts index 88219423d..11841a603 100644 --- a/src/server/ApiManagers/GooglePhotosManager.ts +++ b/src/server/ApiManagers/GooglePhotosManager.ts @@ -56,7 +56,7 @@ export default class GooglePhotosManager extends ApiManager { const { media } = req.body; // first we need to ensure that we know the google account to which these photos will be uploaded - const token = await GoogleApiServerUtils.retrieveAccessToken(user.id); + const token = (await GoogleApiServerUtils.retrieveCredentials(user.id))?.credentials?.access_token; if (!token) { return _error(res, authenticationError); } diff --git a/src/server/ApiManagers/SessionManager.ts b/src/server/ApiManagers/SessionManager.ts index bcaa6598f..fa2f6002a 100644 --- a/src/server/ApiManagers/SessionManager.ts +++ b/src/server/ApiManagers/SessionManager.ts @@ -55,7 +55,7 @@ export default class SessionManager extends ApiManager { register({ method: Method.GET, - subscription: this.secureSubscriber("delete"), + subscription: this.secureSubscriber("deleteSession"), secureHandler: this.authorizedAction(async ({ res }) => { const { error } = await sessionAgent.serverWorker.emit("delete"); res.send(error ? error.message : "Your request was successful: the server successfully deleted the database. Return to /home."); diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 98f029c7d..b185d3b55 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -171,7 +171,7 @@ export default class UploadManager extends ApiManager { await Promise.all(docs.map((doc: any) => new Promise(res => Database.Instance.replace(doc.id, doc, (err, r) => { err && console.log(err); res(); - }, true, "newDocuments")))); + }, true)))); } catch (e) { console.log(e); } unlink(path_2, () => { }); } diff --git a/src/server/ApiManagers/UserManager.ts b/src/server/ApiManagers/UserManager.ts index d9d346cc1..68b3107ae 100644 --- a/src/server/ApiManagers/UserManager.ts +++ b/src/server/ApiManagers/UserManager.ts @@ -89,8 +89,6 @@ export default class UserManager extends ApiManager { } }); - - register({ method: Method.GET, subscription: "/activity", diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index 5cbba13de..ef9b88541 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -37,7 +37,7 @@ export class DashSessionAgent extends AppliedSessionAgent { monitor.addReplCommand("debug", [/\S+\@\S+/], async ([to]) => this.dispatchZippedDebugBackup(to)); monitor.on("backup", this.backup); monitor.on("debug", async ({ to }) => this.dispatchZippedDebugBackup(to)); - monitor.on("delete", WebSocket.deleteFields); + monitor.on("delete", WebSocket.doDelete); monitor.coreHooks.onCrashDetected(this.dispatchCrashReport); return sessionKey; } diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 3f903a861..8567631cd 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -325,12 +325,7 @@ export namespace DashUploadUtils { const outputPath = path.resolve(outputDirectory, writtenFiles[suffix] = InjectSize(outputFileName, suffix)); await new Promise<void>(async (resolve, reject) => { const source = streamProvider(); - let readStream: Stream; - if (source instanceof Promise) { - readStream = await source; - } else { - readStream = source; - } + let readStream: Stream = source instanceof Promise ? await source : source; if (resizer) { readStream = readStream.pipe(resizer.withMetadata()); } diff --git a/src/server/GarbageCollector.ts b/src/server/GarbageCollector.ts index 5729c3ee5..24745cbb4 100644 --- a/src/server/GarbageCollector.ts +++ b/src/server/GarbageCollector.ts @@ -76,7 +76,7 @@ async function GarbageCollect(full: boolean = true) { if (!fetchIds.length) { continue; } - const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res, "newDocuments")); + const docs = await new Promise<{ [key: string]: any }[]>(res => Database.Instance.getDocuments(fetchIds, res)); for (const doc of docs) { const id = doc.id; if (doc === undefined) { @@ -116,10 +116,10 @@ async function GarbageCollect(full: boolean = true) { const count = Math.min(toDelete.length, 5000); const toDeleteDocs = toDelete.slice(i, i + count); i += count; - const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }, "newDocuments"); + const result = await Database.Instance.delete({ _id: { $in: toDeleteDocs } }); deleted += result.deletedCount || 0; } - // const result = await Database.Instance.delete({ _id: { $in: toDelete } }, "newDocuments"); + // const result = await Database.Instance.delete({ _id: { $in: toDelete } }); console.log(`${deleted} documents deleted`); await Search.deleteDocuments(toDelete); diff --git a/src/server/IDatabase.ts b/src/server/IDatabase.ts index 6a63df485..dd4968579 100644 --- a/src/server/IDatabase.ts +++ b/src/server/IDatabase.ts @@ -2,7 +2,6 @@ import * as mongodb from 'mongodb'; import { Transferable } from './Message'; export const DocumentsCollection = 'documents'; -export const NewDocumentsCollection = 'newDocuments'; export interface IDatabase { update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert?: boolean, collectionName?: string): Promise<void>; updateMany(query: any, update: any, collectionName?: string): Promise<mongodb.WriteOpResult>; @@ -12,12 +11,13 @@ export interface IDatabase { delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; - deleteAll(collectionName?: string, persist?: boolean): Promise<any>; + dropSchema(...schemaNames: string[]): Promise<any>; insert(value: any, collectionName?: string): Promise<void>; getDocument(id: string, fn: (result?: Transferable) => void, collectionName?: string): void; getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName?: string): void; + getCollectionNames(): Promise<string[]>; visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName?: string): Promise<void>; query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName?: string): Promise<mongodb.Cursor>; diff --git a/src/server/MemoryDatabase.ts b/src/server/MemoryDatabase.ts index 543f96e7f..1f1d702d9 100644 --- a/src/server/MemoryDatabase.ts +++ b/src/server/MemoryDatabase.ts @@ -1,4 +1,4 @@ -import { IDatabase, DocumentsCollection, NewDocumentsCollection } from './IDatabase'; +import { IDatabase, DocumentsCollection } from './IDatabase'; import { Transferable } from './Message'; import * as mongodb from 'mongodb'; @@ -15,6 +15,10 @@ export class MemoryDatabase implements IDatabase { } } + public getCollectionNames() { + return Promise.resolve(Object.keys(this.db)); + } + public update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, _upsert?: boolean, collectionName = DocumentsCollection): Promise<void> { const collection = this.getCollection(collectionName); const set = "$set"; @@ -41,7 +45,7 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve(undefined); } - public updateMany(query: any, update: any, collectionName = NewDocumentsCollection): Promise<mongodb.WriteOpResult> { + public updateMany(query: any, update: any, collectionName = DocumentsCollection): Promise<mongodb.WriteOpResult> { throw new Error("Can't updateMany a MemoryDatabase"); } @@ -58,8 +62,15 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve({} as any); } - public deleteAll(collectionName = DocumentsCollection, _persist = true): Promise<any> { - delete this.db[collectionName]; + public async dropSchema(...schemaNames: string[]): Promise<any> { + const existing = await this.getCollectionNames(); + let valid: string[]; + if (schemaNames.length) { + valid = schemaNames.filter(collection => existing.includes(collection)); + } else { + valid = existing; + } + valid.forEach(schemaName => delete this.db[schemaName]); return Promise.resolve(); } @@ -69,14 +80,14 @@ export class MemoryDatabase implements IDatabase { return Promise.resolve(); } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = NewDocumentsCollection): void { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = DocumentsCollection): void { fn(this.getCollection(collectionName)[id]); } public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection): void { fn(ids.map(id => this.getCollection(collectionName)[id])); } - public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = NewDocumentsCollection): Promise<void> { + public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = DocumentsCollection): Promise<void> { const visited = new Set<string>(); while (ids.length) { const count = Math.min(ids.length, 1000); diff --git a/src/server/Websocket/Websocket.ts b/src/server/Websocket/Websocket.ts index 947aa4289..844535056 100644 --- a/src/server/Websocket/Websocket.ts +++ b/src/server/Websocket/Websocket.ts @@ -10,9 +10,9 @@ import { GoogleCredentialsLoader } from "../credentials/CredentialsLoader"; import { logPort } from "../ActionUtilities"; import { timeMap } from "../ApiManagers/UserManager"; import { green } from "colors"; -import { serverPathToFile, Directory } from "../ApiManagers/UploadManager"; import { networkInterfaces } from "os"; import executeImport from "../../scraping/buxton/final/BuxtonImporter"; +import { DocumentsCollection } from "../IDatabase"; export namespace WebSocket { @@ -97,7 +97,7 @@ export namespace WebSocket { Utils.AddServerHandlerCallback(socket, MessageStore.GetField, getField); Utils.AddServerHandlerCallback(socket, MessageStore.GetFields, getFields); if (isRelease) { - Utils.AddServerHandler(socket, MessageStore.DeleteAll, deleteFields); + Utils.AddServerHandler(socket, MessageStore.DeleteAll, () => doDelete(false)); } Utils.AddServerHandler(socket, MessageStore.CreateField, CreateField); @@ -111,6 +111,12 @@ export namespace WebSocket { Utils.AddServerHandler(socket, MessageStore.MobileDocumentUpload, content => processMobileDocumentUpload(socket, content)); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefField, GetRefField); Utils.AddServerHandlerCallback(socket, MessageStore.GetRefFields, GetRefFields); + + /** + * Whenever we receive the go-ahead, invoke the import script and pass in + * as an emitter and a terminator the functions that simply broadcast a result + * or indicate termination to the client via the web socket + */ Utils.AddServerHandler(socket, MessageStore.BeginBuxtonImport, () => { executeImport( deviceOrError => Utils.Emit(socket, MessageStore.BuxtonDocumentResult, deviceOrError), @@ -158,24 +164,10 @@ export namespace WebSocket { } } - export async function deleteFields() { - await Database.Instance.deleteAll(); - if (process.env.DISABLE_SEARCH !== "true") { - await Search.clear(); - } - await Database.Instance.deleteAll('newDocuments'); - } - - // export async function deleteUserDocuments() { - // await Database.Instance.deleteAll(); - // await Database.Instance.deleteAll('newDocuments'); - // } - - export async function deleteAll() { - await Database.Instance.deleteAll(); - await Database.Instance.deleteAll('newDocuments'); - await Database.Instance.deleteAll('sessions'); - await Database.Instance.deleteAll('users'); + export async function doDelete(onlyFields = true) { + const target: string[] = []; + onlyFields && target.push(DocumentsCollection); + await Database.Instance.dropSchema(...target); if (process.env.DISABLE_SEARCH !== "true") { await Search.clear(); } @@ -205,11 +197,11 @@ export namespace WebSocket { } function GetRefField([id, callback]: [string, (result?: Transferable) => void]) { - Database.Instance.getDocument(id, callback, "newDocuments"); + Database.Instance.getDocument(id, callback); } function GetRefFields([ids, callback]: [string[], (result?: Transferable[]) => void]) { - Database.Instance.getDocuments(ids, callback, "newDocuments"); + Database.Instance.getDocuments(ids, callback); } const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { @@ -266,7 +258,7 @@ export namespace WebSocket { function UpdateField(socket: Socket, diff: Diff) { Database.Instance.update(diff.id, diff.diff, - () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false, "newDocuments"); + () => socket.broadcast.emit(MessageStore.UpdateField.Message, diff), false); const docfield = diff.diff.$set || diff.diff.$unset; if (!docfield) { return; @@ -291,7 +283,7 @@ export namespace WebSocket { } function DeleteField(socket: Socket, id: string) { - Database.Instance.delete({ _id: id }, "newDocuments").then(() => { + Database.Instance.delete({ _id: id }).then(() => { socket.broadcast.emit(MessageStore.DeleteField.Message, id); }); @@ -299,14 +291,14 @@ export namespace WebSocket { } function DeleteFields(socket: Socket, ids: string[]) { - Database.Instance.delete({ _id: { $in: ids } }, "newDocuments").then(() => { + Database.Instance.delete({ _id: { $in: ids } }).then(() => { socket.broadcast.emit(MessageStore.DeleteFields.Message, ids); }); Search.deleteDocuments(ids); } function CreateField(newValue: any) { - Database.Instance.insert(newValue, "newDocuments"); + Database.Instance.insert(newValue); } } diff --git a/src/server/apis/google/GoogleApiServerUtils.ts b/src/server/apis/google/GoogleApiServerUtils.ts index 0f75833ee..48a8da89f 100644 --- a/src/server/apis/google/GoogleApiServerUtils.ts +++ b/src/server/apis/google/GoogleApiServerUtils.ts @@ -149,26 +149,6 @@ export namespace GoogleApiServerUtils { } /** - * Returns the lengthy string or access token that can be passed into - * the headers of an API request or into the constructor of the Photos - * client API wrapper. - * @param userId the Dash user id of the user requesting his/her associated - * access_token - * @returns the current access_token associated with the requesting - * Dash user. The access_token is valid for only an hour, and - * is then refreshed. - */ - export async function retrieveAccessToken(userId: string): Promise<string> { - return new Promise(async resolve => { - const { credentials } = await retrieveCredentials(userId); - if (!credentials) { - return resolve(); - } - resolve(credentials.access_token!); - }); - } - - /** * Manipulates a mapping such that, in the limit, each Dash user has * an associated authenticated OAuth2 client at their disposal. This * function ensures that the client's credentials always remain up to date @@ -217,18 +197,6 @@ export namespace GoogleApiServerUtils { } /** - * This is what we return to the server in processNewUser(), after the - * worker OAuth2Client has used the user-pasted authentication code - * to retrieve an access token and an info token. The avatar is the - * URL to the Google-hosted mono-color, single white letter profile 'image'. - */ - export interface GoogleAuthenticationResult { - access_token: string; - avatar: string; - name: string; - } - - /** * This method receives the authentication code that the * user pasted into the overlay in the client side and uses the worker * and the authentication code to fetch the full set of credentials that @@ -245,7 +213,7 @@ export namespace GoogleApiServerUtils { * and display basic user information in the overlay on successful authentication. * This can be expanded as needed by adding properties to the interface GoogleAuthenticationResult. */ - export async function processNewUser(userId: string, authenticationCode: string): Promise<GoogleAuthenticationResult> { + export async function processNewUser(userId: string, authenticationCode: string): Promise<EnrichedCredentials> { const credentials = await new Promise<Credentials>((resolve, reject) => { worker.getToken(authenticationCode, async (err, credentials) => { if (err || !credentials) { @@ -257,12 +225,7 @@ export namespace GoogleApiServerUtils { }); const enriched = injectUserInfo(credentials); await Database.Auxiliary.GoogleAuthenticationToken.Write(userId, enriched); - const { given_name, picture } = enriched.userInfo; - return { - access_token: enriched.access_token!, - avatar: picture, - name: given_name - }; + return enriched; } /** @@ -316,15 +279,15 @@ export namespace GoogleApiServerUtils { * @returns the credentials, or undefined if the user has no stored associated credentials, * and a flag indicating whether or not they were refreshed during retrieval */ - async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<Credentials>, refreshed: boolean }> { - let credentials: Opt<Credentials> = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); + export async function retrieveCredentials(userId: string): Promise<{ credentials: Opt<EnrichedCredentials>, refreshed: boolean }> { + let credentials = await Database.Auxiliary.GoogleAuthenticationToken.Fetch(userId); let refreshed = false; if (!credentials) { return { credentials: undefined, refreshed }; } // check for token expiry if (credentials.expiry_date! <= new Date().getTime()) { - credentials = await refreshAccessToken(credentials, userId); + credentials = { ...credentials, ...(await refreshAccessToken(credentials, userId)) }; refreshed = true; } return { credentials, refreshed }; diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts index ba5ada6c4..782582930 100644 --- a/src/server/authentication/models/current_user_utils.ts +++ b/src/server/authentication/models/current_user_utils.ts @@ -1,5 +1,6 @@ import { action, computed, observable, reaction } from "mobx"; import * as rp from 'request-promise'; +import { Utils } from "../../../Utils"; import { DocServer } from "../../../client/DocServer"; import { Docs, DocumentOptions } from "../../../client/documents/Documents"; import { UndoManager } from "../../../client/util/UndoManager"; @@ -7,8 +8,7 @@ import { Doc, DocListCast, DocListCastAsync } from "../../../new_fields/Doc"; import { List } from "../../../new_fields/List"; import { listSpec } from "../../../new_fields/Schema"; import { ScriptField, ComputedField } from "../../../new_fields/ScriptField"; -import { Cast, PromiseValue, StrCast } from "../../../new_fields/Types"; -import { Utils } from "../../../Utils"; +import { Cast, PromiseValue, StrCast, NumCast } from "../../../new_fields/Types"; import { nullAudio, ImageField } from "../../../new_fields/URLField"; import { DragManager } from "../../../client/util/DragManager"; import { InkingControl } from "../../../client/views/InkingControl"; @@ -21,6 +21,7 @@ import { FormattedTextBox } from "../../../client/views/nodes/formattedText/Form import { MainView } from "../../../client/views/MainView"; import { DocumentType } from "../../../client/documents/DocumentTypes"; import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField"; +import { DimUnit } from "../../../client/views/collections/collectionMulticolumn/CollectionMulticolumnView"; export class CurrentUserUtils { private static curr_id: string; @@ -72,24 +73,62 @@ export class CurrentUserUtils { } if (doc["template-button-description"] === undefined) { - const descriptionTemplate = Docs.Create.TextDocument("", { title: "header", _height: 100 }); - Doc.GetProto(descriptionTemplate).layout = "<div><FormattedTextBox {...props} background='orange' height='50px' fieldKey={'header'}/><FormattedTextBox {...props} height='calc(100% - 50px)' fieldKey={'text'}/></div>"; + const descriptionTemplate = Docs.Create.TextDocument(" ", { title: "header", _height: 100 }, "header"); // text needs to be a space to allow templateText to be created + Doc.GetProto(descriptionTemplate).layout = + "<div><FormattedTextBox {...props} height='{this._headerHeight||75}px' background='{this._headerColor||`orange`}' fieldKey={'header'}/>" + + "<FormattedTextBox {...props} height='calc(100% - {this._headerHeight||75}px)' fieldKey={'text'}/></div>"; descriptionTemplate.isTemplateDoc = makeTemplate(descriptionTemplate, true, "descriptionView"); doc["template-button-description"] = CurrentUserUtils.ficon({ - onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + onDragStart: ScriptField.MakeFunction('makeDelegate(this.dragFactory)'), dragFactory: new PrefetchProxy(descriptionTemplate) as any as Doc, removeDropProperties: new List<string>(["dropAction"]), title: "description view", icon: "window-maximize" }); } + if (doc["template-button-switch"] === undefined) { + const { FreeformDocument, MulticolumnDocument, TextDocument } = Docs.Create; + + const yes = FreeformDocument([], { title: "yes", _height: 35, _width: 50, _LODdisable: true, _dimUnit: DimUnit.Pixel, _dimMagnitude: 40 }); + const name = TextDocument("name", { title: "name", _height: 35, _width: 70, _dimMagnitude: 1 }); + const no = FreeformDocument([], { title: "no", _height: 100, _width: 100, _LODdisable: true }); + const labelTemplate = { + doc: { + type: "doc", content: [{ + type: "paragraph", + content: [{ type: "dashField", attrs: { fieldKey: "PARAMS", hideKey: true } }] + }] + }, + selection: { type: "text", anchor: 1, head: 1 }, + storedMarks: [] + }; + Doc.GetProto(name).text = new RichTextField(JSON.stringify(labelTemplate), "PARAMS"); + Doc.GetProto(yes).backgroundColor = ComputedField.MakeFunction("self[this.PARAMS] ? 'green':'red'"); + // Doc.GetProto(no).backgroundColor = ComputedField.MakeFunction("!self[this.PARAMS] ? 'red':'white'"); + // Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = true"); + Doc.GetProto(yes).onClick = ScriptField.MakeScript("self[this.PARAMS] = !self[this.PARAMS]"); + // Doc.GetProto(no).onClick = ScriptField.MakeScript("self[this.PARAMS] = false"); + const box = MulticolumnDocument([/*no, */ yes, name], { title: "value", _width: 120, _height: 35, }); + box.isTemplateDoc = makeTemplate(box, true, "switch"); + + doc["template-button-switch"] = CurrentUserUtils.ficon({ + onDragStart: ScriptField.MakeFunction('getCopy(this.dragFactory, true)'), + dragFactory: new PrefetchProxy(box) as any as Doc, + removeDropProperties: new List<string>(["dropAction"]), title: "data switch", icon: "toggle-on" + }); + } + if (doc["template-button-detail"] === undefined) { const { TextDocument, MasonryDocument, CarouselDocument } = Docs.Create; - const carousel = CarouselDocument([], { title: "data", _height: 350, _itemIndex: 0, backgroundColor: "#9b9b9b3F" }); + const openInTarget = ScriptField.MakeScript("openOnRight(self.doubleClickView)"); + const carousel = CarouselDocument([], { + title: "data", _height: 350, _itemIndex: 0, "_carousel-caption-xMargin": 10, "_carousel-caption-yMargin": 10, + onChildDoubleClick: openInTarget, backgroundColor: "#9b9b9b3F" + }); const details = TextDocument("", { title: "details", _height: 350, _autoHeight: true }); - const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 50, _autoHeight: true }); + const short = TextDocument("", { title: "shortDescription", treeViewOpen: true, treeViewExpandedView: "layout", _height: 100, _autoHeight: true }); const long = TextDocument("", { title: "longDescription", treeViewOpen: false, treeViewExpandedView: "layout", _height: 350, _autoHeight: true }); const buxtonFieldKeys = ["year", "originalPrice", "degreesOfFreedom", "company", "attribute", "primaryKey", "secondaryKey", "dimensions"]; @@ -111,8 +150,9 @@ export class CurrentUserUtils { const descriptionWrapper = MasonryDocument([details, short, long], { ...shared, ...descriptionWrapperOpts }); descriptionWrapper.sectionHeaders = new List<SchemaHeaderField>([ - new SchemaHeaderField("[Long Description]", "LemonChiffon", undefined, undefined, undefined, true), - new SchemaHeaderField("[Details]", "lightBlue", undefined, undefined, undefined, true), + new SchemaHeaderField("[A Short Description]", "dimGray", undefined, undefined, undefined, false), + new SchemaHeaderField("[Long Description]", "dimGray", undefined, undefined, undefined, true), + new SchemaHeaderField("[Details]", "dimGray", undefined, undefined, undefined, true), ]); const detailView = Docs.Create.StackingDocument([carousel, descriptionWrapper], { ...shared, ...detailViewOpts }); detailView.isTemplateDoc = makeTemplate(detailView); @@ -130,13 +170,19 @@ export class CurrentUserUtils { if (doc["template-buttons"] === undefined) { doc["template-buttons"] = new PrefetchProxy(Docs.Create.MasonryDocument([doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, - doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc], { - title: "Compound Item Creators", _xMargin: 0, _showTitle: "title", + doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc], { + title: "Advanced Item Prototypes", _xMargin: 0, _showTitle: "title", _autoHeight: true, _width: 500, columnWidth: 35, ignoreClick: true, lockedPosition: true, _chromeStatus: "disabled", dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), })); } else { - DocListCast(Cast(doc["template-buttons"], Doc, null)?.data); // prefetch templates + const curButnTypes = Cast(doc["template-buttons"], Doc, null); + const requiredTypes = [doc["template-button-slides"] as Doc, doc["template-button-description"] as Doc, + doc["template-button-query"] as Doc, doc["template-button-detail"] as Doc, doc["template-button-switch"] as Doc]; + DocListCastAsync(curButnTypes.data).then(async curBtns => { + await Promise.all(curBtns!); + requiredTypes.map(btype => Doc.AddDocToList(curButnTypes, "data", btype)); + }); } return doc["template-buttons"] as Doc; } @@ -210,28 +256,29 @@ export class CurrentUserUtils { static setupDefaultIconTemplates(doc: Doc) { if (doc["template-icon-view"] === undefined) { const iconView = Docs.Create.TextDocument("", { - title: "icon", _width: 150, _height: 30, isTemplateDoc: true, - onClick: ScriptField.MakeScript("deiconifyView(self)") + title: "icon", _width: 150, _height: 30, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); Doc.GetProto(iconView).icon = new RichTextField('{"doc":{"type":"doc","content":[{"type":"paragraph","attrs":{"align":null,"color":null,"id":null,"indent":null,"inset":null,"lineSpacing":null,"paddingBottom":null,"paddingTop":null},"content":[{"type":"dashField","attrs":{"fieldKey":"title","docid":""}}]}]},"selection":{"type":"text","anchor":2,"head":2},"storedMarks":[]}', ""); iconView.isTemplateDoc = makeTemplate(iconView); doc["template-icon-view"] = new PrefetchProxy(iconView); } if (doc["template-icon-view-rtf"] === undefined) { - const iconRtfView = Docs.Create.LabelDocument({ title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onClick: ScriptField.MakeScript("deiconifyView(self)") }); + const iconRtfView = Docs.Create.LabelDocument({ + title: "icon_" + DocumentType.RTF, textTransform: "unset", letterSpacing: "unset", + _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") + }); iconRtfView.isTemplateDoc = makeTemplate(iconRtfView, true, "icon_" + DocumentType.RTF); doc["template-icon-view-rtf"] = new PrefetchProxy(iconRtfView); } if (doc["template-icon-view-img"] === undefined) { const iconImageView = Docs.Create.ImageDocument("http://www.cs.brown.edu/~bcz/face.gif", { - title: "data", _width: 50, isTemplateDoc: true, - onClick: ScriptField.MakeScript("deiconifyView(self)") + title: "data", _width: 50, isTemplateDoc: true, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); iconImageView.isTemplateDoc = makeTemplate(iconImageView, true, "icon_" + DocumentType.IMG); doc["template-icon-view-img"] = new PrefetchProxy(iconImageView); } if (doc["template-icon-view-col"] === undefined) { - const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onClick: ScriptField.MakeScript("deiconifyView(self)") }); + const iconColView = Docs.Create.TreeDocument([], { title: "data", _width: 180, _height: 80, onDoubleClick: ScriptField.MakeScript("deiconifyView(self)") }); iconColView.isTemplateDoc = makeTemplate(iconColView, true, "icon_" + DocumentType.COL); doc["template-icon-view-col"] = new PrefetchProxy(iconColView); } @@ -240,8 +287,12 @@ export class CurrentUserUtils { doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc], { title: "icon templates", _height: 75 })); } else { const templateIconsDoc = Cast(doc["template-icons"], Doc, null); - DocListCastAsync(templateIconsDoc).then(list => templateIconsDoc.data = new List<Doc>([doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, - doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc])); + const requiredTypes = [doc["template-icon-view"] as Doc, doc["template-icon-view-img"] as Doc, + doc["template-icon-view-col"] as Doc, doc["template-icon-view-rtf"] as Doc]; + DocListCastAsync(templateIconsDoc.data).then(async curIcons => { + await Promise.all(curIcons!); + requiredTypes.map(ntype => Doc.AddDocToList(templateIconsDoc, "data", ntype)); + }); } return doc["template-icons"] as Doc; } @@ -252,15 +303,23 @@ export class CurrentUserUtils { }[] { if (doc.emptyPresentation === undefined) { doc.emptyPresentation = Docs.Create.PresDocument(new List<Doc>(), - { title: "Presentation", _viewType: CollectionViewType.Stacking, _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); + { title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); } if (doc.emptyCollection === undefined) { doc.emptyCollection = Docs.Create.FreeformDocument([], { _nativeWidth: undefined, _nativeHeight: undefined, _LODdisable: true, _width: 150, _height: 100, title: "freeform" }); } + if (doc.emptyDocHolder === undefined) { + doc.emptyDocHolder = Docs.Create.DocumentDocument( + ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]") as any, + { _width: 250, _height: 250, title: "container" }); + } + if (doc.emptyWebpage === undefined) { + doc.emptyWebpage = Docs.Create.WebDocument("", { title: "New Webpage", _width: 600, UseCors: true }); + } return [ { title: "Drag a collection", label: "Col", icon: "folder", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyCollection as Doc }, - { title: "Drag a web page", label: "Web", icon: "globe-asia", ignoreClick: true, drag: 'Docs.Create.WebDocument("", { title: "New Webpage" })' }, + { title: "Drag a web page", label: "Web", icon: "globe-asia", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory, true)', dragFactory: doc.emptyWebpage as Doc }, { title: "Drag a cat image", label: "Img", icon: "cat", ignoreClick: true, drag: 'Docs.Create.ImageDocument("https://upload.wikimedia.org/wikipedia/commons/thumb/3/3a/Cat03.jpg/1200px-Cat03.jpg", { _width: 250, _nativeWidth:250, title: "an image of a cat" })' }, { title: "Drag a screenshot", label: "Grab", icon: "photo-video", ignoreClick: true, drag: 'Docs.Create.ScreenshotDocument("", { _width: 400, _height: 200, title: "screen snapshot" })' }, { title: "Drag a webcam", label: "Cam", icon: "video", ignoreClick: true, drag: 'Docs.Create.WebCamDocument("", { _width: 400, _height: 400, title: "a test cam" })' }, @@ -277,10 +336,10 @@ export class CurrentUserUtils { // { title: "use stamp", icon: "stamp", click: 'activateStamp(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this)', backgroundColor: "orange", ischecked: `sameDocs(this.activePen.inkPen, this)`, activePen: doc }, // { title: "use eraser", icon: "eraser", click: 'activateEraser(this.activePen.inkPen = sameDocs(this.activePen.inkPen, this) ? undefined : this);', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "pink", activePen: doc }, // { title: "use drag", icon: "mouse-pointer", click: 'deactivateInk();this.activePen.inkPen = this;', ischecked: `sameDocs(this.activePen.inkPen, this)`, backgroundColor: "white", activePen: doc }, - { title: "Drag a document previewer", label: "Prev", icon: "expand", ignoreClick: true, drag: 'Docs.Create.DocumentDocument(ComputedField.MakeFunction("selectedDocs(this,this.excludeCollections,[_last_])?.[0]"), { _width: 250, _height: 250, title: "container" })' }, - { title: "Drag a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, + { title: "Drag a document previewer", label: "Prev", icon: "expand", click: 'openOnRight(getCopy(this.dragFactory, true))', drag: 'getCopy(this.dragFactory,true)', dragFactory: doc.emptyDocHolder as Doc }, + { title: "Toggle a Calculator REPL", label: "repl", icon: "calculator", click: 'addOverlayWindow("ScriptingRepl", { x: 300, y: 100, width: 200, height: 200, title: "Scripting REPL" })' }, + { title: "Connect a Google Account", label: "Google Account", icon: "external-link-alt", click: 'GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(true)' }, { title: "query", icon: "bolt", label: "Col", ignoreClick: true, drag: 'Docs.Create.SearchDocument({ _width: 200, title: "an image of a cat" })' }, - ]; } @@ -296,21 +355,22 @@ export class CurrentUserUtils { alreadyCreatedButtons = dragDocs.map(d => StrCast(d.title)); } } - const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)). - map(data => Docs.Create.FontIconDocument({ - _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, - icon: data.icon, - title: data.title, - label: data.label, - ignoreClick: data.ignoreClick, - dropAction: data.click ? "copy" : undefined, - onDragStart: data.drag ? ScriptField.MakeFunction(data.drag) : undefined, - onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - ischecked: data.ischecked ? ComputedField.MakeFunction(data.ischecked) : undefined, - activePen: data.activePen, - backgroundColor: data.backgroundColor, removeDropProperties: new List<string>(["dropAction"]), - dragFactory: data.dragFactory, - })); + const buttons = CurrentUserUtils.creatorBtnDescriptors(doc).filter(d => !alreadyCreatedButtons?.includes(d.title)); + const creatorBtns = buttons.map(({ title, label, icon, ignoreClick, drag, click, ischecked, activePen, backgroundColor, dragFactory }) => Docs.Create.FontIconDocument({ + _nativeWidth: 100, _nativeHeight: 100, _width: 100, _height: 100, + icon, + title, + label, + ignoreClick, + dropAction: "copy", + onDragStart: drag ? ScriptField.MakeFunction(drag) : undefined, + onClick: click ? ScriptField.MakeScript(click) : undefined, + ischecked: ischecked ? ComputedField.MakeFunction(ischecked) : undefined, + activePen, + backgroundColor, + removeDropProperties: new List<string>(["dropAction"]), + dragFactory, + })); if (dragCreatorSet === undefined) { doc.myItemCreators = new PrefetchProxy(Docs.Create.MasonryDocument(creatorBtns, { @@ -448,13 +508,13 @@ export class CurrentUserUtils { return doc.myWorkspaces as Doc; } - static setupDocumentCollection(doc: Doc) { - if (doc.myDocuments === undefined) { - doc.myDocuments = new PrefetchProxy(Docs.Create.TreeDocument([], { - title: "DOCUMENTS", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: true, lockedPosition: true, + static setupCatalog(doc: Doc) { + if (doc.myCatalog === undefined) { + doc.myCatalog = new PrefetchProxy(Docs.Create.TreeDocument([], { + title: "CATALOG", _height: 42, forceActive: true, boxShadow: "0 0", treeViewPreventOpen: false, lockedPosition: true, })); } - return doc.myDocuments as Doc; + return doc.myCatalog as Doc; } static setupRecentlyClosed(doc: Doc) { // setup Recently Closed library item @@ -474,7 +534,7 @@ export class CurrentUserUtils { // setup the Library button which will display the library panel. This panel includes a collection of workspaces, documents, and recently closed views static setupLibraryPanel(doc: Doc, sidebarContainer: Doc) { const workspaces = CurrentUserUtils.setupWorkspaces(doc); - const documents = CurrentUserUtils.setupDocumentCollection(doc); + const documents = CurrentUserUtils.setupCatalog(doc); const recentlyClosed = CurrentUserUtils.setupRecentlyClosed(doc); if (doc["tabs-button-library"] === undefined) { @@ -482,7 +542,7 @@ export class CurrentUserUtils { _width: 50, _height: 25, title: "Library", _fontSize: 10, letterSpacing: "0px", textTransform: "unset", borderRounding: "5px 5px 0px 0px", boxShadow: "3px 3px 0px rgb(34, 34, 34)", sourcePanel: new PrefetchProxy(Docs.Create.TreeDocument([workspaces, documents, recentlyClosed, doc], { - title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "move", lockedPosition: true, boxShadow: "0 0", dontRegisterChildren: true + title: "Library", _xMargin: 5, _yMargin: 5, _gridGap: 5, forceActive: true, childDropAction: "alias", lockedPosition: true, boxShadow: "0 0", dontRegisterChildViews: true })) as any as Doc, targetContainer: new PrefetchProxy(sidebarContainer) as any as Doc, onClick: ScriptField.MakeScript("this.targetContainer.proto = this.sourcePanel;") @@ -581,7 +641,7 @@ export class CurrentUserUtils { } if (doc.activePresentation === undefined) { doc.activePresentation = Docs.Create.PresDocument(new List<Doc>(), { - title: "Presentation", _viewType: CollectionViewType.Stacking, + title: "Presentation", _viewType: CollectionViewType.Stacking, targetDropAction: "alias", _LODdisable: true, _chromeStatus: "replaced", _showTitle: "title", boxShadow: "0 0" }); } @@ -594,25 +654,37 @@ export class CurrentUserUtils { } static setupClickEditorTemplates(doc: Doc) { - if (doc.childClickFuncs === undefined) { + if (doc["clickFuncs-child"] === undefined) { const openInTarget = Docs.Create.ScriptingDocument(ScriptField.MakeScript( - "docCast(thisContainer.target).then((target) => { target && docCast(this.source).then((source) => { target.proto.data = new List([source || this]); } ); } )", - { target: Doc.name }), { title: "On Child Clicked (open in target)", _width: 300, _height: 200 }); + "docCast(thisContainer.target).then((target) => {" + + " target && docCast(this.source).then((source) => { " + + " target.proto.data = new List([source || this]); " + + " }); " + + "})", + { target: Doc.name }), { title: "Click to open in target", _width: 300, _height: 200, targetScriptKey: "onChildClick" }); + + const openDetail = Docs.Create.ScriptingDocument(ScriptField.MakeScript( + "openOnRight(self.doubleClickView)", + { target: Doc.name }), { title: "Double click to open doubleClickView", _width: 300, _height: 200, targetScriptKey: "onChildDoubleClick" }); - doc.childClickFuncs = Docs.Create.TreeDocument([openInTarget], { title: "on Child Click function templates" }); + doc["clickFuncs-child"] = Docs.Create.TreeDocument([openInTarget, openDetail], { title: "on Child Click function templates" }); } // this is equivalent to using PrefetchProxies to make sure all the childClickFuncs have been retrieved. - PromiseValue(Cast(doc.childClickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); + PromiseValue(Cast(doc["clickFuncs-child"], Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); if (doc.clickFuncs === undefined) { const onClick = Docs.Create.ScriptingDocument(undefined, { title: "onClick", "onClick-rawScript": "console.log('click')", isTemplateDoc: true, isTemplateForField: "onClick", _width: 300, _height: 200 }, "onClick"); + const onDoubleClick = Docs.Create.ScriptingDocument(undefined, { + title: "onDoubleClick", "onDoubleClick-rawScript": "console.log('double click')", + isTemplateDoc: true, isTemplateForField: "onDoubleClick", _width: 300, _height: 200 + }, "onDoubleClick"); const onCheckedClick = Docs.Create.ScriptingDocument(undefined, { title: "onCheckedClick", "onCheckedClick-rawScript": "console.log(heading + checked + containingTreeView)", "onCheckedClick-params": new List<string>(["heading", "checked", "containingTreeView"]), isTemplateDoc: true, isTemplateForField: "onCheckedClick", _width: 300, _height: 200 }, "onCheckedClick"); - doc.clickFuncs = Docs.Create.TreeDocument([onClick, onCheckedClick], { title: "onClick funcs" }); + doc.clickFuncs = Docs.Create.TreeDocument([onClick, onDoubleClick, onCheckedClick], { title: "onClick funcs" }); } PromiseValue(Cast(doc.clickFuncs, Doc)).then(func => func && PromiseValue(func.data).then(DocListCast)); @@ -623,6 +695,11 @@ export class CurrentUserUtils { new InkingControl(); doc.title = Doc.CurrentUserEmail; doc.activePen = doc; + doc.inkColor = StrCast(doc.backgroundColor, ""); + doc.fontSize = NumCast(doc.fontSize, 12); + doc["constants-snapThreshold"] = NumCast(doc["constants-snapThreshold"], 10); // + doc["constants-dragThreshold"] = NumCast(doc["constants-dragThreshold"], 4); // + Utils.DRAG_THRESHOLD = NumCast(doc["constants-dragThreshold"]); this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon this.setupDocTemplates(doc); // sets up the template menu of templates this.setupRightSidebar(doc); // sets up the right sidebar collection for mobile upload documents and sharing diff --git a/src/server/authentication/models/user_model.ts b/src/server/authentication/models/user_model.ts index 78e39dbc1..a0b688328 100644 --- a/src/server/authentication/models/user_model.ts +++ b/src/server/authentication/models/user_model.ts @@ -74,9 +74,9 @@ userSchema.pre("save", function save(next) { const comparePassword: comparePasswordFunction = function (this: DashUserModel, candidatePassword, cb) { // Choose one of the following bodies for authentication logic. - // secure + // secure (expected, default) bcrypt.compare(candidatePassword, this.password, cb); - // bypass password + // bypass password (debugging) // cb(undefined, true); }; diff --git a/src/server/database.ts b/src/server/database.ts index ad285765b..580f7f919 100644 --- a/src/server/database.ts +++ b/src/server/database.ts @@ -4,7 +4,7 @@ import { Opt } from '../new_fields/Doc'; import { Utils, emptyFunction } from '../Utils'; import { Credentials } from 'google-auth-library'; import { GoogleApiServerUtils } from './apis/google/GoogleApiServerUtils'; -import { IDatabase } from './IDatabase'; +import { IDatabase, DocumentsCollection } from './IDatabase'; import { MemoryDatabase } from './MemoryDatabase'; import * as mongoose from 'mongoose'; import { Upload } from './SharedMediaTypes'; @@ -14,7 +14,7 @@ export namespace Database { export let disconnect: Function; const schema = 'Dash'; const port = 27017; - export const url = `mongodb://localhost:${port}/`; + export const url = `mongodb://localhost:${port}/${schema}`; enum ConnectionStates { disconnected = 0, @@ -47,28 +47,29 @@ export namespace Database { } export class Database implements IDatabase { - public static DocumentsCollection = 'documents'; private MongoClient = mongodb.MongoClient; private currentWrites: { [id: string]: Promise<void> } = {}; private db?: mongodb.Db; private onConnect: (() => void)[] = []; - doConnect() { + async doConnect() { console.error(`\nConnecting to Mongo with URL : ${url}\n`); - this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000, useUnifiedTopology: true }, (_err, client) => { - console.error("mongo connect response\n"); - if (!client) { - console.error("\nMongo connect failed with the error:\n"); - console.log(_err); - process.exit(0); - } - this.db = client.db(); - this.onConnect.forEach(fn => fn()); + return new Promise<void>(resolve => { + this.MongoClient.connect(url, { connectTimeoutMS: 30000, socketTimeoutMS: 30000, useUnifiedTopology: true }, (_err, client) => { + console.error("mongo connect response\n"); + if (!client) { + console.error("\nMongo connect failed with the error:\n"); + console.log(_err); + process.exit(0); + } + this.db = client.db(); + this.onConnect.forEach(fn => fn()); + resolve(); + }); }); } - public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { - + public async update(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = DocumentsCollection) { if (this.db) { const collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; @@ -93,7 +94,7 @@ export namespace Database { } } - public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = Database.DocumentsCollection) { + public replace(id: string, value: any, callback: (err: mongodb.MongoError, res: mongodb.UpdateWriteOpResult) => void, upsert = true, collectionName = DocumentsCollection) { if (this.db) { const collection = this.db.collection(collectionName); const prom = this.currentWrites[id]; @@ -117,9 +118,21 @@ export namespace Database { } } + public async getCollectionNames() { + const cursor = this.db?.listCollections(); + const collectionNames: string[] = []; + if (cursor) { + while (await cursor.hasNext()) { + const collection: any = await cursor.next(); + collection && collectionNames.push(collection.name); + } + } + return collectionNames; + } + public delete(query: any, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; public delete(id: string, collectionName?: string): Promise<mongodb.DeleteWriteOpResultObject>; - public delete(id: any, collectionName = Database.DocumentsCollection) { + public delete(id: any, collectionName = DocumentsCollection) { if (typeof id === "string") { id = { _id: id }; } @@ -131,25 +144,26 @@ export namespace Database { } } - public async deleteAll(collectionName = Database.DocumentsCollection, persist = true): Promise<any> { - return new Promise(resolve => { - const executor = async (database: mongodb.Db) => { - if (persist) { - await database.collection(collectionName).deleteMany({}); - } else { - await database.dropCollection(collectionName); - } - resolve(); - }; - if (this.db) { - executor(this.db); + public async dropSchema(...targetSchemas: string[]): Promise<any> { + const executor = async (database: mongodb.Db) => { + const existing = await Instance.getCollectionNames(); + let valid: string[]; + if (targetSchemas.length) { + valid = targetSchemas.filter(collection => existing.includes(collection)); } else { - this.onConnect.push(() => this.db && executor(this.db)); + valid = existing; } - }); + const pending = Promise.all(valid.map(schemaName => database.dropCollection(schemaName))); + return (await pending).every(dropOutcome => dropOutcome); + }; + if (this.db) { + return executor(this.db); + } else { + this.onConnect.push(() => this.db && executor(this.db)); + } } - public async insert(value: any, collectionName = Database.DocumentsCollection) { + public async insert(value: any, collectionName = DocumentsCollection) { if (this.db) { if ("id" in value) { value._id = value.id; @@ -177,7 +191,7 @@ export namespace Database { } } - public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = "newDocuments") { + public getDocument(id: string, fn: (result?: Transferable) => void, collectionName = DocumentsCollection) { if (this.db) { this.db.collection(collectionName).findOne({ _id: id }, (err, result) => { if (result) { @@ -193,7 +207,7 @@ export namespace Database { } } - public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = Database.DocumentsCollection) { + public getDocuments(ids: string[], fn: (result: Transferable[]) => void, collectionName = DocumentsCollection) { if (this.db) { this.db.collection(collectionName).find({ _id: { "$in": ids } }).toArray((err, docs) => { if (err) { @@ -211,7 +225,7 @@ export namespace Database { } } - public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = "newDocuments"): Promise<void> { + public async visit(ids: string[], fn: (result: any) => string[] | Promise<string[]>, collectionName = DocumentsCollection): Promise<void> { if (this.db) { const visited = new Set<string>(); while (ids.length) { @@ -238,7 +252,7 @@ export namespace Database { } } - public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = "newDocuments"): Promise<mongodb.Cursor> { + public query(query: { [key: string]: any }, projection?: { [key: string]: 0 | 1 }, collectionName = DocumentsCollection): Promise<mongodb.Cursor> { if (this.db) { let cursor = this.db.collection(collectionName).find(query); if (projection) { @@ -252,7 +266,7 @@ export namespace Database { } } - public updateMany(query: any, update: any, collectionName = "newDocuments") { + public updateMany(query: any, update: any, collectionName = DocumentsCollection) { if (this.db) { const db = this.db; return new Promise<mongodb.WriteOpResult>(res => db.collection(collectionName).update(query, update, (_, result) => res(result))); @@ -277,12 +291,13 @@ export namespace Database { } } - export const Instance: IDatabase = getDatabase(); + export const Instance = getDatabase(); export namespace Auxiliary { export enum AuxiliaryCollections { - GooglePhotosUploadHistory = "uploadedFromGooglePhotos" + GooglePhotosUploadHistory = "uploadedFromGooglePhotos", + GoogleAuthentication = "googleAuthentication" } const SanitizedCappedQuery = async (query: { [key: string]: any }, collection: string, cap: number, removeId = true) => { @@ -306,27 +321,30 @@ export namespace Database { export namespace GoogleAuthenticationToken { - const GoogleAuthentication = "googleAuthentication"; - - export type StoredCredentials = Credentials & { _id: string }; + type StoredCredentials = GoogleApiServerUtils.EnrichedCredentials & { _id: string }; export const Fetch = async (userId: string, removeId = true): Promise<Opt<StoredCredentials>> => { - return SanitizedSingletonQuery<StoredCredentials>({ userId }, GoogleAuthentication, removeId); + return SanitizedSingletonQuery<StoredCredentials>({ userId }, AuxiliaryCollections.GoogleAuthentication, removeId); }; export const Write = async (userId: string, enrichedCredentials: GoogleApiServerUtils.EnrichedCredentials) => { - return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, GoogleAuthentication); + return Instance.insert({ userId, canAccess: [], ...enrichedCredentials }, AuxiliaryCollections.GoogleAuthentication); }; export const Update = async (userId: string, access_token: string, expiry_date: number) => { const entry = await Fetch(userId, false); if (entry) { const parameters = { $set: { access_token, expiry_date } }; - return Instance.update(entry._id, parameters, emptyFunction, true, GoogleAuthentication); + return Instance.update(entry._id, parameters, emptyFunction, true, AuxiliaryCollections.GoogleAuthentication); } }; - export const DeleteAll = () => Instance.deleteAll(GoogleAuthentication, false); + export const Revoke = async (userId: string) => { + const entry = await Fetch(userId, false); + if (entry) { + Instance.delete({ _id: entry._id }, AuxiliaryCollections.GoogleAuthentication); + } + }; } @@ -338,12 +356,6 @@ export namespace Database { return Instance.insert(bundle, AuxiliaryCollections.GooglePhotosUploadHistory); }; - export const DeleteAll = async (persist = false) => { - const collectionNames = Object.values(AuxiliaryCollections); - const pendingDeletions = collectionNames.map(name => Instance.deleteAll(name, persist)); - return Promise.all(pendingDeletions); - }; - } } diff --git a/src/server/remapUrl.ts b/src/server/remapUrl.ts index 45d2fdd33..91a3cb6bf 100644 --- a/src/server/remapUrl.ts +++ b/src/server/remapUrl.ts @@ -1,6 +1,4 @@ import { Database } from "./database"; -import { Search } from "./Search"; -import * as path from 'path'; //npx ts-node src/server/remapUrl.ts @@ -50,7 +48,7 @@ async function update() { return new Promise(res => Database.Instance.update(doc[0], doc[1], () => { console.log("wrote " + JSON.stringify(doc[1])); res(); - }, false, "newDocuments")); + }, false)); })); console.log("Done"); // await Promise.all(updates.map(update => { |
