diff --git a/.circleci/config.yml b/.circleci/config.yml index 44ec98450..ed8076483 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,7 +33,7 @@ workflows: context: main-builds filters: branches: - only: cristi/ci-test + only: cristi/ci-k8s-tor jobs: test_server: @@ -90,13 +90,21 @@ jobs: command: env SPECKLE_SERVER_PACKAGE=preview-service ./.circleci/build.sh - run: name: Deploy - command: ./.circleci/deploy.sh + command: | + ./.circleci/deploy.sh + if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then + env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_RELEASE} ./.circleci/deploy_in_new_setup.sh + else + env K8S_CLUSTER=TOR1 K8S_NAMESPACE=${K8S_NAMESPACE_TOR1_1_LATEST} ./.circleci/deploy_in_new_setup.sh + fi - run: name: Test deployment command: | ./utils/test-deployment/install_prerequisites.sh - SPECKLE_SERVER=https://latest.speckle.dev if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then - SPECKLE_SERVER=https://speckle.xyz + ./utils/test-deployment/run_tests.py https://speckle.xyz + ./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_RELEASE} + else + ./utils/test-deployment/run_tests.py https://latest.speckle.dev + ./utils/test-deployment/run_tests.py ${SPECKLE_URL_TOR1_1_LATEST} fi - ./utils/test-deployment/run_tests.py $SPECKLE_SERVER diff --git a/.circleci/deploy_in_new_setup.sh b/.circleci/deploy_in_new_setup.sh new file mode 100755 index 000000000..b0b4a20c0 --- /dev/null +++ b/.circleci/deploy_in_new_setup.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -e + +K8S_CLUSTER_CERTIFICATE_VARIABLE=K8S_${K8S_CLUSTER}_CERTIFICATE +K8S_CLUSTER_CERTIFICATE=${!K8S_CLUSTER_CERTIFICATE_VARIABLE} + +K8S_TOKEN_VARIABLE=K8S_${K8S_CLUSTER}_TOKEN +K8S_TOKEN=${!K8S_TOKEN_VARIABLE} + +K8S_SERVER_VARIABLE=K8S_${K8S_CLUSTER}_SERVER +K8S_SERVER=${!K8S_SERVER_VARIABLE} + +# K8S_NAMESPACE + +IMAGE_VERSION_TAG=$CIRCLE_SHA1 + +if [[ "$CIRCLE_TAG" =~ ^v.* ]]; then + IMAGE_VERSION_TAG=$CIRCLE_TAG +fi + +echo "$K8S_CLUSTER_CERTIFICATE" | base64 --decode > k8s_cert.crt + +# Update deployments +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + set image deployment/speckle-frontend main=$DOCKER_IMAGE_TAG-frontend:$IMAGE_VERSION_TAG + +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + set image deployment/speckle-server main=$DOCKER_IMAGE_TAG-server:$IMAGE_VERSION_TAG + +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + set image deployment/speckle-preview-service main=$DOCKER_IMAGE_TAG-preview-service:$IMAGE_VERSION_TAG + + +# Wait for rollout to complete +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + rollout status -w deployment/speckle-frontend --timeout=3m + +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + rollout status -w deployment/speckle-server --timeout=3m + +./kubectl \ + --kubeconfig=/dev/null \ + --server=$K8S_SERVER \ + --certificate-authority=k8s_cert.crt \ + --token=$K8S_TOKEN \ + --namespace=$K8S_NAMESPACE \ + rollout status -w deployment/speckle-preview-service --timeout=3m diff --git a/packages/frontend/package-lock.json b/packages/frontend/package-lock.json index 3ebad0591..e831be58c 100644 --- a/packages/frontend/package-lock.json +++ b/packages/frontend/package-lock.json @@ -12963,6 +12963,11 @@ "is-plain-obj": "^1.0.0" } }, + "sortablejs": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz", + "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A==" + }, "source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", @@ -14821,6 +14826,14 @@ "@seregpie/claw": "^3.0.0" } }, + "vuedraggable": { + "version": "2.24.3", + "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.3.tgz", + "integrity": "sha512-6/HDXi92GzB+Hcs9fC6PAAozK1RLt1ewPTLjK0anTYguXLAeySDmcnqE8IC0xa7shvSzRjQXq3/+dsZ7ETGF3g==", + "requires": { + "sortablejs": "1.10.2" + } + }, "vuetify": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-2.4.0.tgz", diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 10f178b11..0a2241932 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -29,6 +29,7 @@ "vue-matomo": "^3.14.0-0", "vue-router": "^3.4.9", "vue-timeago": "^5.1.2", + "vuedraggable": "^2.24.3", "vuetify": "^2.3.21", "vuetify-image-input": "^19.1.0", "vuex": "^3.6.0" diff --git a/packages/frontend/src/components/GlobalsBuilder.vue b/packages/frontend/src/components/GlobalsBuilder.vue new file mode 100644 index 000000000..10e37c24a --- /dev/null +++ b/packages/frontend/src/components/GlobalsBuilder.vue @@ -0,0 +1,292 @@ + + + + + + Globals + + mdi-source-commit + {{ commitMessage }} + + + These global variables can be used for storing design values, project requirements, notes, or + any info you want to keep track of alongside your geometry. Variable values can be text, numbers, + lists, or booleans. Click the box icon next to any field to turn it into a nested group of + fields, and drag and drop fields in and out of groups as you please! Note that field order + may not always be preserved. + + + You are free to play around with the globals here, but you do not have the required stream + permission to save your changes. + + + + + + clear + + + reset all + + + save + + + + + + + + + + + + + + diff --git a/packages/frontend/src/components/GlobalsEntry.vue b/packages/frontend/src/components/GlobalsEntry.vue new file mode 100644 index 000000000..687d9fb7a --- /dev/null +++ b/packages/frontend/src/components/GlobalsEntry.vue @@ -0,0 +1,292 @@ + + + + + + + + + mdi-minus + + + + + mdi-cube-outline + + + + + + + + + mdi-minus + + {{ entry.key }} + + mdi-pencil + + + + + + mdi-check + + + + + + mdi-arrow-collapse-down + + + + + + + + + + + mdi-plus + + + + + + diff --git a/packages/frontend/src/components/ListItemCommit.vue b/packages/frontend/src/components/ListItemCommit.vue index c3feae697..7703979e1 100644 --- a/packages/frontend/src/components/ListItemCommit.vue +++ b/packages/frontend/src/components/ListItemCommit.vue @@ -1,5 +1,5 @@ - + mdi-source-commit {{ stream.commits.items[0].id }} @@ -38,7 +42,11 @@ on mdi-source-branch {{ stream.commits.items[0].branchName }} @@ -93,6 +101,7 @@ mdi-cog-outline Edit + + + + Globals + + + Collaborators diff --git a/packages/frontend/src/components/UserAvatar.vue b/packages/frontend/src/components/UserAvatar.vue index b3a7d5e05..ed5fca569 100644 --- a/packages/frontend/src/components/UserAvatar.vue +++ b/packages/frontend/src/components/UserAvatar.vue @@ -2,12 +2,22 @@ - + + + + - + @@ -24,6 +34,13 @@ + + + Speckle Ghost + + This user no longer exists. + + diff --git a/packages/frontend/src/components/dialogs/BranchEditDialog.vue b/packages/frontend/src/components/dialogs/BranchEditDialog.vue index 311070322..4cd650c4d 100644 --- a/packages/frontend/src/components/dialogs/BranchEditDialog.vue +++ b/packages/frontend/src/components/dialogs/BranchEditDialog.vue @@ -1,74 +1,64 @@ - - - - - - Edit Branch - - - - - - - Save + + + + + + + Edit Branch + + + + + + + + Cancel + Save + + + + Delete Branch + + + Are you sure? + + You cannot undo this action. The branch + {{ branch.name }} + will be permanently deleted. + + + + Cancel + Delete + + + - - - Delete Branch - - - Are you sure? - - You cannot undo this action. The branch - {{ name }} - will be permanently deleted. - - - - Cancel - Delete - - - - - - - You cannot edit the main branch. - - + + + You cannot edit the main branch. + + + diff --git a/packages/frontend/src/components/dialogs/GlobalsSaveDialog.vue b/packages/frontend/src/components/dialogs/GlobalsSaveDialog.vue new file mode 100644 index 000000000..aed0cc3ce --- /dev/null +++ b/packages/frontend/src/components/dialogs/GlobalsSaveDialog.vue @@ -0,0 +1,94 @@ + + + + + + Save Globals + + + + + + + Save + + + + + diff --git a/packages/frontend/src/components/dialogs/ServerInviteDialog.vue b/packages/frontend/src/components/dialogs/ServerInviteDialog.vue index d04b7ccbb..189fc4e08 100644 --- a/packages/frontend/src/components/dialogs/ServerInviteDialog.vue +++ b/packages/frontend/src/components/dialogs/ServerInviteDialog.vue @@ -20,7 +20,11 @@ :rules="validation.emailRules" label="email" > - + Send invite @@ -32,6 +36,7 @@ + + diff --git a/packages/frontend/src/views/StreamMain.vue b/packages/frontend/src/views/StreamMain.vue index 6c44c9dfe..c4221a66c 100644 --- a/packages/frontend/src/views/StreamMain.vue +++ b/packages/frontend/src/views/StreamMain.vue @@ -9,7 +9,7 @@ mdi-source-branch - {{ branches.totalCount }} branch{{ branches.totalCount > 1 ? 'es' : '' }} + {{ branches.length }} branch{{ branches.length > 1 ? 'es' : '' }} + + + + + + + + + + + mdi-dots-vertical + + + + + + mdi-plus-circle-outline + + + New branch + + + + + + mdi-cog-outline + + + Edit {{ selectedBranch.name }} + + + + + + + The last commit of this stream is on the + + {{ stream.commit.branchName }} + + branch, see + + {{ stream.commit.message }} + + + @@ -152,7 +228,7 @@ /> - Description + Stream Description !b.name.startsWith('globals')) } }, description: { @@ -247,6 +328,26 @@ export default { }, update: (data) => data.stream.description }, + stream: { + query: gql` + query($id: String!) { + stream(id: $id) { + id + commit { + branchName + id + message + } + } + } + `, + variables() { + return { + id: this.$route.params.streamId + } + } + //update: (data) => data.stream.description + }, $subscribe: { branchCreated: { query: gql` @@ -297,7 +398,7 @@ export default { }, branchNames() { if (!this.branches) return [] - return this.branches.items.map((b) => b.name) + return this.branches.map((b) => b.name) }, compiledStreamDescription() { if (!this.description) return '' @@ -337,15 +438,48 @@ export default { this.dialogDescription = false this.$apollo.queries.description.refetch() }, - closeBranchDialog() { - this.dialogBranch = false - this.$apollo.queries.branches.refetch() + editBranch() { + this.$refs.editBranchDialog.open(this.selectedBranch).then((dialog) => { + if (!dialog.result) return + else if (dialog.deleted) { + this.$router.push({ path: `/streams/${this.$route.params.streamId}` }) + } else if (dialog.name !== this.selectedBranch.name) { + //this.$router.push does not work, refresh entire window + window.location = + window.origin + + '/streams/' + + this.$route.params.streamId + + '/branches/' + + encodeURIComponent(dialog.name) + } else { + this.$apollo.queries.branches.refetch() + } + }) + }, + newBranch() { + this.$refs.newBranchDialog + .open( + this.$route.params.streamId, + this.branches.map((b) => b.name) + ) + .then((dialog) => { + if (!dialog.result) return + else { + //this.$router.push does not work, refresh entire window + window.location = + window.origin + + '/streams/' + + this.$route.params.streamId + + '/branches/' + + encodeURIComponent(dialog.name) + } + }) }, selectBranch() { if (!this.branches) return let branchName = this.$route.params.branchName ? this.$route.params.branchName : 'main' - let index = this.branches.items.findIndex((x) => x.name === branchName) - if (index > -1) this.selectedBranch = this.branches.items[index] + let index = this.branches.findIndex((x) => x.name === branchName) + if (index > -1) this.selectedBranch = this.branches[index] else this.error = 'Branch ' + branchName + ' does not exist' }, changeBranch() { diff --git a/packages/frontend/src/views/Streams.vue b/packages/frontend/src/views/Streams.vue index d2fef85dd..5246aad14 100644 --- a/packages/frontend/src/views/Streams.vue +++ b/packages/frontend/src/views/Streams.vue @@ -39,7 +39,13 @@ - + {{ a.message }} diff --git a/packages/server/modules/core/graph/resolvers/branches.js b/packages/server/modules/core/graph/resolvers/branches.js index fa8a98801..4779743a7 100644 --- a/packages/server/modules/core/graph/resolvers/branches.js +++ b/packages/server/modules/core/graph/resolvers/branches.js @@ -40,7 +40,9 @@ module.exports = { Branch: { async author( parent, args, context, info ) { - return await getUserById( { userId: parent.authorId } ) + if ( parent.userId ) + return await getUserById( { userId: parent.authorId } ) + else return null } }, diff --git a/packages/server/modules/core/graph/resolvers/users.js b/packages/server/modules/core/graph/resolvers/users.js index 6e3c33b6d..af85ab1d2 100644 --- a/packages/server/modules/core/graph/resolvers/users.js +++ b/packages/server/modules/core/graph/resolvers/users.js @@ -40,8 +40,8 @@ module.exports = { if ( args.limit && args.limit > 100 ) throw new UserInputError( 'Cannot return more than 100 items, please use pagination.' ) - let {cursor, users} = await searchUsers( args.query, args.limit, args.cursor ) - return {cursor: cursor, items: users} + let { cursor, users } = await searchUsers( args.query, args.limit, args.cursor ) + return { cursor: cursor, items: users } }, async userPwdStrength( parent, args, context, info ) { diff --git a/packages/server/modules/core/graph/schemas/branchesAndCommits.graphql b/packages/server/modules/core/graph/schemas/branchesAndCommits.graphql index f2f56cc59..89aaa5a68 100644 --- a/packages/server/modules/core/graph/schemas/branchesAndCommits.graphql +++ b/packages/server/modules/core/graph/schemas/branchesAndCommits.graphql @@ -12,7 +12,7 @@ extend type User { type Branch { id: String! name: String! - author: User! + author: User description: String commits(limit: Int! = 25, cursor: String): CommitCollection } diff --git a/packages/server/modules/core/migrations/20210603160000_optional_user_references.js b/packages/server/modules/core/migrations/20210603160000_optional_user_references.js new file mode 100644 index 000000000..8d5de7ea2 --- /dev/null +++ b/packages/server/modules/core/migrations/20210603160000_optional_user_references.js @@ -0,0 +1,44 @@ +// /* istanbul ignore file */ +exports.up = async ( knex ) => { + await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" DROP NOT NULL;' ) + await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' ) + await knex.raw( ` + ALTER TABLE commits + ADD CONSTRAINT commits_author_foreign + FOREIGN KEY (author) + REFERENCES users(id) + ON DELETE SET NULL; + ` ) + + await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' ) + await knex.raw( ` + ALTER TABLE branches + ADD CONSTRAINT branches_authorid_foreign + FOREIGN KEY ("authorId") + REFERENCES users(id) + ON DELETE SET NULL; + ` ) +} + +exports.down = async ( knex ) => { + // NOTE: + // This migration cannot run backwards: if a user deletes their account, the previous not null + // constraint cannot be satisfied. Therefore, there's no going back (and there isn't really a need either). + + // await knex.raw( 'ALTER TABLE branches DROP CONSTRAINT branches_authorid_foreign;' ) + // await knex.raw( ` + // ALTER TABLE branches + // ADD CONSTRAINT branches_authorid_foreign + // FOREIGN KEY ("authorId") + // REFERENCES users(id); + // ` ) + + // await knex.raw( 'ALTER TABLE commits DROP CONSTRAINT commits_author_foreign;' ) + // await knex.raw( ` + // ALTER TABLE commits + // ADD CONSTRAINT commits_author_foreign + // FOREIGN KEY (author) + // REFERENCES users(id); + // ` ) + // await knex.raw( 'ALTER TABLE commits ALTER COLUMN "author" SET NOT NULL;' ) +} diff --git a/packages/server/modules/core/rest/diffDownload.js b/packages/server/modules/core/rest/diffDownload.js index 5bdc9750b..36e157c7f 100644 --- a/packages/server/modules/core/rest/diffDownload.js +++ b/packages/server/modules/core/rest/diffDownload.js @@ -8,9 +8,11 @@ const cors = require( 'cors' ) const { matomoMiddleware } = require( `${appRoot}/logging/matomoHelper` ) const { contextMiddleware, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` ) const { validatePermissionsReadStream } = require( './authUtils' ) - +const { SpeckleObjectsStream } = require( './speckleObjectsStream' ) const { getObjectsStream } = require( '../services/objects' ) +const { pipeline } = require( 'stream' ) + module.exports = ( app ) => { app.options( '/api/getobjects/:streamId', cors() ) @@ -24,72 +26,25 @@ module.exports = ( app ) => { let simpleText = req.headers.accept === 'text/plain' - let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } ) - - let currentChunkSize = 0 - let maxChunkSize = 50000 - let chunk = simpleText ? '' : [ ] - let isFirstBuffer = true - res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } ) - const gzip = zlib.createGzip( ) + let dbStream = await getObjectsStream( { streamId: req.params.streamId, objectIds: childrenList } ) + let speckleObjStream = new SpeckleObjectsStream( simpleText ) + let gzipStream = zlib.createGzip( ) - if ( !simpleText ) gzip.write( '[' ) - - // helper func to flush the gzip buffer - const writeBuffer = () => { - if ( simpleText ) { - gzip.write( chunk ) - } else { - if ( !isFirstBuffer ) { - gzip.write( ',' ) - } - gzip.write( chunk.join( ',' ) ) - } - gzip.flush( ) - chunk = simpleText ? '' : [ ] - isFirstBuffer = false - } - - let k = 0 - let requestDropped = false - dbStream.on( 'data', row => { - try { - let data = JSON.stringify( row.data ) - currentChunkSize += Buffer.byteLength( data, 'utf8' ) - if ( simpleText ) { - chunk += `${row.data.id}\t${data}\n` + pipeline( + dbStream, + speckleObjStream, + gzipStream, + res, + ( err ) => { + if ( err ) { + debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error streaming objects from stream ${req.params.streamId}: ${err}` ) } else { - chunk.push( data ) + debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Streamed ${childrenList.length} objects from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` ) } - if ( currentChunkSize >= maxChunkSize ) { - currentChunkSize = 0 - writeBuffer() - } - k++ - } catch ( e ) { - requestDropped = true - debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` ) - return } - } ) + ) - dbStream.on( 'error', err => { - debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` ) - requestDropped = true - return - } ) - - dbStream.on( 'end', ( ) => { - if ( currentChunkSize !== 0 ) { - writeBuffer() - } - if ( !simpleText ) gzip.write( ']' ) - gzip.end( ) - } ) - - // 🚬 - gzip.pipe( res ) } ) } diff --git a/packages/server/modules/core/rest/diffUpload.js b/packages/server/modules/core/rest/diffUpload.js index 00b72e0fa..c1a437a0a 100644 --- a/packages/server/modules/core/rest/diffUpload.js +++ b/packages/server/modules/core/rest/diffUpload.js @@ -19,6 +19,8 @@ module.exports = ( app ) => { let objectList = JSON.parse( req.body.objects ) + debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Diffing ${objectList.length} objects for stream ${req.params.streamId}` ) + let response = await hasObjects( { streamId: req.params.streamId, objectIds: objectList } ) // console.log(response) res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': 'application/json' } ) diff --git a/packages/server/modules/core/rest/download.js b/packages/server/modules/core/rest/download.js index 96437fd80..d17bba6a4 100644 --- a/packages/server/modules/core/rest/download.js +++ b/packages/server/modules/core/rest/download.js @@ -10,6 +10,8 @@ const { contextMiddleware } = require( `${appRoot}/modules/shared` ) const { validatePermissionsReadStream } = require( './authUtils' ) const { getObject, getObjectChildrenStream } = require( '../services/objects' ) +const { SpeckleObjectsStream } = require( './speckleObjectsStream' ) +const { pipeline } = require( 'stream' ) module.exports = ( app ) => { @@ -28,85 +30,30 @@ module.exports = ( app ) => { return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` ) } - obj = obj.data - let simpleText = req.headers.accept === 'text/plain' - let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } ) - - let currentChunkSize = 0 - let maxChunkSize = 50000 - let chunk = simpleText ? '' : [ ] - let isFirst = true - - res.writeHead( 200, { 'Content-Encoding': 'gzip', 'Content-Type': simpleText ? 'text/plain' : 'application/json' } ) - const gzip = zlib.createGzip( ) + let dbStream = await getObjectChildrenStream( { streamId: req.params.streamId, objectId: req.params.objectId } ) + let speckleObjStream = new SpeckleObjectsStream( simpleText ) + let gzipStream = zlib.createGzip( ) - if ( !simpleText ) gzip.write( '[' ) + speckleObjStream.write( obj ) - // helper func to flush the gzip buffer - const writeBuffer = ( addStartingComma ) => { - if ( simpleText ) { - gzip.write( chunk ) - } else { - if ( addStartingComma ) { - gzip.write( ',' ) - } - gzip.write( chunk.join( ',' ) ) - } - gzip.flush( ) - chunk = simpleText ? '' : [ ] - } - - var objString = JSON.stringify( obj ) - if ( simpleText ) { - chunk += `${obj.id}\t${objString}\n` - } else { - chunk.push( objString ) - } - writeBuffer( false ) - - let k = 0 - let requestDropped = false - dbStream.on( 'data', row => { - try { - let data = JSON.stringify( row.data ) - currentChunkSize += Buffer.byteLength( data, 'utf8' ) - if ( simpleText ) { - chunk += `${row.data.id}\t${data}\n` + pipeline( + dbStream, + speckleObjStream, + gzipStream, + res, + ( err ) => { + if ( err ) { + debug( 'speckle:error' )( `[User ${req.context.userId || '-'}] Error downloading object ${req.params.objectId} from stream ${req.params.streamId}: ${err}` ) } else { - chunk.push( data ) + debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded object ${req.params.objectId} from stream ${req.params.streamId} (size: ${gzipStream.bytesWritten / 1000000} MB)` ) } - if ( currentChunkSize >= maxChunkSize ) { - currentChunkSize = 0 - writeBuffer( true ) - } - k++ - } catch ( e ) { - requestDropped = true - debug( 'speckle:error' )( `'Failed to find object, or object is corrupted.' ${req.params.objectId}` ) - return } - } ) + ) - dbStream.on( 'error', err => { - debug( 'speckle:error' )( `Error in streaming object children for ${req.params.objectId}: ${err}` ) - requestDropped = true - return - } ) - - dbStream.on( 'end', ( ) => { - if ( currentChunkSize !== 0 ) { - writeBuffer( true ) - } - if ( !simpleText ) gzip.write( ']' ) - gzip.end( ) - } ) - - // 🚬 - gzip.pipe( res ) } ) app.options( '/objects/:streamId/:objectId/single', cors() ) @@ -122,6 +69,8 @@ module.exports = ( app ) => { return res.status( 404 ).send( `Failed to find object ${req.params.objectId}.` ) } + debug( 'speckle:info' )( `[User ${req.context.userId || '-'}] Downloaded single object ${req.params.objectId} from stream ${req.params.streamId}` ) + res.send( obj.data ) } ) } diff --git a/packages/server/modules/core/rest/speckleObjectsStream.js b/packages/server/modules/core/rest/speckleObjectsStream.js new file mode 100644 index 000000000..95ff22d03 --- /dev/null +++ b/packages/server/modules/core/rest/speckleObjectsStream.js @@ -0,0 +1,37 @@ +const { Transform } = require( 'stream' ) + +// A stream that converts database objects stream to "{id}\t{data_json}\n" stream or a json stream of obj.data fields + +class SpeckleObjectsStream extends Transform { + constructor( simpleText ) { + super( { writableObjectMode: true } ) + this.simpleText = simpleText + + if ( !this.simpleText ) this.push( '[' ) + this.isFirstObject = true + } + + _transform( dbObj, encoding, callback ) { + try { + if ( this.simpleText ) { + this.push( `${dbObj.data.id}\t${JSON.stringify( dbObj.data )}\n` ) + } else { + // JSON output + if ( !this.isFirstObject ) this.push( ',' ) + this.push( JSON.stringify( dbObj.data ) ) + this.isFirstObject = false + } + callback() + } catch ( e ) { + callback( e ) + } + } + + _flush( callback ) { + if ( !this.simpleText ) this.push( ']' ) + callback() + } + +} + +exports.SpeckleObjectsStream = SpeckleObjectsStream diff --git a/packages/server/modules/core/rest/upload.js b/packages/server/modules/core/rest/upload.js index 994658b1a..1baae71ba 100644 --- a/packages/server/modules/core/rest/upload.js +++ b/packages/server/modules/core/rest/upload.js @@ -18,8 +18,6 @@ module.exports = ( app ) => { return res.status( hasStreamAccess.status ).end() } - debug( 'speckle:upload-endpoint' )( 'Upload started' ) - let busboy = new Busboy( { headers: req.headers } ) let totalProcessed = 0 let last = {} @@ -94,7 +92,6 @@ module.exports = ( app ) => { await Promise.all( promises ) - debug( 'speckle:upload-endpoint' )( 'Upload ended' ) res.status( 201 ).end( ) } ) diff --git a/packages/server/modules/core/services/commits.js b/packages/server/modules/core/services/commits.js index b08b60de0..8af28a67e 100644 --- a/packages/server/modules/core/services/commits.js +++ b/packages/server/modules/core/services/commits.js @@ -63,9 +63,9 @@ module.exports = { let query = await Commits( ) .columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] ) .select( ) - .join( 'users', 'commits.author', 'users.id' ) .join( 'branch_commits', 'commits.id', 'branch_commits.commitId' ) .join( 'branches', 'branches.id', 'branch_commits.branchId' ) + .leftJoin( 'users', 'commits.author', 'users.id' ) .where( { 'commits.id': id } ) .first( ) return await query @@ -97,8 +97,8 @@ module.exports = { .columns( [ { id: 'commitId' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' },{ authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] ) .select( ) .join( 'commits', 'commits.id', 'branch_commits.commitId' ) - .join( 'users', 'commits.author', 'users.id' ) .join( 'branches', 'branches.id', 'branch_commits.branchId' ) + .leftJoin( 'users', 'commits.author', 'users.id' ) .where( 'branchId', branchId ) if ( cursor ) @@ -132,9 +132,9 @@ module.exports = { .columns( [ { id: 'commits.id' }, 'message', 'referencedObject', 'sourceApplication', 'totalChildrenCount', 'parents', 'commits.createdAt', { branchName: 'branches.name' }, { authorName: 'users.name' }, { authorId: 'users.id' }, { authorAvatar: 'users.avatar' } ] ) .select( ) .join( 'commits', 'commits.id', 'stream_commits.commitId' ) - .join( 'users', 'commits.author', 'users.id' ) .join( 'branch_commits', 'commits.id', 'branch_commits.commitId' ) .join( 'branches', 'branches.id', 'branch_commits.branchId' ) + .leftJoin( 'users', 'commits.author', 'users.id' ) .where( 'stream_commits.streamId', streamId ) diff --git a/packages/server/modules/core/services/objects.js b/packages/server/modules/core/services/objects.js index 9c7bcbdce..0d715b5e7 100644 --- a/packages/server/modules/core/services/objects.js +++ b/packages/server/modules/core/services/objects.js @@ -203,7 +203,7 @@ module.exports = { } ) .where( knex.raw( 'object_children_closure."streamId" = ? AND parent = ?', [ streamId, objectId ] ) ) .orderBy( 'objects.id' ) - return q.stream( ) + return q.stream( { highWaterMark: 2 } ) }, async getObjectChildren( { streamId, objectId, limit, depth, select, cursor } ) { @@ -442,7 +442,7 @@ module.exports = { .andWhere( 'streamId', streamId ) .orderBy( 'id' ) .select( 'id', 'speckleType', 'totalChildrenCount', 'totalChildrenCountByDepth', 'createdAt', 'data' ) - return res.stream( ) + return res.stream( { highWaterMark: 2 } ) }, async hasObjects( { streamId, objectIds } ) { diff --git a/packages/server/modules/core/services/users.js b/packages/server/modules/core/services/users.js index bd86ae907..438430794 100644 --- a/packages/server/modules/core/services/users.js +++ b/packages/server/modules/core/services/users.js @@ -63,14 +63,16 @@ module.exports = { async getUserById( { userId } ) { let user = await Users( ).where( { id: userId } ).select( '*' ).first( ) - delete user.passwordDigest + if ( user ) + delete user.passwordDigest return user }, // TODO: deprecate async getUser( id ) { let user = await Users( ).where( { id: id } ).select( '*' ).first( ) - delete user.passwordDigest + if ( user ) + delete user.passwordDigest return user }, @@ -150,8 +152,6 @@ module.exports = { for ( let i in streams.rows ) { await deleteStream( { streamId: streams.rows[i].id } ) } - await knex.raw( 'DELETE FROM commits WHERE author = ?', [ id ] ) - await knex.raw( 'DELETE FROM branches WHERE "authorId" = ?', [ id ] ) return await Users( ).where( { id: id } ).del( ) } diff --git a/packages/server/modules/core/tests/users.spec.js b/packages/server/modules/core/tests/users.spec.js index 02232d4e9..568a75faa 100644 --- a/packages/server/modules/core/tests/users.spec.js +++ b/packages/server/modules/core/tests/users.spec.js @@ -15,6 +15,21 @@ const { createUser, findOrCreateUser, getUser, searchUsers, updateUser, deleteUs const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' ) const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' ) +const { + createBranch, + getBranchesByStreamId +} = require( '../services/branches' ) + +const { + createCommitByBranchName, + getCommitsByBranchName, + getCommitById, + getCommitsByStreamId, + deleteCommit, +} = require( '../services/commits' ) + +const { createObject, createObjects } = require( '../services/objects' ) + describe( 'Actors & Tokens @user-services', ( ) => { let myTestActor = { name: 'Dimitrie Stefanescu', @@ -110,29 +125,53 @@ describe( 'Actors & Tokens @user-services', ( ) => { } ) + // Note: deletion is more complicated. it( 'Should delete a user', async ( ) => { let soloOwnerStream = { name: 'Test Stream 01', description: 'wonderful test stream', isPublic: true } let multiOwnerStream = { name: 'Test Stream 02', description: 'another test stream', isPublic: true } + soloOwnerStream.id = await createStream( { ...soloOwnerStream, ownerId: ballmerUserId } ) multiOwnerStream.id = await createStream( { ...multiOwnerStream, ownerId: ballmerUserId } ) + await grantPermissionsStream( { streamId: multiOwnerStream.id, userId: myTestActor.id, role: 'stream:owner' } ) + // create a branch for ballmer on the multiowner stream + let branch = { name: 'ballmer/dev' } + branch.id = await createBranch( { ...branch, streamId: multiOwnerStream.id, authorId: ballmerUserId } ) + + let branchSecond = { name: 'steve/jobs' } + branchSecond.id = await createBranch( { ...branchSecond, streamId: multiOwnerStream.id, authorId: myTestActor.id } ) + + // create an object and a commit around it on the multiowner stream + let objId = await createObject( multiOwnerStream.id, { pie: 'in the sky' } ) + let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId:objId, authorId: ballmerUserId } ) + await deleteUser( ballmerUserId ) if ( await getStream( { streamId: soloOwnerStream.id } ) !== undefined ) { assert.fail( 'user stream not deleted' ) } + let multiOwnerStreamCopy = await getStream( { streamId: multiOwnerStream.id } ) if ( !multiOwnerStreamCopy || multiOwnerStreamCopy.id != multiOwnerStream.id ) { assert.fail( 'shared stream deleted' ) } - try { - let user = await getUser( ballmerUserId ) - } catch ( e ) { - return - } - assert.fail( 'user not deleted' ) + let branches = await getBranchesByStreamId( { streamId: multiOwnerStream.id } ) + expect( branches.items.length ).to.equal( 3 ) + + let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName:'ballmer/dev' } ) + expect( branchCommits.commits.length ).to.equal( 1 ) + + let commit = await getCommitById( { id: commitId } ) + expect( commit ).to.be.not.null + + let commitsByStreamId = await getCommitsByStreamId( { streamId: multiOwnerStream.id } ) + expect( commitsByStreamId.commits.length ).to.equal( 1 ) + + let user = await getUser( ballmerUserId ) + if ( user ) + assert.fail( 'user not deleted' ) } ) it( 'Should get a user', async ( ) => { diff --git a/packages/server/modules/serverinvites/services/index.js b/packages/server/modules/serverinvites/services/index.js index ac97bbd4a..f0d5c5390 100644 --- a/packages/server/modules/serverinvites/services/index.js +++ b/packages/server/modules/serverinvites/services/index.js @@ -2,6 +2,7 @@ const appRoot = require( 'app-root-path' ) const crs = require( 'crypto-random-string' ) const knex = require( `${appRoot}/db/knex` ) +const sanitizeHtml = require( 'sanitize-html' ) const { getUserByEmail, getUserById } = require( `${appRoot}/modules/core/services/users` ) @@ -19,6 +20,15 @@ module.exports = { if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' ) + if ( message ) { + + if ( message.length >= 1024 ) { + throw new Error( 'Personal message too long.' ) + } + + message = module.exports.sanitizeMessage( message ) + } + // check if email is already invited let existingInvite = await module.exports.getInviteByEmail( { email } ) if ( existingInvite ) throw new Error( 'Already invited!' ) @@ -126,5 +136,11 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep await Invites().where( { id: id } ).update( { used: true } ) return true + }, + + async sanitizeMessage( message ) { + return sanitizeHtml( message, { + allowedTags: [ 'b', 'i', 'em', 'strong' ], + } ) } } diff --git a/packages/server/modules/serverinvites/tests/serverInvites.spec.js b/packages/server/modules/serverinvites/tests/serverInvites.spec.js index 1bed94c85..9b9f354c5 100644 --- a/packages/server/modules/serverinvites/tests/serverInvites.spec.js +++ b/packages/server/modules/serverinvites/tests/serverInvites.spec.js @@ -4,6 +4,7 @@ const chai = require( 'chai' ) const request = require( 'supertest' ) const assert = require( 'assert' ) const appRoot = require( 'app-root-path' ) +const { async } = require( 'crypto-random-string' ) const { init, startHttp } = require( `${appRoot}/app` ) const expect = chai.expect @@ -11,9 +12,8 @@ const expect = chai.expect const knex = require( `${appRoot}/db/knex` ) const { createUser } = require( `${appRoot}/modules/core/services/users` ) -const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite } = require( `${appRoot}/modules/serverinvites/services` ) +const { createAndSendInvite, getInviteById, getInviteByEmail, validateInvite, useInvite, sanitizeMessage } = require( `${appRoot}/modules/serverinvites/services` ) const { createStream, getStream, getStreamUsers, getUserStreams } = require( `${appRoot}/modules/core/services/streams` ) -const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services/tokens` ) const serverAddress = `http://localhost:${process.env.PORT || 3000}` @@ -55,30 +55,51 @@ describe( 'Server Invites @server-invites', ( ) => { try { await createAndSendInvite( { email:'cat@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } ) - assert.fail() } catch ( e ) { - // pass + return } + assert.fail( 'should not allow multiple invites for the same email' ) } ) it( 'should not allow self invites', async() => { try { await createAndSendInvite( { email: 'didimitrie-100@gmail.com', inviterId: actor.id } ) - assert.fail() } catch ( e ) { - // pass + return } + assert.fail( 'should not allow self invites' ) } ) it( 'should not allow invites from no user', async() => { try { await createAndSendInvite( { email: 'didimitrie233-100@gmail.com', inviterId: 'fake' } ) - assert.fail() } catch ( e ) { - // pass + return } + assert.fail( 'should not allow invites from no user' ) + } ) + + it( 'should not allow invites with a too long message', async() => { + + try { + let inviteId = await createAndSendInvite( { + email: '123456@gmail.com', + inviterId: actor.id, + message: longInviteMessage + } ) + } catch ( e ){ + return + } + + assert.fail( 'created invite with too long message' ) + } ) + + it( 'should sanitize invite messages', async() => { + let clean = await sanitizeMessage( 'Click on my spam link please!' ) + const includesLink = clean.includes( ' { @@ -220,3 +241,6 @@ function sendRequest( auth, obj, address = serverAddress ) { return chai.request( address ).post( '/graphql' ).set( 'Authorization', auth ).send( obj ) } + +const longInviteMessage = + 'Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.' diff --git a/packages/server/package-lock.json b/packages/server/package-lock.json index 603e828bb..846ed5e9d 100644 --- a/packages/server/package-lock.json +++ b/packages/server/package-lock.json @@ -4498,6 +4498,11 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==" + }, "default-require-extensions": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.0.tgz", @@ -4666,6 +4671,39 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, + "domhandler": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, "dot-prop": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz", @@ -4798,6 +4836,11 @@ "ansi-colors": "^4.1.1" } }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, "env-paths": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz", @@ -6794,6 +6837,17 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "http-cache-semantics": { "version": "3.8.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", @@ -7834,6 +7888,11 @@ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, + "klona": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", + "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==" + }, "knex": { "version": "0.21.15", "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.15.tgz", @@ -8830,6 +8889,11 @@ "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", "optional": true }, + "nanoid": { + "version": "3.1.23", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", + "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -9776,6 +9840,11 @@ "protocols": "^1.4.0" } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" + }, "parse-url": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-5.0.2.tgz", @@ -10073,6 +10142,23 @@ "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" }, + "postcss": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.0.tgz", + "integrity": "sha512-+ogXpdAjWGa+fdYY5BQ96V/6tAo+TdSSIMP5huJBIygdWwKtVoB5JWZ7yUd4xZ8r+8Kvvx4nyg/PQ071H4UtcQ==", + "requires": { + "colorette": "^1.2.2", + "nanoid": "^3.1.23", + "source-map-js": "^0.6.2" + }, + "dependencies": { + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + } + } + }, "postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -10863,6 +10949,32 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sanitize-html": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.4.0.tgz", + "integrity": "sha512-Y1OgkUiTPMqwZNRLPERSEi39iOebn2XJLbeiGOBhaJD/yLqtLGu6GE5w7evx177LeGgSE+4p4e107LMiydOf6A==", + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "klona": "^2.0.3", + "parse-srcset": "^1.0.2", + "postcss": "^8.0.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -11245,6 +11357,11 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, + "source-map-js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", + "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==" + }, "source-map-resolve": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", diff --git a/packages/server/package.json b/packages/server/package.json index 19ce60c1c..a0e8ca964 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -60,6 +60,7 @@ "pg-query-stream": "^3.4.2", "prom-client": "^13.1.0", "redis": "^3.1.1", + "sanitize-html": "^2.4.0", "zxcvbn": "^4.4.2" }, "devDependencies": { diff --git a/packages/viewer/package-lock.json b/packages/viewer/package-lock.json index dc3d1d8f5..8b7b22275 100644 --- a/packages/viewer/package-lock.json +++ b/packages/viewer/package-lock.json @@ -2008,12 +2008,6 @@ "@sinonjs/commons": "^1.7.0" } }, - "@speckle/objectloader": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@speckle/objectloader/-/objectloader-2.0.3.tgz", - "integrity": "sha512-hSyJU0ktZOYbgjDtwrHHXowRu5L0lxoO32N2JPJUjURoy+M1ZpvJVGyT4jKG03HvH30j9rynja0PVjVoW9LdEw==", - "dev": true - }, "@types/anymatch": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-1.3.1.tgz", diff --git a/packages/viewer/src/modules/Converter.js b/packages/viewer/src/modules/Converter.js index 9880fe69e..0f9ac268a 100644 --- a/packages/viewer/src/modules/Converter.js +++ b/packages/viewer/src/modules/Converter.js @@ -91,6 +91,7 @@ export default class Coverter { // Last attempt: iterate through all object keys and see if we can display anything! // traverses the object in case there's any sub-objects we can convert. for ( let prop in target ) { + if ( prop === 'bbox' ) continue if ( typeof target[prop] !== 'object' ) continue let childPromise = this.traverseAndConvert( target[prop], callback, scale ) childrenConversionPromisses.push( childPromise ) @@ -363,6 +364,20 @@ export default class Coverter { return new ObjectWrapper( geometry, obj, 'line' ) } + async BoxToBufferGeometry( object, scale = true ){ + let conversionFactor = scale ? getConversionFactor( object.units ) : 1 + + var move = this.PointToVector3( object.basePlane.origin ) + var width = ( object.xSize.end - object.xSize.start ) * conversionFactor + var depth = ( object.ySize.end - object.ySize.start ) * conversionFactor + var height = ( object.zSize.end - object.zSize.start ) * conversionFactor + + var box = new THREE.BoxBufferGeometry( width,height,depth,1,1,1 ) + box.applyMatrix4( new THREE.Matrix4().setPosition( move ) ) + + return new ObjectWrapper( box, object ) + } + async PolycurveToBufferGeometry( object, scale = true ) { let obj = {} Object.assign( obj, object )