Merge branch 'main' of github.com:specklesystems/speckle-server into user_admin_features

This commit is contained in:
Gergő Jedlicska
2021-10-11 13:00:02 +02:00
26 changed files with 17895 additions and 263 deletions
+17211 -12
View File
File diff suppressed because it is too large Load Diff
@@ -2,7 +2,10 @@
<v-card class="my-4 elevation-1" :loading="$apollo.loading">
<div v-if="!$apollo.loading && file">
<v-toolbar dense flat color="transparent">
<v-app-bar-nav-icon>
<v-app-bar-nav-icon
v-tooltip="`Download the original file`"
@click="downloadOriginalFile()"
>
<v-icon>mdi-download</v-icon>
</v-app-bar-nav-icon>
<v-toolbar-title>
@@ -104,6 +107,24 @@ export default {
mounted() {
this.$apollo.queries.file.startPolling(1000)
},
methods: {}
methods: {
async downloadOriginalFile() {
let res = await fetch(`/api/file/${this.fileId}`, {
headers: {
Authorization: localStorage.getItem('AuthToken')
}
})
let blob = await res.blob()
let file = window.URL.createObjectURL(blob)
let a = document.createElement('a')
document.body.appendChild(a)
a.style = 'display: none'
a.href = file
a.download = this.file.fileName
a.click()
window.URL.revokeObjectURL(file)
}
}
}
</script>
@@ -9,47 +9,37 @@
<span class="caption">{{ file.size }}kb</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn @click="upload()" color="primary">Upload</v-btn>
<v-menu offset-y>
<template #activator="{ attrs, on }">
<v-btn v-tooltip="`Change the branch to upload to`" text v-bind="attrs" v-on="on">
<v-icon small>mdi-source-branch</v-icon>
<span class="caption">{{ selectedBranch }}</span>
</v-btn>
</template>
<v-list>
<v-list-item
v-for="item in branches.filter((b) => b.name != 'globals')"
:key="item.name"
link
@click="selectedBranch = item.name"
>
<v-list-item-title class="caption">{{ item.name }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn color="primary" @click="upload()">Upload</v-btn>
</v-toolbar>
<v-alert v-if="error" type="error" dismissible>An error occurred.</v-alert>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: ['file'],
props: ['file', 'branches'],
data: () => ({
percentCompleted: -1,
error: null
error: null,
selectedBranch: 'main'
}),
apollo: {
streams: {
query: gql`
query Streams($query: String) {
streams(query: $query) {
totalCount
cursor
items {
id
name
updatedAt
}
}
}
`,
variables() {
return {
query: this.search
}
},
skip() {
return !this.search || this.search.length < 3
},
debounce: 300
}
},
watch: {},
methods: {
upload() {
let data = new FormData()
@@ -57,14 +47,19 @@ export default {
data.append('file', this.file)
let request = new XMLHttpRequest()
request.open('POST', `/api/file/ifc/${this.$route.params.streamId}`)
request.open(
'POST',
`/api/file/ifc/${this.$route.params.streamId}/${
this.selectedBranch ? this.selectedBranch : 'main'
}`
)
request.setRequestHeader('Authorization', `Bearer ${localStorage.getItem('AuthToken')}`)
request.upload.addEventListener(
'progress',
function (e) {
this.percentCompleted = (e.loaded / e.total) * 100
if (this.percentCompleted >= 100) {
if (this.percentCompleted >= 100) {
this.$emit('done', this.file.name)
}
}.bind(this)
+18 -4
View File
@@ -39,7 +39,11 @@
<v-dialog v-model="liff" max-width="400" :fullscreen="$vuetify.breakpoint.xsOnly">
<v-card>
<v-toolbar>
<v-toolbar-title>thanks for all the fish <v-icon>mdi-fish</v-icon><v-icon>mdi-arrow-up</v-icon></v-toolbar-title>
<v-toolbar-title>
thanks for all the fish
<v-icon>mdi-fish</v-icon>
<v-icon>mdi-arrow-up</v-icon>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn icon @click="liff = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
@@ -51,6 +55,12 @@
import gql from 'graphql-tag'
export default {
props: {
gotostreamonclick: {
type: Boolean,
default: true
}
},
data: () => ({
search: '',
liff: false,
@@ -85,12 +95,16 @@ export default {
},
watch: {
selectedSearchResult(val) {
this.search = ''
let myStream = this.streams.items.find((s) => s.id === val.id)
this.$emit('select', myStream)
this.streams.items = []
if (val) this.$router.push({ name: 'stream', params: { streamId: val.id } })
this.search = ''
if (val && this.gotostreamonclick)
this.$router.push({ name: 'stream', params: { stream: val } })
},
search(val) {
console.log(val)
if (val === '42') this.liff = true
}
},
@@ -69,6 +69,8 @@ export default {
showDelete: false,
nameRules: [
(v) => !!v || 'Branches need a name too!',
(v) =>
!(v.startsWith('#') || v.startsWith('/')) || 'Branch names cannot start with "#" or "/"',
(v) =>
(v && this.allBranchNames.findIndex((e) => e === v) === -1) ||
'A branch with this name already exists',
@@ -42,18 +42,17 @@ export default {
showError: false,
error: null,
streamId: null,
branchNames: ['main', 'globals'],
reservedBranchNames: ['main', 'globals'],
valid: false,
loading: false,
name: null,
nameRules: [
(v) => !!v || 'Branches need a name too!',
(v) =>
(v && !v.startsWith('globals')) ||
'Globals is a reserved branch name. Please choose a different name.',
!(v.startsWith('#') || v.startsWith('/')) || 'Branch names cannot start with "#" or "/"',
(v) =>
(v && this.branchNames.findIndex((e) => e === v) === -1) ||
'A branch with this name already exists',
(v && this.reservedBranchNames.findIndex((e) => e === v) === -1) ||
'This is a reserved branch name',
(v) => (v && v.length <= 100) || 'Name must be less than 100 characters',
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
],
@@ -61,6 +60,11 @@ export default {
}
},
computed: {},
watch: {
name(val) {
this.name = val.toLowerCase()
}
},
methods: {
show() {
this.showDialog = true
@@ -80,7 +84,7 @@ export default {
variables: {
params: {
streamId: this.$route.params.streamId,
name: this.name,
name: this.name.toLowerCase(),
description: this.description
}
}
@@ -90,7 +94,9 @@ export default {
this.loading = false
this.showDialog = false
this.$emit('refetch-branches')
this.$router.push(`/streams/${this.$route.params.streamId}/branches/${this.name}`)
this.$router.push(
`/streams/${this.$route.params.streamId}/branches/${this.name.toLowerCase()}`
)
} catch (err) {
this.showError = true
if (err.message.includes('branches_streamid_name_unique'))
+17 -5
View File
@@ -57,7 +57,7 @@ const routes = [
meta: {
title: 'Home | Speckle'
},
component: () => import('@/views/Frontend_re.vue'),
component: () => import('@/views/Frontend.vue'),
children: [
{
path: '',
@@ -80,7 +80,7 @@ const routes = [
meta: {
title: 'Stream | Speckle'
},
component: () => import('@/views/stream/Stream_re_re.vue'),
component: () => import('@/views/stream/Stream.vue'),
children: [
{
path: '',
@@ -88,7 +88,7 @@ const routes = [
meta: {
title: 'Stream | Speckle'
},
component: () => import('@/views/stream/Details_re.vue')
component: () => import('@/views/stream/Details.vue')
},
{
@@ -102,7 +102,14 @@ const routes = [
meta: {
title: 'Branch | Speckle'
},
component: () => import('@/views/stream/Branch.vue')
component: () => import('@/views/stream/Branch.vue'),
beforeEnter: (to, from, next) => {
if (to.params.branchName.toLowerCase() !== to.params.branchName)
return next(
`/streams/${to.params.streamId}/branches/${to.params.branchName.toLowerCase()}`
)
else next()
}
},
{
path: 'commits/:commitId',
@@ -228,6 +235,11 @@ const routes = [
name: 'Admin | Settings',
path: 'settings',
component: () => import('@/views/admin/AdminSettings.vue')
},
{
name: 'Admin | Invites',
path: 'invites',
component: () => import('@/views/admin/AdminInvites.vue')
}
]
}
@@ -263,7 +275,7 @@ const router = new VueRouter({
mode: 'history',
// base: process.env.BASE_URL,
routes,
scrollBehavior (to, from, savedPosition) {
scrollBehavior(to, from, savedPosition) {
return { x: 0, y: 0 }
}
})
@@ -251,18 +251,32 @@
<router-view></router-view>
</transition>
</v-main>
<v-snackbar
v-model="streamSnackbar"
rounded="pill"
:timeout="10000"
style="z-index: 10000"
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
>
<span v-if="streamSnackbarInfo.sharedBy">You have been granted access to a new stream!</span>
<span v-else>New stream created!</span>
<template #action="{ attrs }">
<v-btn color="white" text v-bind="attrs" @click="goToStreamAndCloseSnackbar()">View</v-btn>
<v-btn color="pink" icon v-bind="attrs" @click="streamSnackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</v-app>
</template>
<script>
import gql from 'graphql-tag'
import { signOut } from '@/auth-helpers'
import userQuery from '../graphql/user.gql'
import SearchBar from '../components/SearchBar'
import StreamInviteDialog from '../components/dialogs/StreamInviteDialog'
import UserAvatarIcon from '@/components/UserAvatarIcon'
export default {
components: { UserAvatarIcon, SearchBar, StreamInviteDialog },
components: { UserAvatarIcon },
data() {
return {
streamSnackbar: false,
@@ -298,6 +312,7 @@ export default {
}
`,
result(streamInfo) {
console.log(streamInfo)
if (!streamInfo.data.userStreamAdded) return
this.streamSnackbar = true
this.streamSnackbarInfo = streamInfo.data.userStreamAdded
@@ -321,7 +336,10 @@ export default {
}
},
watch: {
$route(to, from) {
$route(to) {
this.bottomSheet = false
// close the snackbar if it's a stream create event in this window
if (to.params.streamId === this.streamSnackbarInfo.id) this.streamSnackbar = false
this.bottomSheet = false
}
},
@@ -335,6 +353,10 @@ export default {
},
showStreamInviteDialog() {
this.$refs.streamInviteDialog.show()
},
goToStreamAndCloseSnackbar() {
this.streamSnackbar = false
this.$router.push(`/streams/${this.streamSnackbarInfo.id}`)
}
}
}
+7 -11
View File
@@ -7,10 +7,10 @@
pr-0
>
<v-navigation-drawer
v-model="streamNav"
app
fixed
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
v-model="streamNav"
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
>
<main-nav-actions :open-new-stream="newStreamDialog" />
@@ -51,12 +51,12 @@
<v-subheader class="ml-2">Your latest commits:</v-subheader>
<v-list-item
v-for="(commit, i) in userCommits.commits.items"
v-if="commit"
:key="i"
v-tooltip="`In stream '${commit.streamName}'`"
:to="`streams/${commit.streamId}/${
commit.branchName === 'globals' ? 'globals' : 'commits'
}/${commit.id}`"
v-if="commit"
v-tooltip="`In stream '${commit.streamName}'`"
>
<v-list-item-content>
<v-list-item-title>
@@ -84,12 +84,8 @@
Streams
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items style="margin-right: -18px" v-if="$vuetify.breakpoint.smAndDown">
<v-btn
color="primary"
depressed
@click="newStreamDialog++"
>
<v-toolbar-items v-if="$vuetify.breakpoint.smAndDown" style="margin-right: -18px">
<v-btn color="primary" depressed @click="newStreamDialog++">
<v-icon>mdi-plus-box</v-icon>
</v-btn>
</v-toolbar-items>
@@ -107,7 +103,7 @@
<div v-if="$apollo.loading" class="my-5"></div>
</v-col>
<v-col cols="12" v-else-if="streams && streams.items && streams.items.length > 0">
<v-col v-else-if="streams && streams.items && streams.items.length > 0" cols="12">
<v-row :class="`${$vuetify.breakpoint.xsOnly ? '' : 'pl-2'}`">
<v-col
v-for="(stream, i) in streams.items"
@@ -137,7 +133,7 @@
here.
</p>
<template v-slot:actions>
<template #actions>
<v-list rounded class="transparent">
<v-list-item link class="primary mb-4" dark @click="newStreamDialog++">
<v-list-item-icon>
+50 -32
View File
@@ -1,11 +1,16 @@
<template>
<v-container :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" fluid pt-4 pr-0>
<v-container
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
fluid
pt-4
pr-0
>
<v-navigation-drawer
v-model="activityNav"
app
fixed
:permanent="activityNav && !$vuetify.breakpoint.smAndDown"
v-model="activityNav"
:style="`${ !$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
>
<main-nav-actions :open-new-stream="newStreamDialog" />
@@ -13,9 +18,9 @@
<v-subheader class="mt-3 ml-2">Recently updated streams</v-subheader>
<v-list-item
v-for="(s, i) in streams.items"
v-if="streams.items"
:key="i"
:to="'streams/' + s.id"
v-if="streams.items"
>
<v-list-item-content>
<v-list-item-title>
@@ -32,16 +37,14 @@
</v-list>
</v-navigation-drawer>
<v-app-bar app :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat>
<v-app-bar-nav-icon
@click="activityNav = !activityNav"
></v-app-bar-nav-icon>
<v-app-bar app :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat>
<v-app-bar-nav-icon @click="activityNav = !activityNav"></v-app-bar-nav-icon>
<v-toolbar-title class="space-grotesk pl-0">
<v-icon class="hidden-xs-only">mdi-clock-fast</v-icon>
Feed
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items style="margin-right: -18px;" v-if="$vuetify.breakpoint.smAndDown ">
<v-toolbar-items v-if="$vuetify.breakpoint.smAndDown" style="margin-right: -18px">
<v-btn color="primary" depressed @click="newStreamDialog++">
<v-icon>mdi-plus-box</v-icon>
</v-btn>
@@ -59,7 +62,13 @@
</div>
</v-col>
<v-col cols="12" lg="8" v-else-if="timeline && timeline.items.length > 0" class="pr-2" :style="`${$vuetify.breakpoint.xsOnly ? 'margin-left: -20px;' :''}`">
<v-col
v-else-if="timeline && timeline.items.length > 0"
cols="12"
lg="8"
class="pr-2"
:style="`${$vuetify.breakpoint.xsOnly ? 'margin-left: -20px;' : ''}`"
>
<div>
<div v-if="timeline" key="activity-list">
<v-timeline align-top dense>
@@ -83,20 +92,15 @@
</v-col>
<v-col v-else cols="12">
<no-data-placeholder v-if="quickUser">
<h2>Welcome {{quickUser.name.split(' ')[0]}}!</h2>
<h2>Welcome {{ quickUser.name.split(' ')[0] }}!</h2>
<p class="caption">
Once you will create a stream and start sending some data, your activity will show up
here.
</p>
<template v-slot:actions>
<template #actions>
<v-list rounded class="transparent">
<v-list-item
link
class="primary mb-4"
dark
@click="newStreamDialog++"
>
<v-list-item link class="primary mb-4" dark @click="newStreamDialog++">
<v-list-item-icon>
<v-icon>mdi-plus-box</v-icon>
</v-list-item-icon>
@@ -113,11 +117,11 @@
</v-col>
<v-col
v-show="$vuetify.breakpoint.lgAndUp"
v-if="timeline && timeline.items.length > 0"
cols="12"
lg="4"
v-show="$vuetify.breakpoint.lgAndUp"
class="mt-7"
v-if="timeline && timeline.items.length > 0"
>
<latest-blogposts></latest-blogposts>
<v-card rounded="lg" class="mt-2">
@@ -170,18 +174,16 @@ export default {
activityNav: true
}
},
computed: {},
mounted() {
setTimeout(
function () {
this.activityNav = !this.$vuetify.breakpoint.smAndDown
}.bind(this),
10
)
},
apollo: {
quickUser: {
query: gql`query { quickUser: user { id name } } `
query: gql`
query {
quickUser: user {
id
name
}
}
`
},
timeline: {
query: gql`
@@ -217,7 +219,7 @@ export default {
prefetch: true,
query: gql`
query {
streams (limit: 10) {
streams(limit: 10) {
items {
id
name
@@ -228,9 +230,25 @@ export default {
`
}
},
computed: {},
watch: {
timeline(val) {
if (val.totalCount === 0 && !localStorage.getItem('onboarding')) {
this.$router.push('/onboarding')
}
}
},
mounted() {
setTimeout(
function () {
this.activityNav = !this.$vuetify.breakpoint.smAndDown
}.bind(this),
10
)
},
methods: {
groupSimilarActivities(data) {
if(!data) return
if (!data) return
let groupedTimeline = data.user.timeline.items.reduce(function (prev, curr) {
//first item
if (!prev.length) {
+25 -13
View File
@@ -1,16 +1,15 @@
<template lang="html">
<v-container v-if="$apollo.loading" fluid>
<v-skeleton-loader type="article"></v-skeleton-loader>
</v-container>
<v-container v-else-if="isAdmin" :class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`">
<v-navigation-drawer
v-model="adminNav"
app
fixed
:permanent="adminNav && !$vuetify.breakpoint.smAndDown"
v-model="adminNav"
:style="`${ !$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
>
<v-app-bar style="position: absolute; top: 0; width: 100%; z-index: 90" elevation="0">
<v-toolbar-title>Server Admin</v-toolbar-title>
@@ -50,29 +49,44 @@
<v-list-item-subtitle class="caption">Edit server user details.</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item link to="/admin/invites">
<v-list-item-icon>
<v-icon small class="mt-1">mdi-account-multiple-plus-outline</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Server invites</v-list-item-title>
<v-list-item-subtitle class="caption">Manage server invitations.</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
<v-app-bar app :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" flat v-if="!adminNav">
<v-app-bar-nav-icon @click="adminNav = !adminNav" v-if="!adminNav"/>
<v-toolbar-title v-if="!adminNav">
Server Admin
</v-toolbar-title>
<v-app-bar
v-if="!adminNav"
app
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
flat
>
<v-app-bar-nav-icon v-if="!adminNav" @click="adminNav = !adminNav" />
<v-toolbar-title v-if="!adminNav">Server Admin</v-toolbar-title>
</v-app-bar>
<v-container :style="`${ !$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`" fluid pt-4 pr-0>
<v-container
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''} max-width: 1024px;`"
fluid
pt-4
pr-0
>
<transition name="fade">
<router-view></router-view>
</transition>
</v-container>
</v-container>
<v-container v-else-if="!isAdmin">
<error-placeholder error-type="access">
<h2>Only server admins have access to this section.</h2>
</error-placeholder>
</v-container>
</template>
<script>
@@ -108,5 +122,3 @@ export default {
}
}
</script>
<style lang="scss"></style>
@@ -0,0 +1,233 @@
<template>
<v-card>
<v-toolbar flat>
<v-toolbar-title>Send invites to multiple adresses</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert v-model="success" prominent timeout="3000" dismissible type="success">
Great! All invites were sent.
</v-alert>
<v-alert v-show="errors.length !== 0" prominent dismissible type="error">
<p>Invite send failed for adresses:</p>
<ul>
<li v-for="error in errors" :key="error.email">{{ error.email }}: {{ error.reason }}</li>
</ul>
</v-alert>
<v-alert
v-show="errors.length !== 0 && sentToEmails.length !== 0"
prominent
timeout="3000"
dismissible
type="success"
>
<p>Invite sent to: {{ sentToEmails.join(', ') }}</p>
</v-alert>
<v-form v-model="valid" @submit.prevent="submit">
<v-textarea
v-model="invitation"
label="Invitation message"
rounded
filled
auto-grow
:rules="validation.messageRules"
></v-textarea>
<v-combobox
v-model="chips"
:search-input.sync="emails"
placeholder="Type emails separated by commas or paste the content of a .csv"
deletable-chips
append-icon=""
filled
rounded
flat
type="email"
class="lighten-2"
:error-messages="inputErrors"
multiple
append-outer-icon="mdi-close"
@keydown="keyDownHandler"
@blur="validateAndCreateChips"
@paste="validateAndCreateChips"
@click:append-outer="chips = []"
>
<template #selection="data">
<v-chip
v-if="data.item"
:input-value="data.selected"
close
@click:close="remove(data.item)"
>
{{ data.item }}
</v-chip>
</template>
</v-combobox>
<p v-if="!selectedStream">Optionaly invite users to stream.</p>
<stream-search-bar
v-if="!selectedStream"
:gotostreamonclick="false"
class="py-3"
@select="setStream"
/>
<v-alert v-else text dense type="info" dismissible @input="dismiss">
They will be invited to be collaborators on {{ selectedStream.name }} stream.
</v-alert>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn :disabled="!submitable" color="primary" type="submit">Invite</v-btn>
</v-card-actions>
</v-form>
</v-card-text>
<v-overlay absolute :value="submitting">
<v-progress-circular :width="1.5" indeterminate></v-progress-circular>
</v-overlay>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
import DOMPurify from 'dompurify'
import StreamSearchBar from '@/components/SearchBar'
export default {
components: { StreamSearchBar },
data() {
return {
valid: false,
success: false,
showError: false,
errors: [],
sentToEmails: [],
submitting: false,
invitation: '',
emails: '',
chips: [],
inputErrors: [],
selectedStream: null,
validation: {
messageRules: [
(v) => {
if (v.length >= 1024) return 'Message too long!'
return true
},
(v) => {
let pure = DOMPurify.sanitize(v)
if (pure !== v) return 'No crazy hacks please.'
else return true
}
]
}
}
},
computed: {
submitable() {
return this.chips && this.chips.length !== 0
}
},
apollo: {
user: {
query: gql`
query {
user {
id
name
}
}
`,
prefetch: true
},
serverInfo: {
query: gql`
query {
serverInfo {
name
canonicalUrl
}
}
`
}
},
methods: {
setStream(stream) {
this.selectedStream = stream
},
dismiss() {
this.selectedStream = null
},
remove(item) {
this.chips.splice(this.chips.indexOf(item), 1)
},
validateEmail(email) {
const re = /^\S+@\S+\.\S+$/
return re.test(email)
},
keyDownHandler(val) {
if (!(val.key === ' ' || val.key === ',' || val.key === 'Enter')) return
this.validateAndCreateChips()
},
validateAndCreateChips() {
this.inputErrors = []
if (!this.emails || this.emails === '') return
let splitEmails = this.emails.split(/[ ,]+/)
for (let email of splitEmails) {
let valid = this.validateEmail(email) && this.chips.indexOf(email) === -1
if (valid) {
this.chips.push(email)
} else {
this.inputErrors.push('Invalid email')
}
}
this.emails = ''
},
createInviteMessage() {
let message =
`You have been invited to a Speckle server: ${this.serverInfo.name} ` +
`by ${this.user.name}. Visit ${this.serverInfo.canonicalUrl} to register.`
return this.invitation || message
},
async submit() {
this.submitting = true
this.errors = []
this.sentToEmails = []
for (let chip of this.chips) {
if (!chip || chip.length === 0) continue
try {
await this.sendInvite(chip, this.createInviteMessage(), this.selectedStream?.id)
this.sentToEmails.push(chip)
} catch (err) {
this.errors.push({ email: chip, reason: err.graphQLErrors[0].message })
}
}
this.submitting = false
if (this.errors.length === 0) {
this.success = true
this.chips = []
this.dismiss()
}
},
async sendInvite(email, message, streamId) {
let input = {
email: email,
message: message
}
let query = gql`
mutation($input: ${streamId ? 'StreamInviteCreateInput!' : 'ServerInviteCreateInput!'}) {
${streamId ? 'streamInviteCreate' : 'serverInviteCreate'}(input: $input)
}
`
if (streamId) {
input.streamId = streamId
}
await this.$apollo.mutate({
mutation: query,
variables: {
input: input
}
})
this.$matomo && this.$matomo.trackEvent('invite', 'server')
}
}
}
</script>
@@ -447,6 +447,27 @@
:stream-id="$route.params.streamId"
:stream-name="stream.name"
/>
<v-snackbar
v-model="snackbar"
rounded="pill"
:timeout="10000"
style="z-index: 10000"
:color="`${$vuetify.theme.dark ? 'primary' : 'primary'}`"
>
<template v-if="snackbarInfo.type === 'commit'">
<span>New commit created!</span>
</template>
<template v-if="snackbarInfo.type === 'branch'">
<span>Branch "{{ snackbarInfo.name }}" created!</span>
</template>
<template #action="{ attrs }">
<v-btn color="white" text v-bind="attrs" @click="goToItemAndCloseSnackbar()">View</v-btn>
<v-btn color="pink" icon v-bind="attrs" @click="snackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
@@ -465,8 +486,8 @@ export default {
return {
streamNav: true,
error: '',
commitSnackbar: false,
commitSnackbarInfo: {},
snackbar: false,
snackbarInfo: {},
editStreamDialog: false,
shareStream: false,
branchMenuOpen: false,
@@ -533,6 +554,26 @@ export default {
}
},
$subscribe: {
branchCreated: {
query: gql`
subscription($streamId: String!) {
branchCreated(streamId: $streamId)
}
`,
variables() {
return {
streamId: this.$route.params.streamId
}
},
result(args) {
if (!args.data.branchCreated) return
this.snackbar = true
this.snackbarInfo = { ...args.data.branchCreated, type: 'branch' }
},
skip() {
return !this.loggedIn
}
},
commitCreated: {
query: gql`
subscription($streamId: String!) {
@@ -546,8 +587,9 @@ export default {
},
result(commitInfo) {
if (!commitInfo.data.commitCreated) return
this.commitSnackbar = true
this.commitSnackbarInfo = commitInfo.data.commitCreated
console.log(commitInfo)
this.snackbar = true
this.snackbarInfo = { ...commitInfo.data.commitCreated, type: 'commit' }
},
skip() {
return !this.loggedIn
@@ -589,12 +631,13 @@ export default {
}
},
watch: {
$route(to, from) {
$route() {
// Ensures branch menu is open when navigating to a branch url
if (this.$route.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
this.branchMenuOpen = true
// closes any share dialog
this.shareStream = false
this.snackbar = false
}
},
mounted() {
@@ -615,6 +658,17 @@ export default {
}
},
methods: {
goToItemAndCloseSnackbar() {
if (this.snackbarInfo.type === 'commit') {
this.$router.push(`/streams/${this.$route.params.streamId}/commits/${this.snackbarInfo.id}`)
} else if (this.snackbarInfo.type === 'branch') {
this.$router.push(
`/streams/${this.$route.params.streamId}/branches/${this.snackbarInfo.name}`
)
this.refetchBranches()
}
this.snackbar = false
},
copyToClipboard(e) {
e.target.select()
document.execCommand('copy')
+79 -96
View File
@@ -1,47 +1,5 @@
<template>
<div>
<no-data-placeholder v-if="false" :show-image="false">
<h2>Import IFC Files</h2>
<p class="caption">
Speckle can now process IFC files and store them as a commit (snapshot). You can then access
it from the Speckle API, and receive it in other applications.
</p>
<template #actions>
<v-list rounded class="transparent">
<v-list-item link class="primary mb-4" dark @click="showUploadDialog = true">
<v-list-item-icon>
<v-icon>mdi-plus-box</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Upload IFC File</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item
link
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
href="https://speckle.guide/dev/server-webhooks.html"
target="_blank"
>
<v-list-item-icon>
<v-icon>mdi-book-open-variant</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Release Announcement</v-list-item-title>
<v-list-item-subtitle class="caption"></v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list>
</template>
</no-data-placeholder>
<error-placeholder v-if="error" error-type="access">
<h2>Only stream owners can access webhooks.</h2>
<p class="caption">
If you need to use webhooks, ask the stream's owner to grant you ownership.
</p>
</error-placeholder>
<v-container style="max-width: 768px">
<portal to="streamTitleBar">
<div>
@@ -71,63 +29,70 @@
</v-card-text>
</v-card>
<v-card
elevation="0"
color="transparent"
class=""
style="height: 220px; transition: all 0.2s ease"
:class="`mt-4 mb-4 d-flex justify-center
<v-alert v-if="stream && (stream.role === 'stream:reviewer' || !stream.role)" type="warning">
Your permission level ({{ stream.role ? stream.role : 'none' }}) is not high enough to
access this feature.
</v-alert>
<div v-if="stream && !(stream.role === 'stream:reviewer' || !stream.role)">
<v-card
elevation="0"
color="transparent"
class=""
style="height: 220px; transition: all 0.2s ease"
:class="`mt-4 mb-4 d-flex justify-center
${dragover && !$vuetify.theme.dark ? 'grey lighten-4' : ''}
${dragover && $vuetify.theme.dark ? 'grey darken-4' : ''}
`"
@drop.prevent="onFileDrop($event)"
@dragover.prevent="dragover = true"
@dragenter.prevent="dragover = true"
@dragleave.prevent="dragover = false"
>
<div v-if="!dragError" class="align-self-center text-center">
<input
id="myid"
type="file"
accept=".ifc,.IFC"
style="display: none"
multiple
@change="onFileSelect($event)"
/>
<v-icon
x-large
color="primary"
:class="`hover-tada ${dragover ? 'tada' : ''}`"
style="cursor: pointer"
onclick="document.getElementById('myid').click()"
>
mdi-cloud-upload
</v-icon>
<br />
<span class="primary--text">Drag and drop your IFC file here!</span>
<br />
<span class="caption">Maximum 5 files at a time. Size is restricted to 50mb each.</span>
</div>
<v-alert
v-if="dragError"
dismissible
class="align-self-center text-center"
type="error"
@click="dragError = null"
@drop.prevent="onFileDrop($event)"
@dragover.prevent="dragover = true"
@dragenter.prevent="dragover = true"
@dragleave.prevent="dragover = false"
>
{{ dragError }}
</v-alert>
</v-card>
<!-- {{ uploads }} -->
<template v-for="file in files">
<file-upload-item
:key="file.fileName"
:file="file"
@done="uploadCompleted"
></file-upload-item>
</template>
<div v-if="!dragError" class="align-self-center text-center">
<input
id="myid"
type="file"
accept=".ifc,.IFC"
style="display: none"
multiple
@change="onFileSelect($event)"
/>
<v-icon
x-large
color="primary"
:class="`hover-tada ${dragover ? 'tada' : ''}`"
style="cursor: pointer"
onclick="document.getElementById('myid').click()"
>
mdi-cloud-upload
</v-icon>
<br />
<span class="primary--text">Drag and drop your IFC file here!</span>
<br />
<span class="caption">Maximum 5 files at a time. Size is restricted to 50mb each.</span>
</div>
<v-alert
v-if="dragError"
dismissible
class="align-self-center text-center"
type="error"
@click="dragError = null"
>
{{ dragError }}
</v-alert>
</v-card>
<!-- {{ uploads }} -->
<template v-for="file in files">
<file-upload-item
:key="file.fileName"
:file="file"
:branches="stream.branches.items"
@done="uploadCompleted"
></file-upload-item>
</template>
</div>
<v-card elevation="1" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'white' : ''} mb-2`">
<v-toolbar-title>
@@ -161,17 +126,35 @@ import gql from 'graphql-tag'
export default {
name: 'Webhooks',
components: {
NoDataPlaceholder: () => import('@/components/NoDataPlaceholder'),
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder'),
FileUploadItem: () => import('@/components/FileUploadItem'),
FileProcessingItem: () => import('@/components/FileProcessingItem')
},
apollo: {
stream: {
query: gql`
query stream($id: String!) {
stream(id: $id) {
id
role
branches {
totalCount
items {
name
}
}
}
}
`,
variables() {
return { id: this.$route.params.streamId }
}
},
streamUploads: {
query: gql`
query streamUploads($streamId: String!) {
stream(id: $streamId) {
id
role
fileUploads {
id
}
+4
View File
@@ -1,4 +1,7 @@
module.exports = {
configureWebpack: {
devtool: 'source-map'
},
productionSourceMap: false,
pages: {
app: {
@@ -16,6 +19,7 @@ module.exports = {
},
devServer: {
host: 'localhost',
proxy: 'http://localhost:3000',
historyApiFallback: {
rewrites: [
{ from: /^\/$/, to: '/app.html' },
@@ -1,9 +1,8 @@
'use strict'
const passport = require( 'passport' )
const URL = require( 'url' ).URL
const appRoot = require( 'app-root-path' )
const debug = require( 'debug' )
const { createUser, updateUser, findOrCreateUser, validatePasssword, getUserByEmail } = require( `${appRoot}/modules/core/services/users` )
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` )
@@ -40,7 +39,6 @@ module.exports = async ( app, session, sessionAppId, finalizeAuth ) => {
app.post( '/auth/local/register', session, sessionAppId, async ( req, res, next ) => {
const serverInfo = await getServerInfo()
try {
if ( !req.body.password )
throw new Error( 'Password missing' )
@@ -62,7 +60,6 @@ module.exports = async ( app, session, sessionAppId, finalizeAuth ) => {
req.user = { id: userId, email: user.email }
return next( )
} catch ( err ) {
debug( 'speckle:errors' )( err )
return res.status( 400 ).send( { err: err.message } )
@@ -18,6 +18,8 @@ module.exports = {
branch.name = name.toLowerCase( )
branch.description = description
if(name) module.exports.validateBranchName( { name } )
let [ id ] = await Branches( ).returning( 'id' ).insert( branch )
// update stream updated at
@@ -27,9 +29,14 @@ module.exports = {
},
async updateBranch( { id, name, description } ) {
if ( name ) module.exports.validateBranchName( { name } )
return await Branches( ).where( { id: id } ).update( { name: name ? name.toLowerCase( ) : name, description: description } )
},
validateBranchName( { name } ) {
if ( name.startsWith( '/' ) || name.startsWith( '#' ) ) throw new Error( 'Branch names cannot start with # or /.' )
},
async getBranchById( { id } ) {
return await Branches( ).where( { id: id } ).first( ).select( '*' )
},
@@ -54,7 +61,7 @@ module.exports = {
},
async getBranchByNameAndStreamId( { streamId, name } ) {
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [name]) ).first( )
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [ name.toLowerCase() ] ) ).first( )
return await query
},
@@ -29,6 +29,7 @@ module.exports = {
async createUser( user ) {
user.id = crs( { length: 10 } )
user.email = user.email.toLowerCase()
if ( user.password ) {
if ( user.password.length < 8 ) throw new Error( 'Password to short; needs to be 8 characters or longer.' )
@@ -93,7 +94,7 @@ module.exports = {
},
async getUserByEmail( { email } ) {
let user = await Users( ).where( { email: email } ).select( '*' ).first( )
let user = await Users( ).where( { email: email.toLowerCase() } ).select( '*' ).first( )
if ( !user ) return null
delete user.passwordDigest
return user
@@ -138,7 +139,7 @@ module.exports = {
},
async validatePasssword( { email, password } ) {
let { passwordDigest } = await Users( ).where( { email: email } ).select( 'passwordDigest' ).first( )
let { passwordDigest } = await Users( ).where( { email: email.toLowerCase() } ).select( 'passwordDigest' ).first( )
return bcrypt.compare( password, passwordDigest )
},
@@ -24,7 +24,6 @@ const {
} = require( '../services/branches' )
describe( 'Branches @core-branches', ( ) => {
let user = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie4342@gmail.com',
@@ -73,6 +72,52 @@ describe( 'Branches @core-branches', ( ) => {
}
} )
it( 'Should not allow branch names starting with # or /', async ( ) => {
try {
await createBranch( { name: '/pasta', streamId: stream.id, authorId: user.id } )
assert.fail( 'Illegal branch name passed through.' )
} catch ( err ) {
expect( err.message ).to.contain( 'names cannot start with # or /' )
}
try {
await createBranch( { name: '#rice', streamId: stream.id, authorId: user.id } )
assert.fail( 'Illegal branch name passed through.' )
} catch ( err ) {
expect( err.message ).to.contain( 'names cannot start with # or /' )
}
try {
await updateBranch( { id: branch.id, name: '/super/part/two' } )
assert.fail( 'Illegal branch name passed through in update operation.' )
} catch ( err ) {
expect( err.message ).to.contain( 'names cannot start with # or /' )
}
try {
await updateBranch( { id: branch.id, name: '#super#part#three' } )
assert.fail( 'Illegal branch name passed through in update operation.' )
} catch ( err ) {
expect( err.message ).to.contain( 'names cannot start with # or /' )
}
} )
it( 'Branch names should be case insensitive (always lowercase)', async ( ) => {
let id = await createBranch( { name: 'CaseSensitive', streamId: stream.id, authorId: user.id } )
let b = await getBranchByNameAndStreamId( { streamId: stream.id, name:'casesensitive' } )
expect( b.name ).to.equal( 'casesensitive' )
let bb = await getBranchByNameAndStreamId( { streamId: stream.id, name:'CaseSensitive' } )
expect( bb.name ).to.equal( 'casesensitive' )
let bbb = await getBranchByNameAndStreamId( { streamId: stream.id, name:'CASESENSITIVE' } )
expect( bbb.name ).to.equal( 'casesensitive' )
// cleanup
await deleteBranchById( { id, streamId: stream.id } )
} )
it( 'Should get a branch', async ( ) => {
let myBranch = await getBranchById( { id: branch.id } )
expect( myBranch.authorId ).to.equal( user.id )
@@ -87,7 +132,6 @@ describe( 'Branches @core-branches', ( ) => {
} )
it( 'Should get all stream branches', async ( ) => {
await createBranch( { name: 'main-faster', streamId: stream.id, authorId: user.id } )
await createBranch( { name: 'main-blaster', streamId: stream.id, authorId: user.id } )
await createBranch( { name: 'blaster-farter', streamId: stream.id, authorId: user.id } )
@@ -112,7 +156,5 @@ describe( 'Branches @core-branches', ( ) => {
} catch ( e ){
// pass
}
} )
} )
@@ -12,7 +12,7 @@ chai.use( chaiHttp )
const knex = require( `${appRoot}/db/knex` )
const { createUser, findOrCreateUser, getUser, getUsers, searchUsers, countUsers, updateUser, deleteUser, validatePasssword, updateUserPassword, getUserRole, unmakeUserAdmin, makeUserAdmin } = require( '../services/users' )
const { createUser, findOrCreateUser, getUser, getUserByEmail, getUsers, searchUsers, countUsers, updateUser, deleteUser, validatePasssword, updateUserPassword, getUserRole, unmakeUserAdmin, makeUserAdmin } = require( '../services/users' )
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' )
const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' )
@@ -68,6 +68,24 @@ describe( 'Actors & Tokens @user-services', ( ) => {
expect( actorId ).to.be.a( 'string' )
} )
it( 'Should store user email lowercase', async ( ) => {
let user = { name: 'Marty McFly', email: 'Marty@Mc.Fly', password: 'something_future_proof' }
let userId = await createUser( user )
let storedUser = await getUser( userId )
expect( storedUser.email ).to.equal( user.email.toLowerCase() )
} )
it ( 'Get user by should ignore email casing', async ( ) => {
let user = await getUserByEmail( { email: 'BiLL@GaTES.cOm' } )
expect( user.email ).to.equal( 'bill@gates.com' )
} )
it ( 'Validate password should ignore email casing', async ( ) => {
expect( await validatePasssword( { email: 'BiLL@GaTES.cOm', password: 'testthebest' } ) )
} )
it( 'Should not create a user with a too small password', async () => {
try {
await createUser( { name: 'Dim Sum', email: 'dim@gmail.com', password: '1234567' } )
+3 -1
View File
@@ -53,7 +53,9 @@ exports.init = async ( app, options ) => {
if ( process.env.DISABLE_FILE_UPLOADS ) {
return res.status( 503 ).send( 'File uploads are disabled on this server' )
}
let fileInfo = await getFileInfo( { fileId: req.params.fileId } )
if ( !fileInfo )
return res.status( 404 ).send( 'File not found' )
@@ -85,7 +87,7 @@ exports.init = async ( app, options ) => {
let fileStream = await getFileStream( { fileId: req.params.fileId } )
res.writeHead( 200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': 'attachment' } )
res.writeHead( 200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${fileInfo.fileName}"`, } )
fileStream.pipe( res )
} ),
@@ -12,7 +12,7 @@ exports.up = async knex => {
table.integer( 'fileSize' )
table.boolean( 'uploadComplete' ).notNullable( ).defaultTo( false )
table.timestamp( 'uploadDate' ).notNullable( ).defaultTo( knex.fn.now( ) )
// 0 = queued, 1 = in progress, 2 = success, 3 = error
table.integer( 'convertedStatus' ).notNullable( ).defaultTo( 0 )
table.timestamp( 'convertedLastUpdate' ).notNullable( ).defaultTo( knex.fn.now( ) )
table.string( 'convertedMessage' )
@@ -16,6 +16,7 @@ const Invites = () => knex( 'server_invites' )
module.exports = {
async createAndSendInvite( { email, inviterId, message, resourceTarget, resourceId, role } ) {
// check if email is already registered as a user
email = email.toLowerCase()
let existingUser = await getUserByEmail( { email } )
if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' )
@@ -104,12 +105,12 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
},
async getInviteByEmail( { email } ) {
return await Invites().where( { email: email } ).select( '*' ).first()
return await Invites().where( { email: email.toLowerCase() } ).select( '*' ).first()
},
async validateInvite( { email, id } ) {
const invite = await module.exports.getInviteById( { id } )
return invite && invite.email === email && !invite.used
return invite && invite.email === email.toLowerCase() && !invite.used
},
async useInvite( { id, email } ) {
@@ -119,7 +120,7 @@ This email was sent from ${serverInfo.name} at ${process.env.CANONICAL_URL}, dep
let invite = await module.exports.getInviteById( { id } )
if ( !invite ) throw new Error( 'Invite not found' )
if ( invite.used ) throw new Error( 'Invite has been used' )
if ( invite.email !== email ) throw new Error( 'Invite email mismatch. Please use the original email the invite was sent to register.' )
if ( invite.email !== email.toLowerCase() ) throw new Error( 'Invite email mismatch. Please use the original email the invite was sent to register.' )
if ( invite.resourceId && invite.resourceTarget && invite.role ) {
let user = await getUserByEmail( { email: invite.email } )
@@ -19,7 +19,6 @@ const { createPersonalAccessToken } = require( `${appRoot}/modules/core/services
const serverAddress = 'http://localhost:3300'
describe( 'Server Invites @server-invites', ( ) => {
let myApp
describe( 'Services @server-invites-services', () => {
@@ -43,15 +42,16 @@ describe( 'Server Invites @server-invites', ( ) => {
} )
it( 'should create an invite', async() => {
let inviteId = await createAndSendInvite( { email:'didimitrie@gmail.com', inviterId: actor.id, message: 'Hey, join!' } )
expect( inviteId ).to.be.a( 'string' )
} )
it( 'should store invited email as lowercase', async() => {
let inviteId = await createAndSendInvite( { email:'GerGO@gmaIl.com', inviterId: actor.id, message: 'Hey, join!' } )
expect( inviteId ).to.be.a( 'string' )
} )
it( 'should not allow multiple invites for the same email', async() => {
let inviteId = await createAndSendInvite( { email:'cat@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
try {
@@ -61,9 +61,11 @@ describe( 'Server Invites @server-invites', ( ) => {
}
assert.fail( 'should not allow multiple invites for the same email' )
} )
it( 'low multiple invites for the same email regardles of casing', () => {
return createAndSendInvite( { email:'dIdImItrIe@gmaIl.com', inviterId: actor.id, message: 'Hey, join!' } ).then( ( result ) => {} ).catch( ( result ) => { expect( result.message ).to.equal( 'Already invited!' ) } )
} )
it( 'should not allow self invites', async() => {
try {
await createAndSendInvite( { email: 'didimitrie-100@gmail.com', inviterId: actor.id } )
} catch ( e ) {
@@ -73,7 +75,6 @@ describe( 'Server Invites @server-invites', ( ) => {
} )
it( 'should not allow invites from no user', async() => {
try {
await createAndSendInvite( { email: 'didimitrie233-100@gmail.com', inviterId: 'fake' } )
} catch ( e ) {
@@ -83,7 +84,6 @@ describe( 'Server Invites @server-invites', ( ) => {
} )
it( 'should not allow invites with a too long message', async() => {
try {
let inviteId = await createAndSendInvite( {
email: '123456@gmail.com',
@@ -111,7 +111,6 @@ describe( 'Server Invites @server-invites', ( ) => {
expect( invite.email ).to.equal( 'badger@speckle.systems' )
expect( invite.used ).to.equal( false )
expect( invite.inviterId ).to.equal( actor.id )
} )
it( 'should get an invite by email', async() => {
@@ -127,7 +126,7 @@ describe( 'Server Invites @server-invites', ( ) => {
it( 'should validate an invite', async() => {
let inviteId = await createAndSendInvite( { email:'raven@speckle.systems', inviterId: actor.id, message: 'Hey, join!' } )
const valid = await validateInvite( { email: 'raven@speckle.systems', id: inviteId } )
const valid = await validateInvite( { email: 'rAvEn@specklE.sYstems', id: inviteId } )
const invalid = await validateInvite( { email: 'bunny@speckle.systems', id: inviteId } )
expect( valid ).to.equal( true )
@@ -144,14 +143,14 @@ describe( 'Server Invites @server-invites', ( ) => {
// pass
}
let result = await useInvite( { id: inviteId, email:'crow@speckle.systems' } )
let result = await useInvite( { id: inviteId, email:'crOw@specKle.systeMs' } )
let invite = await getInviteByEmail( { email: 'crow@speckle.systems' } )
let invite = await getInviteByEmail( { email: 'crow@speCkle.syStems' } )
expect( result ).equals( true )
expect( invite.used ).equals( true )
try {
await useInvite( { id: inviteId, email:'crow@speckle.systems' } )
await useInvite( { id: inviteId, email:'CrOw@speckle.systems' } )
assert.fail( 'Should not be able to use an already used invite.' )
} catch ( e ) {
//pass
@@ -209,7 +208,6 @@ describe( 'Server Invites @server-invites', ( ) => {
} )
it( 'should create a server invite', async() => {
const res = await sendRequest( testToken, {
query: 'mutation inviteToServer($input: ServerInviteCreateInput!) { serverInviteCreate( input: $input ) }',
variables: { input: { email: 'cabbages@speckle.systems', message: 'wow!' } }
@@ -220,7 +218,6 @@ describe( 'Server Invites @server-invites', ( ) => {
} )
it( 'should create a stream invite', async() => {
let stream = { name: 'test', description:'wow' }
stream.id = await createStream( { ...stream, ownerId: actor.id } )
@@ -232,14 +229,11 @@ describe( 'Server Invites @server-invites', ( ) => {
expect( res.body.errors ).to.not.exist
expect( res.body.data.streamInviteCreate ).to.equal( true )
} )
} )
} )
function sendRequest( auth, obj, address = serverAddress ) {
return chai.request( address ).post( '/graphql' ).set( 'Authorization', auth ).send( obj )
}
const longInviteMessage =
+2 -3
View File
@@ -21,10 +21,9 @@ async function contextApiTokenHelper( { req, res, connection } ) {
if ( connection && connection.context.token ) { // Websockets (subscriptions)
token = connection.context.token
} else if ( req && req.headers.authorization ) { // Standard http
} else if ( req && req.headers.authorization ) { // Standard http post
token = req.headers.authorization
}
}
if ( token && token.includes( 'Bearer ' ) ) {
token = token.split( ' ' )[ 1 ]
}