Merged with main

This commit is contained in:
AlexandruPopovici
2022-05-24 12:12:30 +03:00
167 changed files with 31110 additions and 141249 deletions
+2 -1
View File
@@ -17,7 +17,7 @@ class Version:
return 1
if self.pre_release_tag == 'beta':
return 2
return 0
return 10
@staticmethod
@@ -61,6 +61,7 @@ class Version:
if self.pre_release_priority > other.pre_release_priority:
return True
if self.pre_release_priority < other.pre_release_priority:
print('foo')
return False
return True
+72 -19
View File
@@ -1,8 +1,5 @@
version: 2.1
orbs:
node: circleci/node@5.0.1
workflows:
version: 2
@@ -20,6 +17,7 @@ workflows:
branches:
only:
- main
- docker-build-and-publish-server:
filters: *filters-build
context: &docker-hub-context
@@ -62,6 +60,12 @@ workflows:
- get-version
- test-server
- docker-build-and-publish-monitor-container:
context: *docker-hub-context
filters: *filters-build
requires:
- get-version
- publish-helm-chart:
filters: *filters-build
requires:
@@ -72,6 +76,7 @@ workflows:
- docker-build-and-publish-webhooks
- docker-build-and-publish-file-imports
- docker-build-and-publish-previews
- docker-build-and-publish-monitor-container
- docker-build-and-publish-test-container
- publish-npm:
@@ -108,7 +113,7 @@ jobs:
test-server:
docker:
- image: cimg/node:lts
- image: cimg/node:16.15
- image: cimg/redis:6.2.6
- image: 'cimg/postgres:12.8'
environment:
@@ -125,8 +130,22 @@ jobs:
CANONICAL_URL: 'http://localhost:3000'
steps:
- checkout
- node/install-packages:
app-dir: ~/project/packages/server
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-server-{{ checksum "yarn.lock" }}
- run:
name: Install Dependencies
command: yarn workspaces focus
working_directory: 'packages/server'
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-server-{{ checksum "yarn.lock" }}
paths:
- .yarn/cache
- .yarn/unplugged
- run: 'dockerize -wait tcp://localhost:5432 -timeout 1m'
- run:
@@ -134,7 +153,7 @@ jobs:
working_directory: 'packages/server'
- run:
command: npm run test:report
command: yarn test:report
working_directory: 'packages/server'
- run:
@@ -150,7 +169,7 @@ jobs:
docker-build-and-publish: &docker-job
docker: &docker-image
- image: cimg/python:3.9-node
- image: cimg/node:16.15
working_directory: *work-dir
steps:
- checkout
@@ -158,6 +177,9 @@ jobs:
at: /tmp/ci/workspace
- run: cat workspace/env-vars >> $BASH_ENV
- setup_remote_docker:
# a weird issue with yarn installing packages throwing EPERM errors
# this fixes it
version: 20.10.12
docker_layer_caching: true
- run:
name: Build and Publish
@@ -194,6 +216,12 @@ jobs:
FOLDER: utils
SPECKLE_SERVER_PACKAGE: test-deployment
docker-build-and-publish-monitor-container:
<<: *docker-job
environment:
FOLDER: utils
SPECKLE_SERVER_PACKAGE: monitor-deployment
publish-npm:
docker: *docker-image
working_directory: *work-dir
@@ -202,25 +230,50 @@ jobs:
- attach_workspace:
at: /tmp/ci/workspace
- run: cat workspace/env-vars >> $BASH_ENV
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-packages-{{ checksum "yarn.lock" }}
- run:
name: restore packages
command: |
npm i
npx lerna bootstrap
# this has to be after lerna bootstrap
# otherwise the auth workflow of lerna is borked...
name: Install Dependencies
command: yarn
- save_cache:
name: Save Yarn Package Cache
key: yarn-packages-{{ checksum "yarn.lock" }}
paths:
- .yarn/cache
- .yarn/unplugged
- run:
name: auth to npm as Speckle
command: |
echo "@speckle:registry=https://registry.npmjs.org" >> .npmrc
echo "registry=http://registry.npmjs.org/" >> .npmrc
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >> .npmrc
echo "npmRegistryServer: https://registry.npmjs.org/" >> .yarnrc.yml
echo "npmAuthToken: ${NPM_TOKEN}" >> .yarnrc.yml
- run:
name: try login to npm
command: npm whoami
command: yarn npm whoami
- run:
name: build public packages
command: yarn workspaces foreach -ptv --no-private run build
- run:
name: bump all versions
# bump all versions in dependency tree order but not in parallel
command: yarn workspaces foreach -tv version $IMAGE_VERSION_TAG
- run:
name: publish to npm
command: npx lerna publish $IMAGE_VERSION_TAG -y --no-git-tag-version --no-push
command: 'yarn workspaces foreach -pv --no-private npm publish --access public --tag next'
# - run:
# name: commit changes
# command: |
# yarn prettier:fix
# git add .
# git commit -m '[ci skip] bump version to $IMAGE_VERSION_TAG'
# git push
publish-helm-chart:
docker: *docker-image
+2 -2
View File
@@ -27,8 +27,8 @@ fi
rm -rf ~/helm/charts/speckle-server
cp -r utils/helm/speckle-server ~/helm/charts/speckle-server
echo 'version: '$RELEASE_VERSION >> ~/helm/charts/speckle-server/Chart.yaml
echo 'appVersion: "'$RELEASE_VERSION'"' >> ~/helm/charts/speckle-server/Chart.yaml
sed -i 's/version: [^\s]*/version: '$RELEASE_VERSION'/g' ~/helm/charts/speckle-server/Chart.yaml
sed -i 's/appVersion: [^\s]*/appVersion: '\"$RELEASE_VERSION\"'/g' ~/helm/charts/speckle-server/Chart.yaml
sed -i 's/docker_image_tag: [^\s]*/docker_image_tag: '$RELEASE_VERSION'/g' ~/helm/charts/speckle-server/values.yaml
+9 -1
View File
@@ -6,9 +6,17 @@ test-queries
.editorconfig
Contributing.md
ISSUE_TEMPLATE.md
lerna.json
**/.env
.env.example
.eslintrc.json
.mocharc.js
readme.md
**/Dockerfile
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
+9 -1
View File
@@ -31,4 +31,12 @@ events.json
# Optional eslint cache
.eslintcache
lerna-debug.log
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
*yarn-error.log
+1 -1
View File
@@ -3,4 +3,4 @@
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
yarn lint-staged
+4 -2
View File
@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/mirrors-eslint
rev: 'v8.11.0' # Use the sha / tag you want to point at
rev: 'v8.15.0' # Use the sha / tag you want to point at
hooks:
- id: eslint
types: [file]
@@ -17,6 +17,8 @@ repos:
- typescript@4.5.4
- repo: https://github.com/pre-commit/mirrors-prettier
rev: 'v2.5.1' # Use the sha / tag you want to point at
rev: 'v2.6.2' # Use the sha / tag you want to point at
hooks:
- id: prettier
ci:
autoupdate_schedule: quarterly
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+785
View File
File diff suppressed because one or more lines are too long
+8
View File
@@ -0,0 +1,8 @@
undecided:
- '@speckle/fileimport-service'
- '@speckle/frontend'
- '@speckle/objectloader'
- '@speckle/preview-service'
- '@speckle/server'
- '@speckle/viewer'
- '@speckle/viewer-sandbox'
+9
View File
@@ -0,0 +1,9 @@
nodeLinker: node-modules
plugins:
- path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
spec: '@yarnpkg/plugin-workspace-tools'
- path: .yarn/plugins/@yarnpkg/plugin-version.cjs
spec: '@yarnpkg/plugin-version'
yarnPath: .yarn/releases/yarn-3.2.0.cjs
-9
View File
@@ -1,9 +0,0 @@
{
"packages": ["packages/*"],
"version": "independent",
"command": {
"version": {
"message": "chore(release): publish to npm\n[skip ci]"
}
}
}
-20661
View File
File diff suppressed because it is too large Load Diff
+18 -5
View File
@@ -1,33 +1,46 @@
{
"packageManager": "yarn@3.2.0",
"workspaces": [
"packages/*"
],
"name": "root",
"private": true,
"engines": {
"node": ">=14.0.0 <17.0.0"
},
"scripts": {
"build": "yarn workspaces foreach -ptv run build",
"build:public": "yarn workspaces foreach -ptv --no-private run build",
"lint": "eslint . --ext .js,.ts,.vue --max-warnings=0",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"docker:deps:up": "docker-compose -f ./docker-compose-deps.yml up -d",
"docker:deps:down": "docker-compose -f ./docker-compose-deps.yml down",
"dev": "lerna run dev --parallel",
"dev:no-server": "npm run dev -- --ignore @speckle/server",
"dev": "yarn workspaces foreach -piv -j unlimited run dev",
"dev:no-server": "yarn workspaces foreach --exclude @speckle/server -piv -j unlimited run dev",
"dev:minimal": "yarn workspaces foreach -piv -j unlimited --include '{@speckle/server,@speckle/frontend}' run dev",
"prepare": "husky install",
"postinstall": "husky install"
"postinstall": "husky install",
"cm": "cz"
},
"devDependencies": {
"@types/eslint": "^8.4.1",
"commitizen": "^4.2.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.11.0",
"eslint-config-prettier": "^8.5.0",
"husky": "^7.0.4",
"lerna": "^3.22.1",
"lint-staged": "^12.3.7",
"prettier": "^2.5.1"
},
"resolutions": {
"tslib": "^2.3.1",
"core-js": "3.22.4",
"vue-cli-plugin-apollo/graphql": "^15"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
"path": "cz-conventional-changelog"
}
},
"lint-staged": {
+16
View File
@@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"request": "launch",
"runtimeArgs": ["run-script", "dev"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "node"
}
]
}
+9 -4
View File
@@ -12,14 +12,19 @@ RUN chmod +x /wait
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /app
WORKDIR /speckle-server
COPY packages/fileimport-service/package*.json ./
RUN npm ci
COPY .yarnrc.yml .
COPY .yarn ./.yarn
COPY package.json yarn.lock ./
WORKDIR /speckle-server/packages/fileimport-service
COPY packages/fileimport-service/package.json ./
RUN yarn workspaces focus --production
COPY packages/fileimport-service/requirements.txt ./
RUN pip install -r requirements.txt
COPY packages/fileimport-service .
CMD ["node", "src/daemon.js"]
CMD ["yarn", "node", "src/daemon.js"]
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@speckle/fileimport-service",
"private": true,
"version": "2.3.17-alpha.2889",
"version": "2.5.4",
"description": "Parse and import files of various types into a stream",
"author": "Dimitrie Stefanescu <didimitrie@gmail.com>",
"homepage": "https://github.com/specklesystems/speckle-server#readme",
@@ -28,6 +28,7 @@
"knex": "^1.0.3",
"node-fetch": "^2.6.5",
"pg": "^8.7.1",
"prom-client": "^14.0.1",
"valid-filename": "^3.1.0",
"web-ifc": "^0.0.33"
},
+18 -2
View File
@@ -1,6 +1,12 @@
/* eslint-disable no-console */
'use strict'
const {
initPrometheusMetrics,
metricDuration,
metricInputFileSize,
metricOperationErrors
} = require('./prometheusMetrics')
const knex = require('../knex')
const { getFileStream } = require('./filesApi')
@@ -39,13 +45,16 @@ async function startTask() {
async function doTask(task) {
let tempUserToken = null
let serverApi = null
let fileTypeForMetric = 'unknown'
let fileSizeForMetric = 0
const metricDurationEnd = metricDuration.startTimer()
try {
console.log('Doing task ', task)
const { rows } = await knex.raw(
`
SELECT
id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType"
id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType", "fileSize"
FROM file_uploads
WHERE id = ?
LIMIT 1
@@ -56,6 +65,8 @@ async function doTask(task) {
if (!info) {
throw new Error('Internal error: DB inconsistent')
}
fileTypeForMetric = info.fileType || 'missing_info'
fileSizeForMetric = Number(info.fileSize) || 0
fs.mkdirSync(TMP_INPUT_DIR, { recursive: true })
@@ -89,7 +100,7 @@ async function doTask(task) {
{
USER_TOKEN: tempUserToken
},
10 * 60 * 1000
20 * 60 * 1000
)
} else if (info.fileType === 'stl') {
await runProcessWithTimeout(
@@ -165,7 +176,10 @@ async function doTask(task) {
`,
[err.toString(), task.id]
)
metricOperationErrors.labels(fileTypeForMetric).inc()
}
metricDurationEnd({ op: fileTypeForMetric })
metricInputFileSize.labels(fileTypeForMetric).observe(fileSizeForMetric)
fs.rmSync(TMP_INPUT_DIR, { force: true, recursive: true })
if (fs.existsSync(TMP_RESULTS_PATH)) fs.unlinkSync(TMP_RESULTS_PATH)
@@ -234,6 +248,7 @@ async function tick() {
// Check for another task very soon
setTimeout(tick, 10)
} catch (err) {
metricOperationErrors.labels('main_loop').inc()
console.log('Error executing task: ', err)
setTimeout(tick, 5000)
}
@@ -241,6 +256,7 @@ async function tick() {
async function main() {
console.log('Starting FileUploads Service...')
initPrometheusMetrics()
process.on('SIGTERM', () => {
shouldExit = true
@@ -0,0 +1,127 @@
/* eslint-disable no-unused-vars */
'use strict'
const http = require('http')
const prometheusClient = require('prom-client')
const knex = require('../knex')
let metricFree = null
let metricUsed = null
let metricPendingAquires = null
let metricQueryDuration = null
let metricQueryErrors = null
const queryStartTime = {}
prometheusClient.register.clear()
prometheusClient.register.setDefaultLabels({
project: 'speckle-server',
app: 'fileimport-service'
})
prometheusClient.collectDefaultMetrics()
let prometheusInitialized = false
function initKnexPrometheusMetrics() {
metricFree = new prometheusClient.Gauge({
name: 'speckle_server_knex_free',
help: 'Number of free DB connections',
collect() {
this.set(knex.client.pool.numFree())
}
})
metricUsed = new prometheusClient.Gauge({
name: 'speckle_server_knex_used',
help: 'Number of used DB connections',
collect() {
this.set(knex.client.pool.numUsed())
}
})
metricPendingAquires = new prometheusClient.Gauge({
name: 'speckle_server_knex_pending',
help: 'Number of pending DB connection aquires',
collect() {
this.set(knex.client.pool.numPendingAcquires())
}
})
metricQueryDuration = new prometheusClient.Summary({
name: 'speckle_server_knex_query_duration',
help: 'Summary of the DB query durations in seconds'
})
metricQueryErrors = new prometheusClient.Counter({
name: 'speckle_server_knex_query_errors',
help: 'Number of DB queries with errors'
})
knex.on('query', (data) => {
const queryId = data.__knexQueryUid + ''
queryStartTime[queryId] = Date.now()
})
knex.on('query-response', (data, obj, builder) => {
const queryId = obj.__knexQueryUid + ''
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
delete queryStartTime[queryId]
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
})
knex.on('query-error', (err, querySpec) => {
const queryId = querySpec.__knexQueryUid + ''
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
delete queryStartTime[queryId]
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
metricQueryErrors.inc()
})
}
module.exports = {
initPrometheusMetrics() {
if (prometheusInitialized) return
prometheusInitialized = true
initKnexPrometheusMetrics()
// Define the HTTP server
const server = http.createServer(async (req, res) => {
if (req.url === '/metrics') {
res.setHeader('Content-Type', prometheusClient.register.contentType)
res.end(await prometheusClient.register.metrics())
} else {
res.end('Speckle FileImport Service - prometheus metrics')
}
})
server.listen(Number(process.env.PROMETHEUS_METRICS_PORT) || 9093)
},
metricDuration: new prometheusClient.Histogram({
name: 'speckle_server_operation_duration',
help: 'Summary of the operation durations in seconds',
buckets: [0.5, 1, 5, 10, 30, 60, 300, 600, 900, 1200],
labelNames: ['op']
}),
metricOperationErrors: new prometheusClient.Counter({
name: 'speckle_server_operation_errors',
help: 'Number of operations with errors',
labelNames: ['op']
}),
metricInputFileSize: new prometheusClient.Histogram({
name: 'speckle_server_operation_file_size',
help: 'Size of the operation input file size',
buckets: [
1000,
100 * 1000,
500 * 1000,
1000 * 1000,
5 * 1000 * 1000,
10 * 1000 * 1000,
100 * 1000 * 1000
],
labelNames: ['op']
})
}
+19 -29
View File
@@ -1,41 +1,31 @@
# NOTE: Docker context should be set to git root directory, to include the viewer
# build stage
FROM node:16.13-bullseye-slim as deps-build
WORKDIR /opt/objectloader
COPY packages/objectloader /opt/objectloader
RUN npm install
RUN npm run build
# this whole thing is required cause npm 6.* doesn't allow for pack output dir specification
RUN mkdir /packages
# invoke npm pack and move its result to the packages folder when done
RUN npm pack --pack-destination=/packages/
WORKDIR /opt/viewer
COPY packages/viewer/package*.json ./
# this installs objectloader from a tarball
RUN npm i $(find /packages -type f -name "speckle*.tgz")
RUN npm install
COPY packages/viewer .
RUN npm run build
RUN npm pack --pack-destination=/packages/
FROM node:16.13-bullseye-slim as build-stage
WORKDIR /opt/frontend
COPY --from=deps-build /packages /packages
COPY packages/frontend/package*.json ./
# RUN npm install ../viewer
RUN npm i $(find /packages -type f -name "speckle*.tgz")
RUN npm ci
COPY packages/frontend .
RUN npm run build
WORKDIR /speckle-server
COPY .yarnrc.yml .
COPY .yarn ./.yarn
COPY package.json yarn.lock ./
# Onyl copy in the relevant package.json files for the dependencies
COPY packages/frontend/package.json ./packages/frontend/
COPY packages/viewer/package.json ./packages/viewer/
COPY packages/objectloader/package.json ./packages/objectloader/
RUN yarn workspaces focus -A
# Onyl copy in the relevant source files for the dependencies
COPY packages/objectloader ./packages/objectloader/
COPY packages/viewer ./packages/viewer/
COPY packages/frontend ./packages/frontend/
# This way the foreach only builds the frontend and its deps
RUN yarn workspaces foreach -pt run build
# production stage
FROM openresty/openresty:1.19.9.1-bullseye as production-stage
COPY --from=build-stage /opt/frontend/dist /usr/share/nginx/html
COPY --from=build-stage /speckle-server/packages/frontend/dist /usr/share/nginx/html
RUN rm /etc/nginx/conf.d/default.conf
COPY packages/frontend/nginx/nginx.conf /etc/nginx/conf.d
EXPOSE 80
+3 -9
View File
@@ -10,7 +10,7 @@ Note that this package contains two vue apps, the main frontend (located under @
Notes:
- In **development** mode, the Speckle Server will proxy the frontend from `localhost:3000` to `localhost:8080`. If you don't see anything, ensure you've run `npm run serve` in the frontend package.
- In **development** mode, the Speckle Server will proxy the frontend from `localhost:3000` to `localhost:8080`. If you don't see anything, ensure you've run `yarn serve` in the frontend package.
- In **production** mode, the Speckle Frontend will be statically served by nginx (see the Dockerfile in the current directory).
@@ -22,18 +22,12 @@ Comprehensive developer and user documentation can be found in our:
## Project setup
The frontend now includes the viewer. Until we get to publish it as a separate module, there's a few extra steps:
- make sure you build the [Speckle Viewer](../viewer)
- afterwards, run
```
lerna bootstrap
```
Make sure you follow the Developing and Debugging section in the project root readme.
### Compiles and hot-reloads for development
```
npm run serve
yarn serve
```
### Packaging for production
+1 -1
View File
@@ -18,7 +18,7 @@ function plugin(api) {
// Add caching config
jsRule
.use('cache-loader')
.loader(require.resolve('cache-loader'))
.loader('cache-loader')
.options(
api.genCacheConfig('js-esbuild-loader', {
target,
-42615
View File
File diff suppressed because it is too large Load Diff
+6 -4
View File
@@ -1,14 +1,14 @@
{
"name": "@speckle/frontend",
"version": "2.3.17-alpha.2889",
"version": "2.5.4",
"private": true,
"scripts": {
"dev": "vue-cli-service serve --mode development",
"serve": "ws -p 8080 -d dist -r '/embed(.*) -> /embedApp.html' '/([a-zA-Z0-9-_/]*)(\\?.*)? -> /app.html' ",
"build": "vue-cli-service build --mode production --silent",
"build:dev": "vue-cli-service build --mode development --silent",
"build:profile": "npm run build -- --profile",
"build:dev:profile": "npm run build:dev -- --profile",
"build:profile": "yarn build -- --profile",
"build:dev:profile": "yarn build:dev -- --profile",
"inspect": "vue-cli-service inspect --mode production",
"inspect:dev": "vue-cli-service inspect --mode development",
"lint": "eslint . --ext .js,.ts,.vue"
@@ -17,13 +17,14 @@
"node": "^16.0.0"
},
"dependencies": {
"@speckle/viewer": "^2.4.2",
"@speckle/viewer": "workspace:^",
"@tryghost/content-api": "^1.5.12",
"@vuejs-community/vue-filter-date-format": "^1.6.3",
"@vuejs-community/vue-filter-date-parse": "^1.1.6",
"apexcharts": "^3.33.1",
"crypto-random-string": "^3.3.0",
"dompurify": "^2.3.6",
"linkify-urls": "^4.0.0",
"lodash": "^4.17.21",
"numeral": "^2.0.6",
"portal-vue": "^2.1.7",
@@ -70,6 +71,7 @@
"vue-cli-plugin-vuetify": "^2.0.8",
"vue-template-compiler": "^2.6.12",
"vuetify-loader": "^1.6.0",
"webpack": "^4.46.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"vuePlugins": {
+5 -1
View File
@@ -38,7 +38,11 @@ import 'vue2-perfect-scrollbar/dist/vue2-perfect-scrollbar.css'
Vue.use(PerfectScrollbar)
import VTooltip from 'v-tooltip'
Vue.use(VTooltip, { defaultDelay: 300, defaultBoundariesElement: document.body })
Vue.use(VTooltip, {
defaultDelay: 300,
defaultBoundariesElement: document.body,
defaultHtml: false
})
import VueMixpanel from 'vue-mixpanel'
Vue.use(VueMixpanel, {
@@ -1,6 +1,6 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<v-timeline-item medium>
<v-timeline-item v-show="!activityGroup[0].actionType.includes('comment_')" medium>
<template #icon>
<user-avatar v-if="user" :id="user.id" :avatar="user.avatar" :name="user.name" />
</template>
@@ -0,0 +1,189 @@
<template>
<!-- eslint-disable vue/no-v-html -->
<div
class="d-flex align-center"
@mouseenter="hover = true"
@mouseleave="hover = false"
>
<div
v-if="!link"
:class="`flex-grow-1 d-flex px-2 py-1 mb-2 align-center rounded-xl elevation-2 ${
$userId() === reply.authorId ? 'primary white--text' : 'background'
}`"
style="width: 290px"
>
<div
:class="`d-inline-block ${
$userId() === reply.authorId ? 'xxx-order-last' : ''
}`"
>
<user-avatar :id="reply.authorId" :size="30" />
</div>
<div
:class="`reply-box d-inline-block mx-2 py-2 flex-grow-1 float-left caption`"
v-html="linkifiedText"
></div>
</div>
<div
v-else
style="width: 300px"
:class="`flex-grow-1 d-flex px-2 py-1 mb-2 align-center`"
>
<div
:class="`d-inline-block ${
$userId() === reply.authorId ? 'xxx-order-last' : ''
}`"
>
<user-avatar :id="reply.authorId" :size="30" />
</div>
<div
:class="`reply-box d-inline-block py-2 flex-grow-1 float-left caption ${
$userId() === reply.authorId ? 'pr-3' : 'pl-1'
}`"
>
<div class="d-block">
<v-btn
v-tooltip="reply.text"
block
rounded
:href="reply.text"
target="_blank"
:class="`reply-box overflow-hidden ${
$userId() === reply.authorId ? 'primary white--text' : 'background'
}`"
>
<span class="caption">
{{ link.host.substring(0, 18) }} {{ link.host.length > 20 ? '...' : '' }}
</span>
<v-icon small class="ml-2">mdi-open-in-new</v-icon>
</v-btn>
</div>
</div>
</div>
<div style="width: 20px; overflow: hidden">
<v-scroll-x-transition>
<v-btn
v-show="hover && canArchive"
v-tooltip="'Archive'"
x-small
icon
class="ml-1"
@click="showArchiveDialog = true"
>
<v-icon small>mdi-delete</v-icon>
</v-btn>
</v-scroll-x-transition>
</div>
<v-dialog v-model="showArchiveDialog" max-width="500">
<v-card>
<v-toolbar color="error" dark flat>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-pencil</v-icon>
</v-app-bar-nav-icon>
<v-toolbar-title>
Archive Comment {{ index === 0 ? 'Thread' : '' }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="showArchiveDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="mt-4">
This comment {{ index === 0 ? 'thread, including all replies, ' : '' }} will
be archived. Are you sure?
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn text @click="showArchiveDialog = false">Cancel</v-btn>
<v-btn color="error" text @click="archiveComment()">Archive</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</div>
</template>
<script>
import gql from 'graphql-tag'
import linkifyUrls from 'linkify-urls'
export default {
components: {
UserAvatar: () => import('@/main/components/common/UserAvatar')
},
props: {
reply: { type: Object, default: () => null },
stream: { type: Object, default: () => null },
index: { type: Number, default: 0 }
},
data() {
return {
hover: false,
showArchiveDialog: false
}
},
computed: {
canArchive() {
if (!this.reply || !this.stream) return false
if (this.stream.role === 'stream:owner' || this.reply.authorId === this.$userId())
return true
return false
},
link() {
if (!this.reply) return false
try {
const url = new URL(this.reply.text)
return url
} catch {
return null
}
},
linkifiedText() {
return linkifyUrls(this.reply.text, {
attributes: {
target: '_blank',
class:
this.reply.authorId === this.$userId()
? 'comment-link white--text font-weight-bold text-decoration-none'
: 'comment-link font-weight-bold text-decoration-none'
}
})
}
},
methods: {
async archiveComment() {
try {
await this.$apollo.mutate({
mutation: gql`
mutation commentArchive($streamId: String!, $commentId: String!) {
commentArchive(streamId: $streamId, commentId: $commentId)
}
`,
variables: {
streamId: this.$route.params.streamId,
commentId: this.reply.id
}
})
this.$emit('deleted', this.reply.id)
this.$mixpanel.track('Comment Action', { type: 'action', name: 'archive' })
this.$eventHub.$emit('notification', {
text: this.index === 0 ? 'Thread archived.' : 'Comment archived.'
})
} catch (e) {
this.$eventHub.$emit('notification', {
text: e.message
})
}
this.showArchiveDialog = false
}
}
}
</script>
<style scoped>
>>> .comment-link:after {
content: ' ↗ ';
}
.reply-box {
white-space: pre-wrap;
word-break: break-word;
}
</style>
@@ -1,9 +1,13 @@
<template>
<div
class="no-mouse pa-2"
class="no-mouse py-2 pl-2"
:style="`${
$vuetify.breakpoint.xs ? 'width: 90vw;' : 'width: 300px;'
} xxx-background: rgba(0.5, 0.5, 0.5, 0.5)`"
$vuetify.breakpoint.xs
? 'width: 90vw; padding-right:30px;'
: 'padding-right:30px; width: 330px;'
} ${hovered ? 'opacity: 1;' : 'opacity: 1;'} transition: opacity 0.2s ease;`"
@mouseenter="hovered = true"
@mouseleave="hovered = false"
>
<div v-if="$vuetify.breakpoint.xs" class="text-right mb-5 mouse">
<v-btn
@@ -50,22 +54,13 @@
<timeago :datetime="reply.createdAt" class="font-italic ma-1"></timeago>
</div>
</div>
<div
:key="index"
:class="`d-flex px-2 py-1 mb-2 align-center rounded-xl elevation-2 ${
$userId() === reply.authorId ? 'primary white--text' : 'background'
}`"
>
<div :class="`${$userId() === reply.authorId ? 'order-last' : ''}`">
<user-avatar :id="reply.authorId" :size="30" />
</div>
<div
:class="`mx-2 px-4 py-2 flex-grow-1 float-left caption`"
style="overflow-wrap: break-word"
>
{{ reply.text }}
</div>
</div>
<comment-thread-reply
:key="index + 'reply'"
:reply="reply"
:stream="stream"
:index="index"
@deleted="handleReplyDeleteEvent"
/>
</template>
<div v-if="$loggedIn()" class="px-0 mb-4">
<v-slide-y-transition>
@@ -167,7 +162,7 @@ import debounce from 'lodash/debounce'
export default {
components: {
UserAvatar: () => import('@/main/components/common/UserAvatar')
CommentThreadReply: () => import('@/main/components/comments/CommentThreadReply')
},
props: {
comment: { type: Object, default: () => null }
@@ -192,6 +187,7 @@ export default {
stream(id: $streamId) {
id
role
allowPublicComments
}
}
`,
@@ -292,6 +288,7 @@ export default {
},
data() {
return {
hovered: true,
replyText: null,
localReplies: [],
minimize: false,
@@ -303,7 +300,7 @@ export default {
},
computed: {
canReply() {
return !!this.stream?.role
return !!this.stream?.role || this.stream?.allowPublicComments
},
canArchiveThread() {
if (!this.comment || !this.stream) return false
@@ -357,7 +354,7 @@ export default {
deep: true,
async handler(newVal) {
if (!this.$loggedIn() || !this.canReply) return
this.hovered = true
await this.$apollo.mutate({
mutation: gql`
mutation commentView($streamId: String!, $commentId: String!) {
@@ -503,6 +500,14 @@ export default {
this.$emit('refresh-layout') // needed for layout reshuffle in parent
}, 100)
},
handleReplyDeleteEvent(id) {
if (this.comment.id === id) {
this.$emit('deleted', this.comment)
return
}
const idx = this.localReplies.findIndex((r) => r.id === id)
this.localReplies.splice(idx, 1)
},
async archiveComment() {
try {
await this.$apollo.mutate({
@@ -26,8 +26,41 @@
</v-list>
<v-scroll-y-transition>
<div v-show="expand" class="px-2">
<div class="d-flex align-center px-2 mb-3">
<span class="caption mr-1">Filter</span>
<v-btn
x-small
class="ml-2"
:depressed="filter === 'all'"
:zzzcolor="`${filter === 'all' ? 'primary' : ''}`"
@click="$emit('set-filter', 'all')"
>
<v-icon x-small class="mr-2">mdi-comment-outline</v-icon>
all
</v-btn>
<v-btn
x-small
class="ml-2"
:depressed="filter === 'unread'"
:zzzcolor="`${filter === 'unread' ? 'primary' : ''}`"
@click="$emit('set-filter', 'unread')"
>
<v-icon x-small class="mr-2">mdi-comment-alert-outline</v-icon>
unread
</v-btn>
<v-btn
x-small
class="ml-2"
:depressed="filter === 'none'"
:zzzcolor="`${filter === 'none' ? 'primary' : ''}`"
@click="$emit('set-filter', 'none')"
>
<v-icon x-small class="mr-2">mdi-comment-off-outline</v-icon>
none
</v-btn>
</div>
<v-row
v-for="comment in comments"
v-for="comment in visibleComments"
:key="comment.id + '-card-sidebar'"
no-gutters
:class="`${isUnread(comment) ? 'border' : ''} my-2 property-row rounded-lg ${
@@ -96,6 +129,10 @@ export default {
comments: {
type: Array,
default: () => []
},
filter: {
type: String,
default: 'all'
}
},
data() {
@@ -103,6 +140,19 @@ export default {
expand: true
}
},
computed: {
visibleComments() {
switch (this.filter) {
case 'all':
return this.comments
case 'unread':
return this.comments.filter((c) => this.isUnread(c))
case 'none':
return this.comments // important, hides in the display, but you can still see all comments
}
return this.comments
}
},
methods: {
isUnread(comment) {
return new Date(comment.updatedAt) - new Date(comment.viewedAt) > 0
@@ -8,7 +8,6 @@
<router-link :to="`/streams/${stream.id}`">
<preview-image
:url="`/preview/${stream.id}`"
:color="hover"
:height="previewHeight"
></preview-image>
<stream-favorite-btn :user="user" :stream="stream" class="favorite-button" />
@@ -168,8 +168,12 @@ export default {
}
const test = prev[prev.length - 1][0]
let action = 'split' // split | combine | skip
if (curr.actionType === test.actionType && curr.streamId === test.streamId) {
if (curr.actionType.includes('stream_permissions')) {
if (
curr.actionType.includes('stream_permissions') ||
curr.actionType.includes('comment_')
) {
//skip multiple stream_permission actions on the same user, just pick the last!
if (
prev[prev.length - 1].some(
@@ -22,7 +22,7 @@
class="absolute-pos"
>
<div
class="d-flex align-center"
class="d-flex"
:style="`height: 48px; width: ${$vuetify.breakpoint.xs ? '90vw' : '320px'}`"
>
<v-btn
@@ -31,42 +31,65 @@
icon
:dark="!expand"
:class="`mouse elevation-5 ${!expand ? 'primary' : 'background'} mr-2`"
:loading="loading"
@click="toggleExpand()"
>
<v-icon v-if="!expand" dark>mdi-plus</v-icon>
<v-icon v-if="!expand" dark small>mdi-message</v-icon>
<v-icon v-else dark x-small>mdi-close</v-icon>
</v-btn>
<v-slide-x-transition>
<div
v-if="expand && !$vuetify.breakpoint.xs"
style="width: 100%"
class="d-flex"
style="width: 100%; top: -10px; position: relative"
class=""
>
<v-textarea
v-if="$loggedIn() && canComment"
v-model="commentText"
solo
hide-details
autofocus
auto-grow
rows="1"
placeholder="Your comment..."
class="mouse rounded-xl caption elevation-15"
append-icon="mdi-send"
@keydown.enter.exact.prevent="addComment()"
></v-textarea>
<v-btn
v-if="$loggedIn() && canComment"
v-tooltip="'Send comment (press enter)'"
icon
dark
large
class="mouse elevation-0 primary pa-0 ma-o"
style="left: -47px; top: 1px; height: 48px; width: 48px"
@click="addComment()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
<div class="d-flex">
<v-textarea
v-if="$loggedIn() && canComment"
v-model="commentText"
:disabled="loading"
solo
hide-details
autofocus
auto-grow
rows="1"
placeholder="Your comment..."
class="mouse rounded-xl caption elevation-15"
append-icon="mdi-send"
@keydown.enter.exact.prevent="addComment()"
></v-textarea>
<v-btn
v-if="$loggedIn() && canComment"
v-tooltip="'Send comment (press enter)'"
:disabled="loading"
icon
dark
large
class="mouse elevation-0 primary pa-0 ma-o"
style="left: -47px; top: 1px; height: 48px; width: 48px"
@click="addComment()"
>
<v-icon dark small>mdi-send</v-icon>
</v-btn>
</div>
<div v-if="$loggedIn() && canComment" class="d-flex mt-2 mouse">
<template v-for="reaction in $store.state.commentReactions">
<v-btn
:key="reaction"
class="mr-2"
fab
small
@click="addCommentDirect(reaction)"
>
<span
class="text-h5"
style="position: relative; top: 1px; left: -1px"
>
{{ reaction }}
</span>
</v-btn>
</template>
</div>
<div
v-if="!canComment && $loggedIn()"
class="caption background px-4 py-2 rounded-xl elevation-2"
@@ -91,7 +114,7 @@
v-if="$vuetify.breakpoint.xs"
v-model="expand"
class="elevation-0 flat"
@input="toggleExpand()"
@click:outside="toggleExpand()"
>
<div
v-if="!canComment && $loggedIn()"
@@ -107,6 +130,7 @@
<v-textarea
v-model="commentText"
solo
:disabled="loading"
hide-details
autofocus
auto-grow
@@ -118,6 +142,7 @@
></v-textarea>
<v-btn
v-tooltip="'Send comment (press enter)'"
:disabled="loading"
icon
dark
large
@@ -139,6 +164,21 @@
<v-icon small class="mr-1">mdi-account</v-icon>
Sign in to comment
</v-btn>
<div class="my-2 d-flex justify-center" style="position: relative">
<template v-for="reaction in $store.state.commentReactions">
<v-btn
:key="reaction"
class="mr-2"
fab
small
@click="addCommentDirect(reaction)"
>
<span class="text-h5" style="position: relative; top: 1px; left: -1px">
{{ reaction }}
</span>
</v-btn>
</template>
</div>
</v-dialog>
</div>
</v-slide-x-transition>
@@ -161,8 +201,7 @@
</div>
</template>
<script>
// TODO: Need to fix the viewer package build process to be able to properly reference THREE.js
/* global THREE */
import * as THREE from 'three'
import gql from 'graphql-tag'
import debounce from 'lodash/debounce'
@@ -188,6 +227,7 @@ export default {
stream(id: $streamId) {
id
role
allowPublicComments
}
}
`,
@@ -201,12 +241,13 @@ export default {
location: null,
expand: false,
visible: true,
loading: false,
commentText: null
}
},
computed: {
canComment() {
return !!this.stream?.role
return !!this.stream?.role || this.stream?.allowPublicComments
}
},
mounted() {
@@ -225,7 +266,12 @@ export default {
)
},
methods: {
async addCommentDirect(emoji) {
this.commentText = emoji
await this.addComment()
},
async addComment() {
if (this.loading) return
if (!this.commentText || this.commentText.length < 1) {
this.$eventHub.$emit('notification', {
text: `Comment cannot be empty.`
@@ -263,6 +309,7 @@ export default {
.map((res) => ({ resourceId: res, resourceType: this.$resourceType(res) }))
)
}
this.loading = true
try {
await this.$apollo.mutate({
mutation: gql`
@@ -277,6 +324,7 @@ export default {
text: e.message
})
}
this.loading = false
this.expand = false
this.visible = false
this.commentText = null
@@ -27,9 +27,10 @@
<!-- Comment bubbles -->
<div
v-for="comment in activeComments"
v-show="isVisible(comment)"
:key="comment.id"
:ref="`comment-${comment.id}`"
:class="`absolute-pos rounded-xl no-mouse`"
:class="`absolute-pos rounded-xl no-mouse `"
:style="`transition: opacity 0.2s ease; z-index:${
comment.expanded ? '20' : '10'
}; ${
@@ -51,6 +52,12 @@
small
icon
:class="`elevation-5 pa-0 ma-0 mouse ${
$store.state.emojis.indexOf(comment.text.split(' ')[0]) != -1 &&
!comment.expanded
? 'emoji-btn transparent elevation-0'
: ''
}
${
comment.expanded || comment.bouncing || isUnread(comment)
? 'dark white--text primary'
: 'background'
@@ -59,7 +66,16 @@
comment.expanded ? collapseComment(comment) : expandComment(comment)
"
>
<v-icon v-if="!comment.expanded" x-small class="">mdi-comment</v-icon>
<template
v-if="$store.state.emojis.indexOf(comment.text.split(' ')[0]) == -1"
>
<v-icon v-if="!comment.expanded" x-small class="">mdi-comment</v-icon>
</template>
<template v-else-if="!comment.expanded">
<span class="text-h5">
{{ comment.text.split(' ')[0] }}
</span>
</template>
<v-icon v-if="comment.expanded" x-small class="">mdi-close</v-icon>
</v-btn>
<v-slide-x-transition>
@@ -88,6 +104,7 @@
<!-- Comment Threads -->
<div
v-for="comment in activeComments"
v-show="isVisible(comment)"
:key="comment.id + '-card'"
:ref="`commentcard-${comment.id}`"
:class="`hover-bg absolute-pos rounded-xl overflow-y-auto ${
@@ -116,32 +133,42 @@
<portal v-if="activeComments.length !== 0" to="comments">
<comments-viewer-navbar
:comments="activeComments"
:filter="commentsFilter"
@select-comment="
(e) => {
if (!e.expanded && !showComments) showComments = true
e.expanded ? collapseComment(e) : expandComment(e)
}
"
@set-filter="
(state) => {
commentsFilter = state
}
"
/>
</portal>
<portal to="viewercontrols" :order="5">
<v-btn
key="comment-toggle-button"
v-tooltip="`Toggle comments (${activeComments.length})`"
v-tooltip="currentCommentVisStatus"
rounded
icon
class="mr-2"
@click="toggleComments()"
>
<v-icon v-if="showComments" small>mdi-comment-outline</v-icon>
<v-icon v-if="!showComments" small>mdi-comment-off-outline</v-icon>
<v-icon v-if="commentsFilter === 'all'" small>mdi-comment-outline</v-icon>
<v-icon v-if="commentsFilter === 'unread'" small class="primary--text">
mdi-comment-alert-outline
</v-icon>
<v-icon v-if="commentsFilter === 'none'" small>mdi-comment-off-outline</v-icon>
<!-- {{ commentsFilter }} -->
<!-- <v-icon v-if="!showComments" small>mdi-comment-off-outline</v-icon> -->
</v-btn>
</portal>
</div>
</template>
<script>
// TODO: Need to fix the viewer package build process to be able to properly reference THREE.js
/* global THREE */
import * as THREE from 'three'
import debounce from 'lodash/debounce'
import gql from 'graphql-tag'
@@ -270,6 +297,7 @@ export default {
return {
localComments: [],
showComments: true,
commentsFilter: 'all', // 'unread', 'none'
openCommentOnInit: null
}
},
@@ -279,6 +307,17 @@ export default {
},
hasExpandedComment() {
return this.localComments.filter((c) => c.expanded).length !== 0
},
currentCommentVisStatus() {
switch (this.commentsFilter) {
case 'all':
return 'Showing all comments'
case 'unread':
return 'Showing unread comments only'
case 'none':
return 'Comments hidden'
}
return ''
}
},
mounted() {
@@ -326,8 +365,31 @@ export default {
isUnread(comment) {
return new Date(comment.updatedAt) - new Date(comment.viewedAt) > 0
},
isVisible(comment) {
if (comment.expanded) return true
switch (this.commentsFilter) {
case 'all':
return true
case 'unread':
return this.isUnread(comment)
case 'none':
return false
}
return true
},
toggleComments() {
this.showComments = !this.showComments
// this.showComments = !this.showComments
switch (this.commentsFilter) {
case 'all':
this.commentsFilter = 'unread'
break
case 'unread':
this.commentsFilter = 'none'
break
case 'none':
this.commentsFilter = 'all'
break
}
},
expandComment(comment) {
for (const c of this.localComments) {
@@ -503,6 +565,13 @@ export default {
}
</script>
<style scoped>
>>> .emoji-btn {
background-color: initial !important;
}
>>> .emoji-btn .v-btn__content {
color: initial;
}
.absolute-pos {
pointer-events: auto;
position: absolute;
@@ -1,6 +1,10 @@
<template>
<div>
<v-card class="mx-2 mb-2 rounded-lg">
<div @mouseenter="hovered = true" @mouseleave="hovered = false">
<v-card
class="mx-2 my-4 rounded-lg"
:elevation="`${hovered ? 10 : 2}`"
style="transition: all 0.2s ease"
>
<v-toolbar
v-ripple
class="transparent"
@@ -41,20 +45,23 @@
mdi-filter
</v-icon>
</v-btn>
<v-btn small icon @click.stop="expanded = !expanded">
<v-icon x-small>{{ expanded ? 'mdi-minus' : 'mdi-plus' }}</v-icon>
</v-btn>
</v-toolbar>
<div class="caption my-2 px-2 pb-2">
<div
class="caption my-2 px-2 pb-2"
style="cursor: pointer"
@click.stop="expanded = !expanded"
>
{{ commit.message }}
<v-divider class="my-2" />
<timeago :datetime="commit.createdAt"></timeago>
,
{{ new Date(commit.createdAt).toLocaleString() }}
<v-btn block depressed x-small class="mt-4" @click.stop="expanded = !expanded">
{{ expanded ? 'Hide' : 'Expand' }} Data View
</v-btn>
</div>
<v-expand-transition>
<div v-show="expanded" class="px-1 pb-2">
<v-divider class="mx-2 my-2" />
<object-properties
:obj="{
referencedId:
@@ -84,7 +91,8 @@ export default {
},
data() {
return {
expanded: false
expanded: false,
hovered: false
}
},
computed: {
@@ -36,7 +36,7 @@ export default {
},
props: {
obj: {
type: Object,
type: [Object, Array],
default: () => null
},
streamId: {
@@ -106,8 +106,8 @@ export default {
generateKVPs() {
for (const key of Object.keys(this.realObject)) {
if (this.ignoredProps.indexOf(key) !== -1) continue
const value = this.realObject[key]
const type = Array.isArray(this.realObject[key])
let value = this.realObject[key]
let type = Array.isArray(this.realObject[key])
? 'array'
: typeof this.realObject[key]
const extras = []
@@ -120,6 +120,13 @@ export default {
)
extras.push('visibility')
// handle undefined as well as null 'values'
// eslint-disable-next-line eqeqeq
if (value == null) {
value = 'null'
type = 'null'
}
this.kvps.push({
key: this.cleanKey(key),
originalKey: key,
@@ -131,11 +131,11 @@ export default {
},
props: {
prop: {
type: Object,
type: [Object, Array],
default: () => null
},
parent: {
type: Object,
type: [Object, Array],
default: () => null
},
refId: {
@@ -87,8 +87,7 @@
</div>
</template>
<script>
// TODO: Need to fix the viewer package build process to be able to properly reference THREE.js
/* global THREE */
import * as THREE from 'three'
import gql from 'graphql-tag'
import { v4 as uuid } from 'uuid'
import debounce from 'lodash/debounce'
@@ -18,14 +18,7 @@
/>
</div>
<div class="text-right flex-grow-1">
<v-btn
v-tooltip="'TODO: Add by object id'"
dark
small
icon
class="ml-2"
@click="showObjectDialog = true"
>
<v-btn dark small icon class="ml-2" @click="showObjectDialog = true">
<v-icon small>mdi-cube-outline</v-icon>
</v-btn>
<v-btn dark icon class="ml-2" @click="$emit('close')">
@@ -138,16 +138,16 @@
<viewer-controls @show-add-overlay="showAddOverlay = true" />
</div>
<div
style="
:style="`
height: 100vh;
width: 100%;
top: -64px;
${!$vuetify.breakpoint.smAndDown ? 'top: -64px;' : 'top: -56px;'}
left: 0;
position: absolute;
z-index: 4;
pointer-events: none;
overflow: none;
"
`"
class=""
>
<viewer-bubbles key="a" />
@@ -1,7 +1,7 @@
<template>
<v-container class="pa-0">
<portal to="toolbar">
<div class="d-flex align-center">
<div v-if="stream" class="d-flex align-center">
<div class="text-truncate">
<router-link
v-tooltip="stream.name"
@@ -19,7 +19,7 @@
</div>
</div>
</portal>
<v-row>
<v-row v-if="stream">
<v-col v-if="stream.role !== 'stream:owner'" cols="12">
<v-alert type="warning">
Your permission level ({{ stream.role }}) is not high enough to edit this
@@ -34,6 +34,7 @@
</template>
<v-card-text>
<v-form ref="form" v-model="valid" class="px-2" @submit.prevent="save">
<h2>Name and description</h2>
<v-text-field
v-model="name"
:rules="validation.nameRules"
@@ -49,7 +50,7 @@
class="mt-5"
:disabled="stream.role !== 'stream:owner'"
/>
<h2>Privacy</h2>
<v-switch
v-model="isPublic"
inset
@@ -63,6 +64,25 @@
persistent-hint
:disabled="stream.role !== 'stream:owner'"
/>
<br />
<h2>Comments</h2>
<v-switch
v-model="allowPublicComments"
inset
class="mt-5"
:label="
allowPublicComments
? 'Anyone can comment'
: 'Only collaborators can comment'
"
:hint="
allowPublicComments
? 'Any signed in user can leave a comment; the stream needs to be public.'
: 'Only collaborators can comment.'
"
persistent-hint
:disabled="stream.role !== 'stream:owner'"
/>
</v-form>
</v-card-text>
<v-card-actions>
@@ -71,6 +91,7 @@
color="primary"
type="submit"
:disabled="!canSave"
block
@click="save"
>
Save Changes
@@ -79,7 +100,7 @@
</section-card>
</v-col>
<v-col cols="12">
<section-card expandable :expand="false">
<section-card :expand="true">
<template #header>Danger Zone</template>
<v-card-text class="d-flex align-center">
@@ -169,6 +190,7 @@ export default {
name
description
isPublic
allowPublicComments
role
}
}
@@ -185,7 +207,8 @@ export default {
({
name: this.name,
description: this.description,
isPublic: this.isPublic
isPublic: this.isPublic,
allowPublicComments: this.allowPublicComments
} = stream)
return stream
@@ -202,6 +225,7 @@ export default {
streamNameConfirm: '',
description: null,
isPublic: true,
allowPublicComments: true,
validation: {
nameRules: [(v) => !!v || 'A stream must have a name!']
}
@@ -213,11 +237,19 @@ export default {
this.valid &&
(this.name !== this.stream.name ||
this.description !== this.stream.description ||
this.isPublic !== this.stream.isPublic)
this.isPublic !== this.stream.isPublic ||
this.allowPublicComments !== this.stream.allowPublicComments)
)
}
},
watch: {
allowPublicComments(newVal) {
if (newVal && !this.isPublic) this.isPublic = true
},
isPublic(newVal) {
if (!newVal && this.allowPublicComments) this.allowPublicComments = false
}
},
methods: {
async save() {
this.loading = true
@@ -234,7 +266,8 @@ export default {
id: this.stream.id,
name: this.name,
description: this.description,
isPublic: this.isPublic
isPublic: this.isPublic,
allowPublicComments: this.allowPublicComments
}
}
})
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -1,6 +1,6 @@
import Vue from 'vue'
import Vuex from 'vuex'
import emojis from './emojis'
Vue.use(Vuex)
// Note: this is currently used only for 3d viewer filtering purposes. All other state
@@ -21,7 +21,9 @@ const store = new Vuex.Store({
hideCategoryValues: [],
selectedComment: null,
addingComment: false,
preventCommentCollapse: false
preventCommentCollapse: false,
commentReactions: ['❤️', '✏️', '🔥', '📍', '😲'],
emojis
},
mutations: {
setViewerBusy(state, { viewerBusyState }) {
@@ -18,11 +18,12 @@
{{ stream.role.split(':')[1] }}
</v-chip>
<span
v-tooltip="
`Last updated: ${new Date(stream.updatedAt).toLocaleString()}<br>
Commits: ${stream.commits.totalCount} <br>
Branches: ${stream.branches.totalCount}`
"
v-tooltip="{
html: true,
content: `Last updated: ${new Date(stream.updatedAt).toLocaleString()}<br>
Commits: ${stream.commits.totalCount} <br>
Branches: ${stream.branches.totalCount}`
}"
class="caption mx-1"
>
Updated
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@speckle/objectloader",
"version": "2.4.2",
"version": "2.5.4",
"description": "Simple API helper to stream in objects from the Speckle Server.",
"main": "dist/objectloader.js",
"module": "dist/objectloader.esm.js",
@@ -17,6 +17,7 @@
"lint": "eslint . --ext .js,.ts",
"build:dev": "rollup --config",
"build": "NODE_ENV=production rollup --config",
"prepack": "yarn build",
"dev": "rollup --config --watch",
"example": "EXAMPLE_BUILD=1 rollup --config && http-server ./examples/browser -p 3031 -o"
},
+3 -2
View File
@@ -14,7 +14,7 @@ This is a small utility class that helps you stream an object and all its sub-co
### Examples
If you've got this repo checked out locally, you can run `npm run example` to run an example web page running ObjectLoader in the browser at 'http://127.0.0.1:3031/'. This will run the example HTML found under ./examples/browser/'.
If you've got this repo checked out locally, you can run `yarn example` to run an example web page running ObjectLoader in the browser at 'http://127.0.0.1:3031/'. This will run the example HTML found under ./examples/browser/'.
To test ObjectLoader in a node environment, just run `node ./examples/node/script.mjs`
@@ -80,7 +80,8 @@ let loader = new ObjectLoader({
## Development
Run `npm run build` to build prod release, run `npm run build:dev` to build dev release. Or run `npm run dev` to run the build in `watch` mode.
Run `yarn build` to build prod release, run `yarn build:dev` to build dev release.
Or run `yarn dev` to run the build in `watch` mode.
## Community
+35 -31
View File
@@ -2,41 +2,34 @@
# build stage
FROM node:14.16-buster-slim as deps-build
FROM node:14.16-buster-slim as build-stage
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /opt/objectloader
COPY packages/objectloader/package*.json ./
RUN npm install --production=false
COPY packages/objectloader .
RUN npm run build
# this whole thing is required cause npm 6.* doesn't allow for pack output dir specification
RUN mkdir /packages
# invoke npm pack and move its result to the packages folder when done
RUN mv $(npm pack) /packages/
WORKDIR /opt/viewer
COPY packages/viewer/package*.json ./
# this installs objectloader from a tarball
RUN npm i $(find /packages -type f -name "speckle*.tgz")
# Install dependencies and devDependencies
RUN npm install --production=false
COPY packages/viewer .
RUN npm run build
RUN mv $(npm pack) /packages/
WORKDIR /speckle-server
COPY .yarnrc.yml .
COPY .yarn ./.yarn
COPY package.json yarn.lock ./
# Onyl copy in the relevant package.json files for the dependencies
COPY packages/preview-service/package.json ./packages/preview-service/
COPY packages/viewer/package.json ./packages/viewer/
COPY packages/objectloader/package.json ./packages/objectloader/
RUN yarn workspaces focus -A
RUN yarn
# Onyl copy in the relevant source files for the dependencies
COPY packages/objectloader ./packages/objectloader/
COPY packages/viewer ./packages/viewer/
COPY packages/preview-service ./packages/preview-service/
# This way the foreach only builds the frontend and its deps
RUN yarn workspaces foreach -pt run build
FROM node:14.16-buster-slim as build-stage
WORKDIR /opt/preview-service
COPY --from=deps-build /packages /packages
COPY packages/preview-service/package*.json ./
# this installs viewer and objectloader from a tarball
RUN npm i $(find /packages -type f -name "speckle*.tgz")
RUN npm ci --production=false
COPY packages/preview-service .
RUN npm run build-fe
FROM node:14.16-buster-slim as node
@@ -55,9 +48,20 @@ RUN chmod +x /wait
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
COPY --from=build-stage /opt/preview-service /opt/preview-service
WORKDIR /speckle-server
COPY .yarnrc.yml .
COPY .yarn ./.yarn
COPY package.json yarn.lock ./
WORKDIR /opt/preview-service
RUN yarn plugin import workspace-tools
# Onyl copy in the relevant package.json files for the dependencies
COPY packages/preview-service/package.json ./packages/preview-service/
WORKDIR /speckle-server/packages/preview-service
RUN yarn workspaces focus --production
COPY --from=build-stage /speckle-server/packages/preview-service ./
ENTRYPOINT [ "tini", "--" ]
CMD ["node", "bin/www"]
CMD ["yarn", "node", "bin/www"]
-14
View File
@@ -10,10 +10,6 @@ const indexRouter = require('./routes/index')
const previewRouter = require('./routes/preview')
const objectsRouter = require('./routes/objects')
const apiRouter = require('./routes/api')
const prometheusClient = require('prom-client')
prometheusClient.register.clear()
prometheusClient.collectDefaultMetrics()
const app = express()
@@ -29,16 +25,6 @@ app.use('/preview', previewRouter)
app.use('/objects', objectsRouter)
app.use('/api', apiRouter)
// Expose prometheus metrics
app.get('/metrics', async (req, res) => {
try {
res.set('Content-Type', prometheusClient.register.contentType)
res.end(await prometheusClient.register.metrics())
} catch (ex) {
res.status(500).end(ex.message)
}
})
// catch 404 and forward to error handler
app.use(function (req, res, next) {
next(createError(404))
@@ -4,6 +4,7 @@ const crypto = require('crypto')
const knex = require('../knex')
const fetch = require('node-fetch')
const fs = require('fs')
const metrics = require('./prometheusMetrics')
let shouldExit = false
@@ -78,6 +79,7 @@ async function doTask(task) {
`,
[{}, task.streamId, task.objectId]
)
metrics.metricOperationErrors.labels('preview').inc()
}
}
@@ -96,11 +98,16 @@ async function tick() {
return
}
const metricDurationEnd = metrics.metricDuration.startTimer()
await doTask(task)
metricDurationEnd({ op: 'preview' })
// Check for another task very soon
setTimeout(tick, 10)
} catch (err) {
metrics.metricOperationErrors.labels('main_loop').inc()
console.log('Error executing task: ', err)
setTimeout(tick, 5000)
}
@@ -119,6 +126,8 @@ async function startPreviewService() {
console.log('Shutting down...')
})
metrics.initPrometheusMetrics()
tick()
}
@@ -0,0 +1,112 @@
/* eslint-disable no-unused-vars */
'use strict'
const http = require('http')
const prometheusClient = require('prom-client')
const knex = require('../knex')
let metricFree = null
let metricUsed = null
let metricPendingAquires = null
let metricQueryDuration = null
let metricQueryErrors = null
const queryStartTime = {}
prometheusClient.register.clear()
prometheusClient.register.setDefaultLabels({
project: 'speckle-server',
app: 'preview-service'
})
prometheusClient.collectDefaultMetrics()
let prometheusInitialized = false
function initKnexPrometheusMetrics() {
metricFree = new prometheusClient.Gauge({
name: 'speckle_server_knex_free',
help: 'Number of free DB connections',
collect() {
this.set(knex.client.pool.numFree())
}
})
metricUsed = new prometheusClient.Gauge({
name: 'speckle_server_knex_used',
help: 'Number of used DB connections',
collect() {
this.set(knex.client.pool.numUsed())
}
})
metricPendingAquires = new prometheusClient.Gauge({
name: 'speckle_server_knex_pending',
help: 'Number of pending DB connection aquires',
collect() {
this.set(knex.client.pool.numPendingAcquires())
}
})
metricQueryDuration = new prometheusClient.Summary({
name: 'speckle_server_knex_query_duration',
help: 'Summary of the DB query durations in seconds'
})
metricQueryErrors = new prometheusClient.Counter({
name: 'speckle_server_knex_query_errors',
help: 'Number of DB queries with errors'
})
knex.on('query', (data) => {
const queryId = data.__knexQueryUid + ''
queryStartTime[queryId] = Date.now()
})
knex.on('query-response', (data, obj, builder) => {
const queryId = obj.__knexQueryUid + ''
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
delete queryStartTime[queryId]
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
})
knex.on('query-error', (err, querySpec) => {
const queryId = querySpec.__knexQueryUid + ''
const durationSec = (Date.now() - queryStartTime[queryId]) / 1000
delete queryStartTime[queryId]
if (!isNaN(durationSec)) metricQueryDuration.observe(durationSec)
metricQueryErrors.inc()
})
}
module.exports = {
initPrometheusMetrics() {
if (prometheusInitialized) return
prometheusInitialized = true
initKnexPrometheusMetrics()
// Define the HTTP server
const server = http.createServer(async (req, res) => {
if (req.url === '/metrics') {
res.setHeader('Content-Type', prometheusClient.register.contentType)
res.end(await prometheusClient.register.metrics())
} else {
res.end('Speckle Preview Service - prometheus metrics')
}
})
server.listen(Number(process.env.PROMETHEUS_METRICS_PORT) || 9094)
},
metricDuration: new prometheusClient.Histogram({
name: 'speckle_server_operation_duration',
help: 'Summary of the operation durations in seconds',
buckets: [0.5, 1, 5, 10, 30, 60, 300, 600],
labelNames: ['op']
}),
metricOperationErrors: new prometheusClient.Counter({
name: 'speckle_server_operation_errors',
help: 'Number of operations with errors',
labelNames: ['op']
})
}
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "@speckle/preview-service",
"private": true,
"version": "2.4.2",
"version": "2.5.4",
"description": "Generate PNG previews of Speckle objects by using a headless viewer",
"main": "index.js",
"homepage": "https://speckle.systems",
@@ -15,7 +15,7 @@
},
"scripts": {
"dev": "DEBUG='preview-service:*' nodemon --trace-deprecation ./bin/www",
"build-fe": "webpack --env dev --config webpack.config.render_page.js && webpack --env build --config webpack.config.render_page.js",
"build": "webpack --env dev --config webpack.config.render_page.js && webpack --env build --config webpack.config.render_page.js",
"lint": "eslint . --ext .js,.ts"
},
"dependencies": {
+2 -2
View File
@@ -13,7 +13,7 @@ This is an overview of this service:
With an updated viewer installed in the current directory, you should first build the frontend-part of the preview service: The simple webpage with the viewer that will be accessed with Puppeteer to generate the preview:
```
npm run build-fe
yarn build
```
This should be rerun whenever you make changes to the viewer (if you make local viewer changes, don't forget to build the viewer module before running this)
@@ -21,7 +21,7 @@ This should be rerun whenever you make changes to the viewer (if you make local
After the viewer web page is up to date, run the preview service with:
```
npm run dev
yarn dev
```
This will use the default dev DB connection of `postgres://speckle:speckle@localhost/speckle`. You can pass the environment variable `PG_CONNECTION_STRING` to change this to a different DB.
@@ -45,7 +45,11 @@ const config = {
})
],
resolve: {
modules: [path.resolve('./node_modules'), path.resolve('.render_page/src')],
modules: [
path.resolve('../../node_modules'),
path.resolve('./node_modules'),
path.resolve('.render_page/src')
],
extensions: ['.json', '.js']
},
devServer: {
+12 -8
View File
@@ -1,4 +1,7 @@
FROM node:16.13-bullseye-slim as node
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
ARG SPECKLE_SERVER_VERSION=custom
RUN apt-get update && apt-get install -y \
tini \
@@ -8,16 +11,17 @@ RUN apt-get update && apt-get install -y \
ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.8.0/wait /wait
RUN chmod +x /wait
ARG NODE_ENV=production
ENV NODE_ENV=${NODE_ENV}
WORKDIR /speckle-server
ARG SPECKLE_SERVER_VERSION=custom
ENV SPECKLE_SERVER_VERSION=${SPECKLE_SERVER_VERSION}
WORKDIR /app
COPY .yarnrc.yml .
COPY .yarn ./.yarn
COPY package.json yarn.lock ./
COPY packages/server/package*.json ./
RUN npm ci
WORKDIR /speckle-server/packages/server
COPY packages/server/package.json .
RUN yarn workspaces focus --production
COPY packages/server .
CMD ["node", "bin/www"]
ENV SPECKLE_SERVER_VERSION=${SPECKLE_SERVER_VERSION}
CMD ["yarn", "node", "bin/www"]
+2 -3
View File
@@ -7,15 +7,14 @@ const express = require('express')
// `express-async-errors` patches express to catch errors in async handlers. no variable needed
require('express-async-errors')
const compression = require('compression')
const appRoot = require('app-root-path')
const logger = require('morgan-debug')
const bodyParser = require('body-parser')
const debug = require('debug')
const { createTerminus } = require('@godaddy/terminus')
const Sentry = require('@sentry/node')
const Logging = require(`${appRoot}/logging`)
const { errorLoggingMiddleware } = require(`${appRoot}/logging/errorLogging`)
const Logging = require('@/logging')
const { errorLoggingMiddleware } = require('@/logging/errorLogging')
const prometheusClient = require('prom-client')
const { ApolloServer, ForbiddenError } = require('apollo-server-express')
+5 -1
View File
@@ -5,11 +5,11 @@
// Initializing module aliases for absolute import paths
require('module-alias')({ base: __dirname })
const appRoot = __dirname
// Initializing env vars
const dotenv = require('dotenv')
const { isTestEnv } = require('./modules/core/helpers/envHelper')
const appRoot = require('app-root-path')
// If running in test env, load .env.test first
// (appRoot necessary, cause env files aren't loaded through require() calls)
@@ -25,3 +25,7 @@ if (isTestEnv()) {
}
dotenv.config({ path: `${appRoot}/.env` })
module.exports = {
appRoot
}
@@ -16,8 +16,10 @@ module.exports = {
labelNames: ['route']
})
}
return responseTime(function (req, res, time) {
let route = 'unknown'
if (req.originalUrl === '/graphql') route = '/graphql'
if (req.route && req.route.path) route = req.route.path
metricRequestDuration.labels(route).observe(time / 1000)
})
+5
View File
@@ -15,7 +15,12 @@ module.exports = function (app) {
if (!prometheusInitialized) {
prometheusInitialized = true
prometheusClient.register.clear()
prometheusClient.register.setDefaultLabels({
project: 'speckle-server',
app: 'server'
})
prometheusClient.collectDefaultMetrics()
initKnexPrometheusMetrics()
}
@@ -1,7 +1,6 @@
'use strict'
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const knex = require('@/db/knex')
const { dispatchStreamEvent } = require('../../webhooks/services/webhooks')
const StreamActivity = () => knex('stream_activity')
@@ -1,14 +1,13 @@
/* istanbul ignore file */
const expect = require('chai').expect
const appRoot = require('app-root-path')
const { createUser } = require('../../core/services/users')
const { createPersonalAccessToken } = require('../../core/services/tokens')
const { createObject } = require('../../core/services/objects')
const { getUserActivity } = require('../services')
const { beforeEachContext, initializeTestServer } = require(`${appRoot}/test/hooks`)
const { noErrors } = require(`${appRoot}/test/helpers`)
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
const { noErrors } = require('@/test/helpers')
let sendRequest
+1 -2
View File
@@ -1,14 +1,13 @@
/* istanbul ignore file */
'use strict'
const debug = require('debug')
const appRoot = require('app-root-path')
exports.init = (app) => {
debug('speckle:modules')('💅 Init graphql api explorer module')
// sweet and simple
app.get('/explorer', (req, res) => {
res.sendFile(`${appRoot}/modules/apiexplorer/explorer.html`)
res.sendFile(require.resolve('@/modules/apiexplorer/explorer.html'))
})
}
+72 -31
View File
@@ -1,11 +1,13 @@
'use strict'
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const debug = require('debug')
const knex = require('@/db/knex')
const Scopes = () => knex('scopes')
const Apps = () => knex('server_apps')
const AppScopes = () => knex('server_apps_scopes')
const { getApp, revokeExistingAppCredentials } = require('./services/apps')
const { getApp } = require('@/modules/auth/services/apps')
const { Scopes: ScopesConst } = require('@/modules/core/helpers/mainConstants')
const { difference } = require('lodash')
let allScopes = []
@@ -47,25 +49,61 @@ async function registerDefaultApp(app) {
}
async function updateDefaultApp(app, existingApp) {
existingApp.scopes = existingApp.scopes.map((s) => s.name)
const existingAppScopes = existingApp.scopes.map((s) => s.name)
const scopeDiffA = app.scopes.filter(
(scope) => existingApp.scopes.indexOf(scope) === -1
)
const scopeDiffB = existingApp.scopes.filter(
(scope) => app.scopes.indexOf(scope) === -1
)
const newScopes = difference(app.scopes, existingAppScopes)
const removedScopes = difference(existingAppScopes, app.scopes)
if (scopeDiffA.length !== 0 || scopeDiffB.length !== 0) {
await revokeExistingAppCredentials({ appId: app.id })
const scopes = app.scopes.map((s) => ({ appId: app.id, scopeName: s }))
await AppScopes().insert(scopes)
let affectedTokenIds = []
if (newScopes.length || removedScopes.length) {
debug('speckle:modules')(`🔑 Updating default app ${app.name}`)
affectedTokenIds = await knex('user_server_app_tokens')
.where({ appId: app.id })
.pluck('tokenId')
}
delete app.scopes
await Apps().where({ id: app.id }).update(app)
// the internal code block makes sure if an error occurred, the trx gets rolled back
await knex.transaction(async (trx) => {
// add new scopes to the app
if (newScopes.length)
await AppScopes()
.insert(newScopes.map((s) => ({ appId: app.id, scopeName: s })))
.transacting(trx)
// remove scopes from the app
if (removedScopes.length)
await AppScopes()
.where({ appId: app.id })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
//update user tokens with scope changes
if (affectedTokenIds.length)
await Promise.all(
affectedTokenIds.map(async (tokenId) => {
if (newScopes.length)
await knex('token_scopes')
.insert(newScopes.map((s) => ({ tokenId, scopeName: s })))
.transacting(trx)
if (removedScopes.length)
await knex('token_scopes')
.where({ tokenId })
.whereIn('scopeName', removedScopes)
.delete()
.transacting(trx)
})
)
delete app.scopes
await Apps().where({ id: app.id }).update(app).transacting(trx)
})
}
// this is exported to be able to test the retention of permissions
module.exports.updateDefaultApp = updateDefaultApp
const SpeckleWebApp = {
id: 'spklwebapp',
secret: 'spklwebapp',
@@ -99,11 +137,12 @@ const SpeckleDesktopApp = {
public: true,
redirectUrl: 'speckle://account',
scopes: [
'streams:read',
'streams:write',
'profile:read',
'profile:email',
'users:read'
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
@@ -116,11 +155,12 @@ const SpeckleConnectorApp = {
public: true,
redirectUrl: 'http://localhost:29363',
scopes: [
'streams:read',
'streams:write',
'profile:read',
'profile:email',
'users:read'
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
@@ -134,10 +174,11 @@ const SpeckleExcel = {
public: true,
redirectUrl: 'https://speckle-excel.netlify.app',
scopes: [
'streams:read',
'streams:write',
'profile:read',
'profile:email',
'users:read'
ScopesConst.Streams.Read,
ScopesConst.Streams.Write,
ScopesConst.Profile.Read,
ScopesConst.Profile.Email,
ScopesConst.Users.Read,
ScopesConst.Users.Invite
]
}
@@ -55,8 +55,10 @@ module.exports = {
async appUpdate(parent, args, context) {
const app = await getApp({ id: args.app.id })
// only admins can update the default apps, generated by the server
if (!app.author && context.role !== 'server:admin')
throw new ForbiddenError('You are not authorized to edit this app.')
// only the author or an admin can update a 3rd party app
if (app.author.id !== context.userId && context.role !== 'server:admin')
throw new ForbiddenError('You are not authorized to edit this app.')
+1 -2
View File
@@ -1,7 +1,6 @@
'use strict'
const debug = require('debug')
const appRoot = require('app-root-path')
const { registerOrUpdateScope } = require(`${appRoot}/modules/shared`)
const { registerOrUpdateScope } = require('@/modules/shared')
exports.init = async (app) => {
debug('speckle:modules')('🔑 Init auth module')
@@ -39,6 +39,7 @@ module.exports = {
'server_apps.id',
'server_apps.name',
'server_apps.description',
'server_apps.trustByDefault',
'server_apps.logo',
'server_apps.termsAndConditionsLink',
'users.name as authorName',
+11 -2
View File
@@ -1,12 +1,11 @@
'use strict'
const appRoot = require('app-root-path')
const redis = require('redis')
const ExpressSession = require('express-session')
const RedisStore = require('connect-redis')(ExpressSession)
const passport = require('passport')
const sentry = require(`${appRoot}/logging/sentryHelper`)
const sentry = require('@/logging/sentryHelper')
const { createAuthorizationCode } = require('./services/apps')
module.exports = async (app) => {
@@ -51,6 +50,16 @@ module.exports = async (app) => {
userId: req.user.id,
challenge: req.session.challenge
})
// const defaultApps = ['explorer', 'sdm', 'sca', 'spklexcel']
// await Promise.all(
// defaultApps.map((appId) =>
// createAuthorizationCode({
// appId,
// userId: req.user.id,
// challenge: req.session.challenge
// })
// )
// )
if (req.session) req.session.destroy()
return res.redirect(`${process.env.CANONICAL_URL}?access_code=${ac}`)
} catch (err) {
@@ -5,16 +5,9 @@ const passport = require('passport')
const OIDCStrategy = require('passport-azure-ad').OIDCStrategy
const URL = require('url').URL
const debug = require('debug')
const appRoot = require('app-root-path')
const {
findOrCreateUser,
getUserByEmail
} = require(`${appRoot}/modules/core/services/users`)
const { getServerInfo } = require(`${appRoot}/modules/core/services/generic`)
const {
validateInvite,
useInvite
} = require(`${appRoot}/modules/serverinvites/services`)
const { findOrCreateUser, getUserByEmail } = require('@/modules/core/services/users')
const { getServerInfo } = require('@/modules/core/services/generic')
const { validateInvite, useInvite } = require('@/modules/serverinvites/services')
module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = new OIDCStrategy(
@@ -5,16 +5,9 @@ const passport = require('passport')
const GithubStrategy = require('passport-github2')
const URL = require('url').URL
const debug = require('debug')
const appRoot = require('app-root-path')
const {
findOrCreateUser,
getUserByEmail
} = require(`${appRoot}/modules/core/services/users`)
const { getServerInfo } = require(`${appRoot}/modules/core/services/generic`)
const {
validateInvite,
useInvite
} = require(`${appRoot}/modules/serverinvites/services`)
const { findOrCreateUser, getUserByEmail } = require('@/modules/core/services/users')
const { getServerInfo } = require('@/modules/core/services/generic')
const { validateInvite, useInvite } = require('@/modules/serverinvites/services')
module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = {
@@ -3,16 +3,9 @@
const passport = require('passport')
const GoogleStrategy = require('passport-google-oauth20').Strategy
const debug = require('debug')
const appRoot = require('app-root-path')
const {
findOrCreateUser,
getUserByEmail
} = require(`${appRoot}/modules/core/services/users`)
const { getServerInfo } = require(`${appRoot}/modules/core/services/generic`)
const {
validateInvite,
useInvite
} = require(`${appRoot}/modules/serverinvites/services`)
const { findOrCreateUser, getUserByEmail } = require('@/modules/core/services/users')
const { getServerInfo } = require('@/modules/core/services/generic')
const { validateInvite, useInvite } = require('@/modules/serverinvites/services')
module.exports = async (app, session, sessionStorage, finalizeAuth) => {
const strategy = {
@@ -1,18 +1,14 @@
'use strict'
const appRoot = require('app-root-path')
const debug = require('debug')
const {
createUser,
updateUser,
validatePasssword,
getUserByEmail
} = require(`${appRoot}/modules/core/services/users`)
const { getServerInfo } = require(`${appRoot}/modules/core/services/generic`)
const {
validateInvite,
useInvite
} = require(`${appRoot}/modules/serverinvites/services`)
const { respectsLimits } = require(`${appRoot}/modules/core/services/ratelimits`)
} = require('@/modules/core/services/users')
const { getServerInfo } = require('@/modules/core/services/generic')
const { validateInvite, useInvite } = require('@/modules/serverinvites/services')
const { respectsLimits } = require('@/modules/core/services/ratelimits')
module.exports = async (app, session, sessionAppId, finalizeAuth) => {
const strategy = {
+113 -10
View File
@@ -1,10 +1,9 @@
/* istanbul ignore file */
const expect = require('chai').expect
const appRoot = require('app-root-path')
const { createUser } = require(`${appRoot}/modules/core/services/users`)
const { validateToken } = require(`${appRoot}/modules/core/services/tokens`)
const { beforeEachContext } = require(`${appRoot}/test/hooks`)
const { createUser } = require(`@/modules/core/services/users`)
const { validateToken } = require(`@/modules/core/services/tokens`)
const { beforeEachContext } = require(`@/test/hooks`)
const {
getApp,
getAllPublicApps,
@@ -17,6 +16,10 @@ const {
revokeExistingAppCredentialsForUser
} = require('../services/apps')
const { Scopes } = require('@/modules/core/helpers/mainConstants')
const { updateDefaultApp } = require('@/modules/auth/defaultApps')
const knex = require('@/db/knex')
describe('Services @apps-services', () => {
const actor = {
name: 'Dimitrie Stefanescu',
@@ -63,7 +66,7 @@ describe('Services @apps-services', () => {
const res = await createApp({
name: 'test application',
public: true,
scopes: ['streams:read'],
scopes: [Scopes.Streams.Read],
redirectUrl: 'http://localhost:1335'
})
@@ -99,7 +102,7 @@ describe('Services @apps-services', () => {
app: {
name: 'updated test application',
id: myTestApp.id,
scopes: ['streams:read', 'users:read']
scopes: [Scopes.Streams.Read, Scopes.Users.Read]
}
})
expect(res).to.be.a('string')
@@ -107,8 +110,8 @@ describe('Services @apps-services', () => {
const app = await getApp({ id: myTestApp.id })
expect(app.name).to.equal('updated test application')
expect(app.scopes).to.be.an('array')
expect(app.scopes.map((s) => s.name)).to.include('users:read')
expect(app.scopes.map((s) => s.name)).to.include('streams:read')
expect(app.scopes.map((s) => s.name)).to.include(Scopes.Users.Read)
expect(app.scopes.map((s) => s.name)).to.include(Scopes.Streams.Read)
})
const challenge = 'random'
@@ -142,7 +145,7 @@ describe('Services @apps-services', () => {
const validation = await validateToken(response.token)
expect(validation.valid).to.equal(true)
expect(validation.userId).to.equal(actor.id)
expect(validation.scopes[0]).to.equal('streams:read')
expect(validation.scopes[0]).to.equal(Scopes.Streams.Read)
})
it('Should refresh the token using the refresh token, and get a fresh refresh token and token', async () => {
@@ -185,7 +188,7 @@ describe('Services @apps-services', () => {
app: {
name: 'updated test application',
id: myTestApp.id,
scopes: ['streams:write', 'users:read']
scopes: [Scopes.Streams.Write, Scopes.Users.Read]
}
})
@@ -214,6 +217,106 @@ describe('Services @apps-services', () => {
.catch((err) => expect(err.message).to.equal('Access code not found.'))
})
const defaultApps = ['spklwebapp', 'explorer', 'sdm', 'sca', 'spklexcel']
defaultApps.forEach((speckleAppId) => {
it(`Should not invalidate tokens, refresh tokens and access codes for default app: ${speckleAppId}, if updated`, async () => {
const [unusedAccessCode, usedAccessCode] = await Promise.all([
createAuthorizationCode({
appId: speckleAppId,
userId: actor.id,
challenge
}),
createAuthorizationCode({
appId: speckleAppId,
userId: actor.id,
challenge
})
])
const apiTokenResponse = await createAppTokenFromAccessCode({
appId: speckleAppId,
appSecret: speckleAppId,
accessCode: usedAccessCode,
challenge
})
// We now have one unused access code, an api token and a refresh token.
// Proceed to update the app:
const existingApp = await getApp({ id: speckleAppId })
const newScopes = [Scopes.Streams.Write, Scopes.Users.Read]
await updateDefaultApp(
{
name: 'updated test application',
id: speckleAppId,
scopes: newScopes
},
existingApp
)
const updatedApp = await getApp({ id: speckleAppId })
expect(updatedApp.scopes.map((s) => s.name)).to.equalInAnyOrder(newScopes)
const validationResponse = await validateToken(apiTokenResponse.token)
expect(validationResponse.valid).to.equal(true)
const refreshedToken = await refreshAppToken({
refreshToken: apiTokenResponse.refreshToken,
appId: speckleAppId,
appSecret: speckleAppId
})
expect(refreshedToken.refreshToken).to.exist
expect(refreshedToken.token).to.exist
const appToken = await createAppTokenFromAccessCode({
appId: speckleAppId,
appSecret: speckleAppId,
accessCode: unusedAccessCode,
challenge: 'random'
})
expect(appToken.token).to.exist
expect(appToken.refreshToken).to.exist
const apiTokens = await knex('user_server_app_tokens')
.join(
'token_scopes',
'user_server_app_tokens.tokenId',
'=',
'token_scopes.tokenId'
)
.where({
appId: speckleAppId
})
expect(newScopes).to.include.members(apiTokens.map((t) => t.scopeName))
})
})
it('Updating a default app with bad data should leave the app in an untouched state', async () => {
const speckleAppId = 'explorer'
const existingApp = await getApp({ id: speckleAppId })
try {
await updateDefaultApp(
{
name: 'updated test application',
id: speckleAppId,
scopes: ['aWeird:Scope']
},
existingApp
)
throw new Error('This should have failed')
} catch (err) {
// check that the weird:Scope violates a foreign key constraint...
// leaky abstractions i know, but no better way to test this for now
expect(err.message).to.contain('server_apps_scopes_scopename_foreign')
}
const notUpdatedApp = await getApp({ id: speckleAppId })
// check that no harm was done
expect(notUpdatedApp.name).to.equal(existingApp.name)
expect(notUpdatedApp.scopes).to.equalInAnyOrder(existingApp.scopes)
})
it('Should revoke access for a given user', async () => {
const secondUser = {
name: 'Dimitrie Stefanescu',
@@ -1,13 +1,12 @@
/* eslint-disable camelcase */
/* istanbul ignore file */
const chai = require('chai')
const appRoot = require('app-root-path')
const expect = chai.expect
const { createUser } = require(`${appRoot}/modules/core/services/users`)
const { createPersonalAccessToken } = require(`${appRoot}/modules/core/services/tokens`)
const { beforeEachContext, initializeTestServer } = require(`${appRoot}/test/hooks`)
const { createUser } = require('@/modules/core/services/users')
const { createPersonalAccessToken } = require('@/modules/core/services/tokens')
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
const {
createAuthorizationCode,
createAppTokenFromAccessCode
@@ -2,14 +2,13 @@
const crs = require('crypto-random-string')
const chai = require('chai')
const request = require('supertest')
const appRoot = require('app-root-path')
const { createStream, getStream } = require(`${appRoot}/modules/core/services/streams`)
const { createStream, getStream } = require('@/modules/core/services/streams')
const { updateServerInfo } = require(`${appRoot}/modules/core/services/generic`)
const { getUserByEmail } = require(`${appRoot}/modules/core/services/users`)
const { LIMITS } = require(`${appRoot}/modules/core/services/ratelimits`)
const { createAndSendInvite } = require(`${appRoot}/modules/serverinvites/services`)
const { beforeEachContext, initializeTestServer } = require(`${appRoot}/test/hooks`)
const { updateServerInfo } = require('@/modules/core/services/generic')
const { getUserByEmail } = require('@/modules/core/services/users')
const { LIMITS } = require('@/modules/core/services/ratelimits')
const { createAndSendInvite } = require('@/modules/serverinvites/services')
const { beforeEachContext, initializeTestServer } = require('@/test/hooks')
const expect = chai.expect
let app
@@ -1,5 +1,5 @@
const knex = require('@/db/knex')
const appRoot = require('app-root-path')
const { appRoot } = require('@/bootstrap')
const fs = require('fs/promises')
/** @type {import('yargs').CommandModule} */
@@ -1,7 +1,7 @@
const appRoot = require('app-root-path')
const { authorizeResolver, pubsub } = require(`${appRoot}/modules/shared`)
const { authorizeResolver, pubsub } = require('@/modules/shared')
const { ForbiddenError, ApolloError, withFilter } = require('apollo-server-express')
const { getStream } = require(`${appRoot}/modules/core/services/streams`)
const { getStream } = require('@/modules/core/services/streams')
const { saveActivity } = require('@/modules/activitystream/services')
const {
getComment,
@@ -13,8 +13,9 @@ const {
viewComment,
archiveComment,
editComment,
streamResourceCheck
} = require(`${appRoot}/modules/comments/services`)
streamResourceCheck,
formatCommentText
} = require('@/modules/comments/services')
const authorizeStreamAccess = async ({ streamId, userId, auth }) => {
const stream = await getStream({ streamId, userId })
@@ -23,8 +24,8 @@ const authorizeStreamAccess = async ({ streamId, userId, auth }) => {
if (!stream.isPublic && auth === false)
throw new ForbiddenError('You are not authorized.')
if (!stream.isPublic) {
await authorizeResolver(userId, streamId, 'stream:reviewer')
if (!stream.isPublic && !stream.role) {
throw new ForbiddenError('You are not authorized.')
}
}
@@ -60,6 +61,9 @@ module.exports = {
limit: args.limit,
cursor: args.cursor
})
},
text(parent) {
return formatCommentText(parent)
}
},
Stream: {
@@ -107,7 +111,16 @@ module.exports = {
},
async userCommentThreadActivityBroadcast(parent, args, context) {
await authorizeResolver(context.userId, args.streamId, 'stream:reviewer')
if (!context.userId) return false
const stream = await getStream({
streamId: args.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
await pubsub.publish('COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: { eventType: 'reply-typing-status', data: args.data },
streamId: args.streamId,
@@ -117,7 +130,16 @@ module.exports = {
},
async commentCreate(parent, args, context) {
await authorizeResolver(context.userId, args.input.streamId, 'stream:reviewer')
if (!context.userId)
throw new ForbiddenError('Only registered users can comment.')
const stream = await getStream({
streamId: args.input.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
const id = await createComment({ userId: context.userId, input: args.input })
@@ -136,10 +158,21 @@ module.exports = {
resourceIds: args.input.resources.map((res) => res.resourceId).join(',') // TODO: hack for now
})
await saveActivity({
streamId: args.input.streamId,
resourceType: 'comment',
resourceId: id,
actionType: 'comment_created',
userId: context.userId,
info: { input: args.input },
message: `Comment added: ${id} (${args.input})`
})
return id
},
async commentEdit(parent, args, context) {
// NOTE: This is NOT in use anywhere
await authorizeResolver(context.userId, args.input.streamId, 'stream:reviewer')
await editComment({ userId: context.userId, input: args.input })
return true
@@ -147,7 +180,14 @@ module.exports = {
// used for flagging a comment as viewed
async commentView(parent, args, context) {
await authorizeResolver(context.userId, args.streamId, 'stream:reviewer')
const stream = await getStream({
streamId: args.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
await viewComment({ userId: context.userId, commentId: args.commentId })
return true
},
@@ -158,7 +198,9 @@ module.exports = {
userId: context.userId,
auth: context.auth
})
await archiveComment({ ...args, userId: context.userId })
await archiveComment({ ...args, userId: context.userId }) // NOTE: permissions check inside service
await pubsub.publish('COMMENT_THREAD_ACTIVITY', {
commentThreadActivity: {
eventType: args.archived ? 'comment-archived' : 'comment-added'
@@ -166,11 +208,30 @@ module.exports = {
streamId: args.streamId,
commentId: args.commentId
})
await saveActivity({
streamId: args.streamId,
resourceType: 'comment',
resourceId: args.commentId,
actionType: 'comment_archived',
userId: context.userId,
info: { input: args },
message: `Comment archived`
})
return true
},
async commentReply(parent, args, context) {
await authorizeResolver(context.userId, args.input.streamId, 'stream:reviewer')
if (!context.userId)
throw new ForbiddenError('Only registered users can comment.')
const stream = await getStream({
streamId: args.input.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
const id = await createCommentReply({
authorId: context.userId,
@@ -192,6 +253,16 @@ module.exports = {
streamId: args.input.streamId,
commentId: args.input.parentComment
})
await saveActivity({
streamId: args.input.streamId,
resourceType: 'comment',
resourceId: args.input.parentComment,
actionType: 'comment_reply',
userId: context.userId,
info: { input: args.input },
message: `Comment reply created.`
})
return id
}
},
@@ -200,7 +271,14 @@ module.exports = {
subscribe: withFilter(
() => pubsub.asyncIterator(['VIEWER_ACTIVITY']),
async (payload, variables, context) => {
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
return (
payload.streamId === variables.streamId &&
payload.resourceId === variables.resourceId
@@ -212,12 +290,20 @@ module.exports = {
subscribe: withFilter(
() => pubsub.asyncIterator(['COMMENT_ACTIVITY']),
async (payload, variables, context) => {
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
// if we're listening for a stream's root comments events
if (!variables.resourceIds) {
return payload.streamId === variables.streamId
}
// otherwise perform a deeper check
try {
// prevents comment exfiltration by listening in to a auth'ed stream, but different commit ("stream hopping" for subscriptions)
await streamResourceCheck({
@@ -247,7 +333,14 @@ module.exports = {
subscribe: withFilter(
() => pubsub.asyncIterator(['COMMENT_THREAD_ACTIVITY']),
async (payload, variables, context) => {
await authorizeResolver(context.userId, payload.streamId, 'stream:reviewer')
const stream = await getStream({
streamId: payload.streamId,
userId: context.userId
})
if (!stream.allowPublicComments && !stream.role)
throw new ForbiddenError('You are not authorized.')
return (
payload.streamId === variables.streamId &&
payload.commentId === variables.commentId
@@ -1,13 +1,22 @@
'use strict'
const crs = require('crypto-random-string')
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const knex = require('@/db/knex')
const sanitizeHtml = require('sanitize-html')
const Comments = () => knex('comments')
const CommentLinks = () => knex('comment_links')
const CommentViews = () => knex('comment_views')
module.exports = {
/**
* Format comment text field (e.g. for returning to frontend or saving to DB)
* @param {Object} comment
* @returns {string}
*/
formatCommentText(comment) {
if (!comment || !comment.text) return ''
return sanitizeHtml(comment.text)
},
async streamResourceCheck({ streamId, resources }) {
// this itches - a for loop with queries... but okay let's hit the road now
for (const res of resources) {
@@ -75,6 +84,7 @@ module.exports = {
comment.id = crs({ length: 10 })
comment.authorId = userId
comment.text = module.exports.formatCommentText(comment)
await Comments().insert(comment)
try {
@@ -106,6 +116,8 @@ module.exports = {
streamId,
parentComment: parentCommentId
}
comment.text = module.exports.formatCommentText(comment)
await Comments().insert(comment)
try {
const commentLink = { resourceId: parentCommentId, resourceType: 'comment' }
@@ -2,13 +2,12 @@
const expect = require('chai').expect
const crs = require('crypto-random-string')
const appRoot = require('app-root-path')
const { beforeEachContext } = require(`${appRoot}/test/hooks`)
const { createUser } = require(`${appRoot}/modules/core/services/users`)
const { createStream } = require(`${appRoot}/modules/core/services/streams`)
const { createCommitByBranchName } = require(`${appRoot}/modules/core/services/commits`)
const { beforeEachContext } = require('@/test/hooks')
const { createUser } = require('@/modules/core/services/users')
const { createStream } = require('@/modules/core/services/streams')
const { createCommitByBranchName } = require('@/modules/core/services/commits')
const { createObject } = require(`${appRoot}/modules/core/services/objects`)
const { createObject } = require('@/modules/core/services/objects')
const {
createComment,
getComments,
@@ -79,6 +78,22 @@ describe('Comments @comments', () => {
})
})
it('Should not accept complex HTML in the comment text', async () => {
const commentId = await createComment({
userId: user.id,
input: {
streamId: stream.id,
resources: [{ resourceId: stream.id, resourceType: 'stream' }],
text: 'Some <img src/onerror=alert(1)> <strong>epic</strong> <a href="javascript:alert(1)">cool</a> text!',
data: { justSome: crs({ length: 10 }) }
}
})
const comment = await getComment({ id: commentId })
expect(comment).to.be.ok
expect((comment.text.match(/alert/) || []).length).to.equal(0)
})
it('Should not be allowed to comment without specifying at least one target resource', async () => {
await createComment({
userId: user.id,
@@ -1,7 +1,6 @@
const { SchemaDirectiveVisitor } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
const appRoot = require('app-root-path')
const { validateServerRole } = require(`${appRoot}/modules/shared`)
const { validateServerRole } = require('@/modules/shared')
module.exports = {
hasRole: class HasRoleDirective extends SchemaDirectiveVisitor {
@@ -1,7 +1,6 @@
const { SchemaDirectiveVisitor } = require('apollo-server-express')
const { defaultFieldResolver } = require('graphql')
const appRoot = require('app-root-path')
const { validateScopes } = require(`${appRoot}/modules/shared`)
const { validateScopes } = require('@/modules/shared')
module.exports = {
hasScope: class HasScopeDirective extends SchemaDirectiveVisitor {
@@ -1,13 +1,12 @@
'use strict'
const appRoot = require('app-root-path')
const {
ForbiddenError,
UserInputError,
ApolloError,
withFilter
} = require('apollo-server-express')
const { authorizeResolver, pubsub } = require(`${appRoot}/modules/shared`)
const { authorizeResolver, pubsub } = require('@/modules/shared')
const {
createBranch,
@@ -19,7 +18,7 @@ const {
} = require('../../services/branches')
const { getUserById } = require('../../services/users')
const { saveActivity } = require(`${appRoot}/modules/activitystream/services`)
const { saveActivity } = require('@/modules/activitystream/services')
// subscription events
const BRANCH_CREATED = 'BRANCH_CREATED'
@@ -1,14 +1,13 @@
'use strict'
const appRoot = require('app-root-path')
const {
ForbiddenError,
UserInputError,
ApolloError,
withFilter
} = require('apollo-server-express')
const { authorizeResolver, pubsub } = require(`${appRoot}/modules/shared`)
const { saveActivity } = require(`${appRoot}/modules/activitystream/services`)
const { authorizeResolver, pubsub } = require('@/modules/shared')
const { saveActivity } = require('@/modules/activitystream/services')
const {
createCommitByBranchName,
@@ -1,10 +1,9 @@
'use strict'
const appRoot = require('app-root-path')
const {
validateServerRole,
validateScopes,
authorizeResolver
} = require(`${appRoot}/modules/shared`)
} = require('@/modules/shared')
const {
createObjects,
@@ -1,6 +1,5 @@
'use strict'
const appRoot = require('app-root-path')
const { validateServerRole, validateScopes } = require(`${appRoot}/modules/shared`)
const { validateServerRole, validateScopes } = require('@/modules/shared')
const {
updateServerInfo,
getServerInfo,
@@ -222,7 +222,8 @@ module.exports = {
streamId: args.stream.id,
name: args.stream.name,
description: args.stream.description,
isPublic: args.stream.isPublic
isPublic: args.stream.isPublic,
allowPublicComments: args.stream.allowPublicComments
}
await updateStream(update)
@@ -1,5 +1,4 @@
'use strict'
const appRoot = require('app-root-path')
const { UserInputError } = require('apollo-server-express')
const {
getUser,
@@ -15,7 +14,7 @@ const {
unmakeUserAdmin,
archiveUser
} = require('../../services/users')
const { saveActivity } = require(`${appRoot}/modules/activitystream/services`)
const { saveActivity } = require('@/modules/activitystream/services')
const { validateServerRole, validateScopes } = require(`@/modules/shared`)
const zxcvbn = require('zxcvbn')
@@ -24,6 +24,7 @@ type Stream {
name: String!
description: String
isPublic: Boolean!
allowPublicComments: Boolean!
"""
Your role for this stream. `null` if request is not authenticated, or the stream is not explicitly shared with you.
"""
@@ -164,6 +165,7 @@ input StreamUpdateInput {
name: String
description: String
isPublic: Boolean
allowPublicComments: Boolean
}
input StreamGrantPermissionInput {
@@ -31,7 +31,8 @@ const Scopes = Object.freeze({
},
Users: {
Read: 'users:read',
Email: 'users:email'
Email: 'users:email',
Invite: 'users:invite'
},
Server: {
Stats: 'server:stats',
+1 -5
View File
@@ -1,10 +1,6 @@
'use strict'
const debug = require('debug')
const appRoot = require('app-root-path')
const {
registerOrUpdateScope,
registerOrUpdateRole
} = require(`${appRoot}/modules/shared`)
const { registerOrUpdateScope, registerOrUpdateRole } = require('@/modules/shared')
exports.init = async (app) => {
debug('speckle:modules')('💥 Init core module')
@@ -5,8 +5,7 @@ https://speckle.community/t/error-in-grasshopper-while-receiving-data-you-dont-h
*/
exports.up = async (knex) => {
const appRoot = require('app-root-path')
const roles = require(`${appRoot}/modules/core/roles.js`)
const roles = require('@/modules/core/roles.js')
const Users = () => knex('users')
@@ -0,0 +1,12 @@
/* istanbul ignore file */
exports.up = async (knex) => {
await knex.schema.alterTable('streams', (table) => {
table.boolean('allowPublicComments').defaultTo(false)
})
}
exports.down = async (knex) => {
await knex.schema.alterTable('streams', (table) => {
table.dropColumn('allowPublicComments')
})
}
@@ -1,10 +1,9 @@
'use strict'
const appRoot = require('app-root-path')
const {
validateScopes,
validateServerRole,
authorizeResolver
} = require(`${appRoot}/modules/shared`)
} = require('@/modules/shared')
const { getStream } = require('../services/streams')
@@ -1,10 +1,9 @@
'use strict'
const zlib = require('zlib')
const debug = require('debug')
const appRoot = require('app-root-path')
const cors = require('cors')
const { contextMiddleware } = require(`${appRoot}/modules/shared`)
const { contextMiddleware } = require('@/modules/shared')
const { validatePermissionsReadStream } = require('./authUtils')
const { SpeckleObjectsStream } = require('./speckleObjectsStream')
const { getObjectsStream } = require('../services/objects')
@@ -2,9 +2,8 @@
const zlib = require('zlib')
const cors = require('cors')
const debug = require('debug')
const appRoot = require('app-root-path')
const { contextMiddleware } = require(`${appRoot}/modules/shared`)
const { contextMiddleware } = require('@/modules/shared')
const { validatePermissionsWriteStream } = require('./authUtils')
const { hasObjects } = require('../services/objects')
@@ -1,10 +1,9 @@
'use strict'
const zlib = require('zlib')
const debug = require('debug')
const appRoot = require('app-root-path')
const cors = require('cors')
const { contextMiddleware } = require(`${appRoot}/modules/shared`)
const { contextMiddleware } = require('@/modules/shared')
const { validatePermissionsReadStream } = require('./authUtils')
const { getObject, getObjectChildrenStream } = require('../services/objects')
+1 -2
View File
@@ -3,9 +3,8 @@ const zlib = require('zlib')
const cors = require('cors')
const Busboy = require('busboy')
const debug = require('debug')
const appRoot = require('app-root-path')
const { contextMiddleware } = require(`${appRoot}/modules/shared`)
const { contextMiddleware } = require('@/modules/shared')
const { validatePermissionsWriteStream } = require('./authUtils')
const { createObjectsBatched } = require('../services/objects')
@@ -1,7 +1,6 @@
'use strict'
const crs = require('crypto-random-string')
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const knex = require('@/db/knex')
const Streams = () => knex('streams')
const Branches = () => knex('branches')
@@ -1,7 +1,6 @@
'use strict'
const crs = require('crypto-random-string')
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const knex = require('@/db/knex')
const Streams = () => knex('streams')
const Commits = () => knex('commits')

Some files were not shown because too many files have changed in this diff Show More