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

This commit is contained in:
Gergő Jedlicska
2021-10-13 14:23:14 +02:00
89 changed files with 472501 additions and 602 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Update issue Status
on:
issues:
types: [closed]
jobs:
update_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
echo "$PROJECT_ID"
echo "$STATUS_FIELD_ID"
echo 'DONE_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .settings | fromjson | .options[] | select(.name== "Done") | .id' project_data.json) >> $GITHUB_ENV
echo "$DONE_ID"
- name: Add Issue to project #it's already in the project, but we do this to get its node id!
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
- name: Update Status
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $status:ID!, $id:ID!, $value:String!) {
set_status: updateProjectNextItemField(
input: {
projectId: $project
itemId: $id
fieldId: $status
value: $value
}
) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f status=$STATUS_FIELD_ID -f id=$ITEM_ID -f value=${{ env.DONE_ID }}
+50
View File
@@ -0,0 +1,50 @@
name: Move new issues into Project
on:
issues:
types: [opened]
jobs:
track_issue:
runs-on: ubuntu-latest
steps:
- name: Get project data
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ORGANIZATION: specklesystems
PROJECT_NUMBER: 9
run: |
gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
query($org: String!, $number: Int!) {
organization(login: $org){
projectNext(number: $number) {
id
fields(first:20) {
nodes {
id
name
settings
}
}
}
}
}' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json
echo 'PROJECT_ID='$(jq '.data.organization.projectNext.id' project_data.json) >> $GITHUB_ENV
echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectNext.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV
- name: Add Issue to project
env:
GITHUB_TOKEN: ${{secrets.GHPROJECT_TOKEN}}
ISSUE_ID: ${{ github.event.issue.node_id }}
run: |
item_id="$( gh api graphql --header 'GraphQL-Features: projects_next_graphql' -f query='
mutation($project:ID!, $id:ID!) {
addProjectNextItem(input: {projectId: $project, contentId: $id}) {
projectNextItem {
id
}
}
}' -f project=$PROJECT_ID -f id=$ISSUE_ID --jq '.data.addProjectNextItem.projectNextItem.id')"
echo 'ITEM_ID='$item_id >> $GITHUB_ENV
+11
View File
@@ -23,6 +23,16 @@ services:
ports:
- "127.0.0.1:6379:6379"
minio:
image: "minio/minio"
command: server /data --console-address ":9001"
restart: always
volumes:
- minio-data:/data
ports:
- "127.0.0.1:9000:9000"
- "127.0.0.1:9001:9001"
# Useful for debugging / exploring local databases
@@ -57,3 +67,4 @@ volumes:
redis-data:
pgadmin-data:
redis_insight-data:
minio-data:
+17211 -12
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
module.exports = {
env: {
node: true,
commonjs: true,
es2021: true
},
parserOptions: {
ecmaVersion: 12
},
ignorePatterns: [ 'node_modules/*' ],
extends: 'eslint:recommended',
rules: {
'object-curly-spacing': [ 'error', 'always' ],
'array-bracket-spacing': [ 'error', 'always' ],
'semi-spacing': [ 'error', { 'before': false, 'after': true } ],
'space-in-parens': [ 'error', 'always' ],
'space-before-blocks': 'error',
'space-infix-ops': 'error',
'comma-dangle': [ 'error', 'never' ],
'no-console': [ 'warn', { allow: [ 'warn', 'error' ] } ],
'space-unary-ops': 'error',
'no-var': 'error',
'no-alert': 'error',
'no-param-reassign': 'warn',
semi: [ 'error', 'never' ],
quotes: [ 'error', 'single' ],
eqeqeq: 'warn'
}
}
+11
View File
@@ -0,0 +1,11 @@
# `ifc-parser`
> TODO: description
## Usage
```
const ifcParser = require('ifc-parser');
// TODO: DEMONSTRATE API
```
+153
View File
@@ -0,0 +1,153 @@
'use strict'
const crypto = require( 'crypto' )
const crs = require( 'crypto-random-string' )
const bcrypt = require( 'bcrypt' )
const knex = require( '../knex' )
const Streams = ( ) => knex( 'streams' )
const Branches = ( ) => knex( 'branches' )
const Commits = ( ) => knex( 'commits' )
const Objects = ( ) => knex( 'objects' )
const Closures = ( ) => knex( 'object_children_closure' )
const ApiTokens = ( ) => knex( 'api_tokens' )
const TokenScopes = ( ) => knex( 'token_scopes' )
const StreamCommits = ( ) => knex( 'stream_commits' )
const BranchCommits = ( ) => knex( 'branch_commits' )
module.exports = class ServerAPI {
constructor( { streamId } ) {
this.streamId = streamId
this.isSending = false
this.buffer = []
}
async saveObject( obj ) {
if( !obj ) throw new Error( 'Null object' )
if( !obj.id ) {
obj.id = crypto.createHash( 'md5' ).update( JSON.stringify( obj ) ).digest( 'hex' )
}
await this.createObject( this.streamId, obj )
return obj.id
}
async createObject( streamId, object ) {
let insertionObject = this.prepInsertionObject( streamId, object )
let closures = [ ]
let totalChildrenCountByDepth = {}
if ( object.__closure !== null ) {
for ( const prop in object.__closure ) {
closures.push( { streamId: streamId, parent: insertionObject.id, child: prop, minDepth: object.__closure[ prop ] } )
if ( totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] )
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ]++
else
totalChildrenCountByDepth[ object.__closure[ prop ].toString( ) ] = 1
}
}
delete insertionObject.__tree
delete insertionObject.__closure
insertionObject.totalChildrenCount = closures.length
insertionObject.totalChildrenCountByDepth = JSON.stringify( totalChildrenCountByDepth )
let q1 = Objects( ).insert( insertionObject ).toString( ) + ' on conflict do nothing'
await knex.raw( q1 )
if ( closures.length > 0 ) {
let q2 = `${ Closures().insert( closures ).toString() } on conflict do nothing`
await knex.raw( q2 )
}
return insertionObject.id
}
prepInsertionObject( streamId, obj ) {
const MAX_OBJECT_SIZE = 10 * 1024 * 1024
if ( obj.hash )
obj.id = obj.hash
else
obj.id = obj.id || crypto.createHash( 'md5' ).update( JSON.stringify( obj ) ).digest( 'hex' ) // generate a hash if none is present
let stringifiedObj = JSON.stringify( obj )
if ( stringifiedObj.length > MAX_OBJECT_SIZE ) {
throw new Error( `Object too large (${stringifiedObj.length} > ${MAX_OBJECT_SIZE})` )
}
return {
data: stringifiedObj, // stored in jsonb column
streamId: streamId,
id: obj.id,
speckleType: obj.speckleType
}
}
async getBranchByNameAndStreamId( { streamId, name } ) {
let query = Branches( ).select( '*' ).where( { streamId: streamId } ).andWhere( knex.raw( 'LOWER(name) = ?', [ name ] ) ).first( )
return await query
}
async createBranch( { name, description, streamId, authorId } ) {
let branch = {}
branch.id = crs( { length: 10 } )
branch.streamId = streamId
branch.authorId = authorId
branch.name = name.toLowerCase( )
branch.description = description
await Branches( ).returning( 'id' ).insert( branch )
// update stream updated at
await Streams().where( { id: streamId } ).update( { updatedAt: knex.fn.now() } )
return branch.id
}
async createBareToken( ) {
let tokenId = crs( { length: 10 } )
let tokenString = crs( { length: 32 } )
let tokenHash = await bcrypt.hash( tokenString, 10 )
let lastChars = tokenString.slice( tokenString.length - 6, tokenString.length )
return { tokenId, tokenString, tokenHash, lastChars }
}
async createToken( { userId, name, scopes, lifespan } ) {
let { tokenId, tokenString, tokenHash, lastChars } = await this.createBareToken( )
if ( scopes.length === 0 ) throw new Error( 'No scopes provided' )
let token = {
id: tokenId,
tokenDigest: tokenHash,
lastChars: lastChars,
owner: userId,
name: name,
lifespan: lifespan
}
let tokenScopes = scopes.map( scope => ( { tokenId: tokenId, scopeName: scope } ) )
await ApiTokens( ).insert( token )
await TokenScopes( ).insert( tokenScopes )
return { id: tokenId, token: tokenId + tokenString }
}
async revokeTokenById( tokenId ) {
let delCount = await ApiTokens( ).where( { id: tokenId.slice( 0, 10 ) } ).del( )
if ( delCount === 0 )
throw new Error( 'Token revokation failed' )
return true
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,647 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('ArchiCAD 9.00 Release 1 generated IFC file.','Build Number of the Ifc 2x3 interface: 63015 (11-04-2006)\X\0D\X\0A'),'2;1');
FILE_NAME('C:\\Documents and Settings\\gkiss\\My Documents\\Ifc\\2x3\\MyPlans\\Railing.ifc','2006-05-26T11:14:01',('Architect'),('Building Designer Office'),'PreProc - EDM 4.5.0033','Windows System','The authorising person');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1= IFCORGANIZATION('GS','Graphisoft','Graphisoft',$,$);
#5= IFCAPPLICATION(#1,'9.0','ArchiCAD 9.0','ArchiCAD');
#6= IFCPERSON($,'Undefined',$,$,$,$,$,$);
#8= IFCORGANIZATION($,'OrganizationName',$,$,$);
#12= IFCPERSONANDORGANIZATION(#6,#8,$);
#13= IFCOWNERHISTORY(#12,#5,$,.NOCHANGE.,$,$,$,1148634841);
#14= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#15= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#16= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#17= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#18= IFCMEASUREWITHUNIT(IFCPLANEANGLEMEASURE(0.017453293),#17);
#19= IFCDIMENSIONALEXPONENTS(0,0,0,0,0,0,0);
#20= IFCCONVERSIONBASEDUNIT(#19,.PLANEANGLEUNIT.,'DEGREE',#18);
#21= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
#22= IFCSIUNIT(*,.MASSUNIT.,$,.GRAM.);
#23= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#24= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
#25= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
#26= IFCUNITASSIGNMENT((#14,#15,#16,#20,#21,#22,#23,#24,#25));
#28= IFCDIRECTION((1.,0.,0.));
#32= IFCDIRECTION((0.,1.,0.));
#36= IFCDIRECTION((0.,0.,1.));
#40= IFCCARTESIANPOINT((0.,0.,0.));
#44= IFCAXIS2PLACEMENT3D(#40,#36,#28);
#47= IFCDIRECTION((6.1230318E-17,1.));
#51= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#44,#47);
#54= IFCPROJECT('24a_voSxrA7gHUExjc3Mp6',#13,'Default Project',$,$,$,$,(#51,#1747),#26);
#61= IFCLOCALPLACEMENT($,#44);
#64= IFCBUILDING('0pppMBzUn8wfkj9sseUD8m',#13,'Default Building',$,$,#61,$,$,.ELEMENT.,$,$,$);
#74= IFCAXIS2PLACEMENT3D(#40,#36,#28);
#77= IFCLOCALPLACEMENT(#61,#74);
#80= IFCBUILDINGSTOREY('3cAh0AqML3CwQE2GizlXBd',#13,'',$,$,#77,$,$,.ELEMENT.,0.);
#90= IFCCARTESIANPOINT((-150.,25.,875.));
#94= IFCCARTESIANPOINT((-150.,-25.,875.));
#98= IFCCARTESIANPOINT((-150.,-25.,900.));
#102= IFCCARTESIANPOINT((-150.,25.,900.));
#106= IFCCARTESIANPOINT((1650.,25.,875.));
#111= IFCCARTESIANPOINT((1650.,-25.,875.));
#115= IFCCARTESIANPOINT((1650.,-25.,900.));
#119= IFCCARTESIANPOINT((1650.,25.,900.));
#123= IFCCARTESIANPOINT((-17.5,-17.5,0.));
#127= IFCCARTESIANPOINT((17.5,-17.5,0.));
#131= IFCCARTESIANPOINT((17.5,-17.5,875.));
#135= IFCCARTESIANPOINT((-17.5,-17.5,875.));
#139= IFCCARTESIANPOINT((-17.5,17.5,0.));
#143= IFCCARTESIANPOINT((17.5,17.5,0.));
#147= IFCCARTESIANPOINT((17.5,17.5,875.));
#151= IFCCARTESIANPOINT((-17.5,17.5,875.));
#155= IFCCARTESIANPOINT((732.5,-17.5,0.));
#159= IFCCARTESIANPOINT((767.5,-17.5,0.));
#163= IFCCARTESIANPOINT((767.5,-17.5,875.));
#167= IFCCARTESIANPOINT((732.5,-17.5,875.));
#171= IFCCARTESIANPOINT((732.5,17.5,0.));
#175= IFCCARTESIANPOINT((767.5,17.5,0.));
#179= IFCCARTESIANPOINT((767.5,17.5,875.));
#183= IFCCARTESIANPOINT((732.5,17.5,875.));
#187= IFCCARTESIANPOINT((1482.5,-17.5,0.));
#191= IFCCARTESIANPOINT((1517.5,-17.5,0.));
#195= IFCCARTESIANPOINT((1517.5,-17.5,875.));
#199= IFCCARTESIANPOINT((1482.5,-17.5,875.));
#203= IFCCARTESIANPOINT((1482.5,17.5,0.));
#207= IFCCARTESIANPOINT((1517.5,17.5,0.));
#211= IFCCARTESIANPOINT((1517.5,17.5,875.));
#215= IFCCARTESIANPOINT((1482.5,17.5,875.));
#219= IFCCARTESIANPOINT((732.5,-10.,170.));
#223= IFCCARTESIANPOINT((732.5,10.,170.));
#227= IFCCARTESIANPOINT((17.5,10.,170.));
#231= IFCCARTESIANPOINT((17.5,-10.,170.));
#235= IFCCARTESIANPOINT((732.5,10.,150.));
#240= IFCCARTESIANPOINT((732.5,-10.,150.));
#244= IFCCARTESIANPOINT((17.5,10.,150.));
#248= IFCCARTESIANPOINT((17.5,-10.,150.));
#252= IFCCARTESIANPOINT((1482.5,-10.,170.));
#256= IFCCARTESIANPOINT((1482.5,10.,170.));
#260= IFCCARTESIANPOINT((767.5,10.,170.));
#264= IFCCARTESIANPOINT((767.5,-10.,170.));
#268= IFCCARTESIANPOINT((1482.5,10.,150.));
#272= IFCCARTESIANPOINT((1482.5,-10.,150.));
#276= IFCCARTESIANPOINT((767.5,10.,150.));
#280= IFCCARTESIANPOINT((767.5,-10.,150.));
#284= IFCCARTESIANPOINT((144.5,-10.,170.));
#288= IFCCARTESIANPOINT((164.5,-10.,170.));
#292= IFCCARTESIANPOINT((164.5,-10.,875.));
#296= IFCCARTESIANPOINT((144.5,-10.,875.));
#300= IFCCARTESIANPOINT((144.5,10.,170.));
#304= IFCCARTESIANPOINT((164.5,10.,170.));
#308= IFCCARTESIANPOINT((164.5,10.,875.));
#312= IFCCARTESIANPOINT((144.5,10.,875.));
#316= IFCCARTESIANPOINT((291.5,-10.,170.));
#320= IFCCARTESIANPOINT((311.5,-10.,170.));
#324= IFCCARTESIANPOINT((311.5,-10.,875.));
#328= IFCCARTESIANPOINT((291.5,-10.,875.));
#332= IFCCARTESIANPOINT((291.5,10.,170.));
#336= IFCCARTESIANPOINT((311.5,10.,170.));
#340= IFCCARTESIANPOINT((311.5,10.,875.));
#344= IFCCARTESIANPOINT((291.5,10.,875.));
#348= IFCCARTESIANPOINT((438.5,-10.,170.));
#352= IFCCARTESIANPOINT((458.5,-10.,170.));
#356= IFCCARTESIANPOINT((458.5,-10.,875.));
#360= IFCCARTESIANPOINT((438.5,-10.,875.));
#364= IFCCARTESIANPOINT((438.5,10.,170.));
#369= IFCCARTESIANPOINT((458.5,10.,170.));
#373= IFCCARTESIANPOINT((458.5,10.,875.));
#377= IFCCARTESIANPOINT((438.5,10.,875.));
#381= IFCCARTESIANPOINT((585.5,-10.,170.));
#385= IFCCARTESIANPOINT((605.5,-10.,170.));
#389= IFCCARTESIANPOINT((605.5,-10.,875.));
#393= IFCCARTESIANPOINT((585.5,-10.,875.));
#397= IFCCARTESIANPOINT((585.5,10.,170.));
#401= IFCCARTESIANPOINT((605.5,10.,170.));
#405= IFCCARTESIANPOINT((605.5,10.,875.));
#409= IFCCARTESIANPOINT((585.5,10.,875.));
#413= IFCCARTESIANPOINT((894.5,-10.,170.));
#417= IFCCARTESIANPOINT((914.5,-10.,170.));
#421= IFCCARTESIANPOINT((914.5,-10.,875.));
#425= IFCCARTESIANPOINT((894.5,-10.,875.));
#429= IFCCARTESIANPOINT((894.5,10.,170.));
#433= IFCCARTESIANPOINT((914.5,10.,170.));
#437= IFCCARTESIANPOINT((914.5,10.,875.));
#441= IFCCARTESIANPOINT((894.5,10.,875.));
#445= IFCCARTESIANPOINT((1041.5,-10.,170.));
#449= IFCCARTESIANPOINT((1061.5,-10.,170.));
#453= IFCCARTESIANPOINT((1061.5,-10.,875.));
#457= IFCCARTESIANPOINT((1041.5,-10.,875.));
#461= IFCCARTESIANPOINT((1041.5,10.,170.));
#465= IFCCARTESIANPOINT((1061.5,10.,170.));
#469= IFCCARTESIANPOINT((1061.5,10.,875.));
#473= IFCCARTESIANPOINT((1041.5,10.,875.));
#477= IFCCARTESIANPOINT((1188.5,-10.,170.));
#481= IFCCARTESIANPOINT((1208.5,-10.,170.));
#485= IFCCARTESIANPOINT((1208.5,-10.,875.));
#489= IFCCARTESIANPOINT((1188.5,-10.,875.));
#494= IFCCARTESIANPOINT((1188.5,10.,170.));
#498= IFCCARTESIANPOINT((1208.5,10.,170.));
#502= IFCCARTESIANPOINT((1208.5,10.,875.));
#506= IFCCARTESIANPOINT((1188.5,10.,875.));
#510= IFCCARTESIANPOINT((1335.5,-10.,170.));
#514= IFCCARTESIANPOINT((1355.5,-10.,170.));
#518= IFCCARTESIANPOINT((1355.5,-10.,875.));
#522= IFCCARTESIANPOINT((1335.5,-10.,875.));
#526= IFCCARTESIANPOINT((1335.5,10.,170.));
#530= IFCCARTESIANPOINT((1355.5,10.,170.));
#534= IFCCARTESIANPOINT((1355.5,10.,875.));
#538= IFCCARTESIANPOINT((1335.5,10.,875.));
#542= IFCPOLYLOOP((#90,#94,#98,#102));
#546= IFCFACEOUTERBOUND(#542,.T.);
#549= IFCFACE((#546));
#553= IFCPOLYLOOP((#90,#106,#111,#94));
#557= IFCFACEOUTERBOUND(#553,.T.);
#560= IFCFACE((#557));
#564= IFCPOLYLOOP((#94,#111,#115,#98));
#568= IFCFACEOUTERBOUND(#564,.T.);
#571= IFCFACE((#568));
#575= IFCPOLYLOOP((#102,#98,#115,#119));
#579= IFCFACEOUTERBOUND(#575,.T.);
#582= IFCFACE((#579));
#586= IFCPOLYLOOP((#106,#90,#102,#119));
#590= IFCFACEOUTERBOUND(#586,.T.);
#593= IFCFACE((#590));
#597= IFCPOLYLOOP((#111,#106,#119,#115));
#601= IFCFACEOUTERBOUND(#597,.T.);
#604= IFCFACE((#601));
#608= IFCCLOSEDSHELL((#549,#560,#571,#582,#593,#604));
#612= IFCFACETEDBREP(#608);
#615= IFCCOLOURRGB($,0.88227665,0.40103761,0.22470436);
#616= IFCSURFACESTYLERENDERING(#615,0.,IFCNORMALISEDRATIOMEASURE(0.),$,$,$,IFCNORMALISEDRATIOMEASURE(0.),$,.NOTDEFINED.);
#617= IFCSURFACESTYLE($,.BOTH.,(#616));
#619= IFCPRESENTATIONSTYLEASSIGNMENT((#617));
#622= IFCSURFACESTYLE($,.BOTH.,(#616));
#624= IFCPRESENTATIONSTYLEASSIGNMENT((#622));
#626= IFCSTYLEDITEM(#612,(#624),$);
#630= IFCPOLYLOOP((#123,#127,#131,#135));
#634= IFCFACEOUTERBOUND(#630,.T.);
#637= IFCFACE((#634));
#641= IFCPOLYLOOP((#123,#139,#143,#127));
#645= IFCFACEOUTERBOUND(#641,.T.);
#648= IFCFACE((#645));
#652= IFCPOLYLOOP((#127,#143,#147,#131));
#656= IFCFACEOUTERBOUND(#652,.T.);
#659= IFCFACE((#656));
#663= IFCPOLYLOOP((#135,#131,#147,#151));
#667= IFCFACEOUTERBOUND(#663,.T.);
#670= IFCFACE((#667));
#674= IFCPOLYLOOP((#139,#123,#135,#151));
#678= IFCFACEOUTERBOUND(#674,.T.);
#681= IFCFACE((#678));
#685= IFCPOLYLOOP((#143,#139,#151,#147));
#689= IFCFACEOUTERBOUND(#685,.T.);
#692= IFCFACE((#689));
#696= IFCCLOSEDSHELL((#637,#648,#659,#670,#681,#692));
#700= IFCFACETEDBREP(#696);
#703= IFCSURFACESTYLE($,.BOTH.,(#616));
#705= IFCPRESENTATIONSTYLEASSIGNMENT((#703));
#707= IFCSURFACESTYLE($,.BOTH.,(#616));
#709= IFCPRESENTATIONSTYLEASSIGNMENT((#707));
#711= IFCSTYLEDITEM(#700,(#709),$);
#715= IFCPOLYLOOP((#155,#159,#163,#167));
#719= IFCFACEOUTERBOUND(#715,.T.);
#722= IFCFACE((#719));
#726= IFCPOLYLOOP((#155,#171,#175,#159));
#730= IFCFACEOUTERBOUND(#726,.T.);
#733= IFCFACE((#730));
#737= IFCPOLYLOOP((#159,#175,#179,#163));
#741= IFCFACEOUTERBOUND(#737,.T.);
#744= IFCFACE((#741));
#748= IFCPOLYLOOP((#167,#163,#179,#183));
#753= IFCFACEOUTERBOUND(#748,.T.);
#756= IFCFACE((#753));
#760= IFCPOLYLOOP((#171,#155,#167,#183));
#764= IFCFACEOUTERBOUND(#760,.T.);
#767= IFCFACE((#764));
#771= IFCPOLYLOOP((#175,#171,#183,#179));
#775= IFCFACEOUTERBOUND(#771,.T.);
#778= IFCFACE((#775));
#782= IFCCLOSEDSHELL((#722,#733,#744,#756,#767,#778));
#786= IFCFACETEDBREP(#782);
#789= IFCSURFACESTYLE($,.BOTH.,(#616));
#791= IFCPRESENTATIONSTYLEASSIGNMENT((#789));
#793= IFCSURFACESTYLE($,.BOTH.,(#616));
#795= IFCPRESENTATIONSTYLEASSIGNMENT((#793));
#797= IFCSTYLEDITEM(#786,(#795),$);
#801= IFCPOLYLOOP((#187,#191,#195,#199));
#805= IFCFACEOUTERBOUND(#801,.T.);
#808= IFCFACE((#805));
#812= IFCPOLYLOOP((#187,#203,#207,#191));
#816= IFCFACEOUTERBOUND(#812,.T.);
#819= IFCFACE((#816));
#823= IFCPOLYLOOP((#191,#207,#211,#195));
#827= IFCFACEOUTERBOUND(#823,.T.);
#830= IFCFACE((#827));
#834= IFCPOLYLOOP((#199,#195,#211,#215));
#838= IFCFACEOUTERBOUND(#834,.T.);
#841= IFCFACE((#838));
#845= IFCPOLYLOOP((#203,#187,#199,#215));
#849= IFCFACEOUTERBOUND(#845,.T.);
#852= IFCFACE((#849));
#856= IFCPOLYLOOP((#207,#203,#215,#211));
#860= IFCFACEOUTERBOUND(#856,.T.);
#863= IFCFACE((#860));
#867= IFCCLOSEDSHELL((#808,#819,#830,#841,#852,#863));
#871= IFCFACETEDBREP(#867);
#874= IFCSURFACESTYLE($,.BOTH.,(#616));
#876= IFCPRESENTATIONSTYLEASSIGNMENT((#874));
#879= IFCSURFACESTYLE($,.BOTH.,(#616));
#881= IFCPRESENTATIONSTYLEASSIGNMENT((#879));
#883= IFCSTYLEDITEM(#871,(#881),$);
#887= IFCPOLYLOOP((#219,#223,#227,#231));
#891= IFCFACEOUTERBOUND(#887,.T.);
#894= IFCFACE((#891));
#898= IFCPOLYLOOP((#235,#223,#219,#240));
#902= IFCFACEOUTERBOUND(#898,.T.);
#905= IFCFACE((#902));
#909= IFCPOLYLOOP((#223,#235,#244,#227));
#913= IFCFACEOUTERBOUND(#909,.T.);
#916= IFCFACE((#913));
#920= IFCPOLYLOOP((#248,#231,#227,#244));
#924= IFCFACEOUTERBOUND(#920,.T.);
#927= IFCFACE((#924));
#931= IFCPOLYLOOP((#240,#219,#231,#248));
#935= IFCFACEOUTERBOUND(#931,.T.);
#938= IFCFACE((#935));
#942= IFCPOLYLOOP((#235,#240,#248,#244));
#946= IFCFACEOUTERBOUND(#942,.T.);
#949= IFCFACE((#946));
#953= IFCCLOSEDSHELL((#894,#905,#916,#927,#938,#949));
#957= IFCFACETEDBREP(#953);
#960= IFCSURFACESTYLE($,.BOTH.,(#616));
#962= IFCPRESENTATIONSTYLEASSIGNMENT((#960));
#964= IFCSURFACESTYLE($,.BOTH.,(#616));
#966= IFCPRESENTATIONSTYLEASSIGNMENT((#964));
#968= IFCSTYLEDITEM(#957,(#966),$);
#972= IFCPOLYLOOP((#252,#256,#260,#264));
#976= IFCFACEOUTERBOUND(#972,.T.);
#979= IFCFACE((#976));
#983= IFCPOLYLOOP((#268,#256,#252,#272));
#987= IFCFACEOUTERBOUND(#983,.T.);
#990= IFCFACE((#987));
#994= IFCPOLYLOOP((#256,#268,#276,#260));
#998= IFCFACEOUTERBOUND(#994,.T.);
#1001= IFCFACE((#998));
#1006= IFCPOLYLOOP((#280,#264,#260,#276));
#1010= IFCFACEOUTERBOUND(#1006,.T.);
#1013= IFCFACE((#1010));
#1017= IFCPOLYLOOP((#272,#252,#264,#280));
#1021= IFCFACEOUTERBOUND(#1017,.T.);
#1024= IFCFACE((#1021));
#1028= IFCPOLYLOOP((#268,#272,#280,#276));
#1032= IFCFACEOUTERBOUND(#1028,.T.);
#1035= IFCFACE((#1032));
#1039= IFCCLOSEDSHELL((#979,#990,#1001,#1013,#1024,#1035));
#1043= IFCFACETEDBREP(#1039);
#1046= IFCSURFACESTYLE($,.BOTH.,(#616));
#1048= IFCPRESENTATIONSTYLEASSIGNMENT((#1046));
#1050= IFCSURFACESTYLE($,.BOTH.,(#616));
#1052= IFCPRESENTATIONSTYLEASSIGNMENT((#1050));
#1054= IFCSTYLEDITEM(#1043,(#1052),$);
#1058= IFCPOLYLOOP((#284,#288,#292,#296));
#1062= IFCFACEOUTERBOUND(#1058,.T.);
#1065= IFCFACE((#1062));
#1069= IFCPOLYLOOP((#284,#300,#304,#288));
#1073= IFCFACEOUTERBOUND(#1069,.T.);
#1076= IFCFACE((#1073));
#1080= IFCPOLYLOOP((#288,#304,#308,#292));
#1084= IFCFACEOUTERBOUND(#1080,.T.);
#1087= IFCFACE((#1084));
#1091= IFCPOLYLOOP((#296,#292,#308,#312));
#1095= IFCFACEOUTERBOUND(#1091,.T.);
#1098= IFCFACE((#1095));
#1102= IFCPOLYLOOP((#300,#284,#296,#312));
#1106= IFCFACEOUTERBOUND(#1102,.T.);
#1109= IFCFACE((#1106));
#1113= IFCPOLYLOOP((#304,#300,#312,#308));
#1117= IFCFACEOUTERBOUND(#1113,.T.);
#1120= IFCFACE((#1117));
#1124= IFCCLOSEDSHELL((#1065,#1076,#1087,#1098,#1109,#1120));
#1128= IFCFACETEDBREP(#1124);
#1131= IFCSURFACESTYLE($,.BOTH.,(#616));
#1134= IFCPRESENTATIONSTYLEASSIGNMENT((#1131));
#1136= IFCSURFACESTYLE($,.BOTH.,(#616));
#1138= IFCPRESENTATIONSTYLEASSIGNMENT((#1136));
#1140= IFCSTYLEDITEM(#1128,(#1138),$);
#1144= IFCPOLYLOOP((#316,#320,#324,#328));
#1148= IFCFACEOUTERBOUND(#1144,.T.);
#1151= IFCFACE((#1148));
#1155= IFCPOLYLOOP((#316,#332,#336,#320));
#1159= IFCFACEOUTERBOUND(#1155,.T.);
#1162= IFCFACE((#1159));
#1166= IFCPOLYLOOP((#320,#336,#340,#324));
#1170= IFCFACEOUTERBOUND(#1166,.T.);
#1173= IFCFACE((#1170));
#1177= IFCPOLYLOOP((#328,#324,#340,#344));
#1181= IFCFACEOUTERBOUND(#1177,.T.);
#1184= IFCFACE((#1181));
#1188= IFCPOLYLOOP((#332,#316,#328,#344));
#1192= IFCFACEOUTERBOUND(#1188,.T.);
#1195= IFCFACE((#1192));
#1199= IFCPOLYLOOP((#336,#332,#344,#340));
#1203= IFCFACEOUTERBOUND(#1199,.T.);
#1206= IFCFACE((#1203));
#1210= IFCCLOSEDSHELL((#1151,#1162,#1173,#1184,#1195,#1206));
#1214= IFCFACETEDBREP(#1210);
#1217= IFCSURFACESTYLE($,.BOTH.,(#616));
#1219= IFCPRESENTATIONSTYLEASSIGNMENT((#1217));
#1221= IFCSURFACESTYLE($,.BOTH.,(#616));
#1223= IFCPRESENTATIONSTYLEASSIGNMENT((#1221));
#1225= IFCSTYLEDITEM(#1214,(#1223),$);
#1229= IFCPOLYLOOP((#348,#352,#356,#360));
#1233= IFCFACEOUTERBOUND(#1229,.T.);
#1236= IFCFACE((#1233));
#1240= IFCPOLYLOOP((#348,#364,#369,#352));
#1244= IFCFACEOUTERBOUND(#1240,.T.);
#1247= IFCFACE((#1244));
#1251= IFCPOLYLOOP((#352,#369,#373,#356));
#1255= IFCFACEOUTERBOUND(#1251,.T.);
#1258= IFCFACE((#1255));
#1263= IFCPOLYLOOP((#360,#356,#373,#377));
#1267= IFCFACEOUTERBOUND(#1263,.T.);
#1270= IFCFACE((#1267));
#1274= IFCPOLYLOOP((#364,#348,#360,#377));
#1278= IFCFACEOUTERBOUND(#1274,.T.);
#1281= IFCFACE((#1278));
#1285= IFCPOLYLOOP((#369,#364,#377,#373));
#1289= IFCFACEOUTERBOUND(#1285,.T.);
#1292= IFCFACE((#1289));
#1296= IFCCLOSEDSHELL((#1236,#1247,#1258,#1270,#1281,#1292));
#1300= IFCFACETEDBREP(#1296);
#1303= IFCSURFACESTYLE($,.BOTH.,(#616));
#1305= IFCPRESENTATIONSTYLEASSIGNMENT((#1303));
#1307= IFCSURFACESTYLE($,.BOTH.,(#616));
#1309= IFCPRESENTATIONSTYLEASSIGNMENT((#1307));
#1311= IFCSTYLEDITEM(#1300,(#1309),$);
#1315= IFCPOLYLOOP((#381,#385,#389,#393));
#1319= IFCFACEOUTERBOUND(#1315,.T.);
#1322= IFCFACE((#1319));
#1326= IFCPOLYLOOP((#381,#397,#401,#385));
#1330= IFCFACEOUTERBOUND(#1326,.T.);
#1333= IFCFACE((#1330));
#1337= IFCPOLYLOOP((#385,#401,#405,#389));
#1341= IFCFACEOUTERBOUND(#1337,.T.);
#1344= IFCFACE((#1341));
#1348= IFCPOLYLOOP((#393,#389,#405,#409));
#1352= IFCFACEOUTERBOUND(#1348,.T.);
#1355= IFCFACE((#1352));
#1359= IFCPOLYLOOP((#397,#381,#393,#409));
#1363= IFCFACEOUTERBOUND(#1359,.T.);
#1366= IFCFACE((#1363));
#1370= IFCPOLYLOOP((#401,#397,#409,#405));
#1374= IFCFACEOUTERBOUND(#1370,.T.);
#1377= IFCFACE((#1374));
#1381= IFCCLOSEDSHELL((#1322,#1333,#1344,#1355,#1366,#1377));
#1385= IFCFACETEDBREP(#1381);
#1388= IFCSURFACESTYLE($,.BOTH.,(#616));
#1391= IFCPRESENTATIONSTYLEASSIGNMENT((#1388));
#1393= IFCSURFACESTYLE($,.BOTH.,(#616));
#1395= IFCPRESENTATIONSTYLEASSIGNMENT((#1393));
#1397= IFCSTYLEDITEM(#1385,(#1395),$);
#1401= IFCPOLYLOOP((#413,#417,#421,#425));
#1405= IFCFACEOUTERBOUND(#1401,.T.);
#1408= IFCFACE((#1405));
#1412= IFCPOLYLOOP((#413,#429,#433,#417));
#1416= IFCFACEOUTERBOUND(#1412,.T.);
#1419= IFCFACE((#1416));
#1423= IFCPOLYLOOP((#417,#433,#437,#421));
#1427= IFCFACEOUTERBOUND(#1423,.T.);
#1430= IFCFACE((#1427));
#1434= IFCPOLYLOOP((#425,#421,#437,#441));
#1438= IFCFACEOUTERBOUND(#1434,.T.);
#1441= IFCFACE((#1438));
#1445= IFCPOLYLOOP((#429,#413,#425,#441));
#1449= IFCFACEOUTERBOUND(#1445,.T.);
#1452= IFCFACE((#1449));
#1456= IFCPOLYLOOP((#433,#429,#441,#437));
#1460= IFCFACEOUTERBOUND(#1456,.T.);
#1463= IFCFACE((#1460));
#1467= IFCCLOSEDSHELL((#1408,#1419,#1430,#1441,#1452,#1463));
#1471= IFCFACETEDBREP(#1467);
#1474= IFCSURFACESTYLE($,.BOTH.,(#616));
#1476= IFCPRESENTATIONSTYLEASSIGNMENT((#1474));
#1478= IFCSURFACESTYLE($,.BOTH.,(#616));
#1480= IFCPRESENTATIONSTYLEASSIGNMENT((#1478));
#1482= IFCSTYLEDITEM(#1471,(#1480),$);
#1486= IFCPOLYLOOP((#445,#449,#453,#457));
#1490= IFCFACEOUTERBOUND(#1486,.T.);
#1493= IFCFACE((#1490));
#1497= IFCPOLYLOOP((#445,#461,#465,#449));
#1501= IFCFACEOUTERBOUND(#1497,.T.);
#1504= IFCFACE((#1501));
#1508= IFCPOLYLOOP((#449,#465,#469,#453));
#1512= IFCFACEOUTERBOUND(#1508,.T.);
#1515= IFCFACE((#1512));
#1520= IFCPOLYLOOP((#457,#453,#469,#473));
#1524= IFCFACEOUTERBOUND(#1520,.T.);
#1527= IFCFACE((#1524));
#1531= IFCPOLYLOOP((#461,#445,#457,#473));
#1535= IFCFACEOUTERBOUND(#1531,.T.);
#1538= IFCFACE((#1535));
#1542= IFCPOLYLOOP((#465,#461,#473,#469));
#1546= IFCFACEOUTERBOUND(#1542,.T.);
#1549= IFCFACE((#1546));
#1553= IFCCLOSEDSHELL((#1493,#1504,#1515,#1527,#1538,#1549));
#1557= IFCFACETEDBREP(#1553);
#1560= IFCSURFACESTYLE($,.BOTH.,(#616));
#1562= IFCPRESENTATIONSTYLEASSIGNMENT((#1560));
#1564= IFCSURFACESTYLE($,.BOTH.,(#616));
#1566= IFCPRESENTATIONSTYLEASSIGNMENT((#1564));
#1568= IFCSTYLEDITEM(#1557,(#1566),$);
#1572= IFCPOLYLOOP((#477,#481,#485,#489));
#1576= IFCFACEOUTERBOUND(#1572,.T.);
#1579= IFCFACE((#1576));
#1583= IFCPOLYLOOP((#477,#494,#498,#481));
#1587= IFCFACEOUTERBOUND(#1583,.T.);
#1590= IFCFACE((#1587));
#1594= IFCPOLYLOOP((#481,#498,#502,#485));
#1598= IFCFACEOUTERBOUND(#1594,.T.);
#1601= IFCFACE((#1598));
#1605= IFCPOLYLOOP((#489,#485,#502,#506));
#1609= IFCFACEOUTERBOUND(#1605,.T.);
#1612= IFCFACE((#1609));
#1616= IFCPOLYLOOP((#494,#477,#489,#506));
#1620= IFCFACEOUTERBOUND(#1616,.T.);
#1623= IFCFACE((#1620));
#1627= IFCPOLYLOOP((#498,#494,#506,#502));
#1631= IFCFACEOUTERBOUND(#1627,.T.);
#1634= IFCFACE((#1631));
#1638= IFCCLOSEDSHELL((#1579,#1590,#1601,#1612,#1623,#1634));
#1642= IFCFACETEDBREP(#1638);
#1646= IFCSURFACESTYLE($,.BOTH.,(#616));
#1648= IFCPRESENTATIONSTYLEASSIGNMENT((#1646));
#1650= IFCSURFACESTYLE($,.BOTH.,(#616));
#1652= IFCPRESENTATIONSTYLEASSIGNMENT((#1650));
#1654= IFCSTYLEDITEM(#1642,(#1652),$);
#1658= IFCPOLYLOOP((#510,#514,#518,#522));
#1662= IFCFACEOUTERBOUND(#1658,.T.);
#1665= IFCFACE((#1662));
#1669= IFCPOLYLOOP((#510,#526,#530,#514));
#1673= IFCFACEOUTERBOUND(#1669,.T.);
#1676= IFCFACE((#1673));
#1680= IFCPOLYLOOP((#514,#530,#534,#518));
#1684= IFCFACEOUTERBOUND(#1680,.T.);
#1687= IFCFACE((#1684));
#1691= IFCPOLYLOOP((#522,#518,#534,#538));
#1695= IFCFACEOUTERBOUND(#1691,.T.);
#1698= IFCFACE((#1695));
#1702= IFCPOLYLOOP((#526,#510,#522,#538));
#1706= IFCFACEOUTERBOUND(#1702,.T.);
#1709= IFCFACE((#1706));
#1713= IFCPOLYLOOP((#530,#526,#538,#534));
#1717= IFCFACEOUTERBOUND(#1713,.T.);
#1720= IFCFACE((#1717));
#1724= IFCCLOSEDSHELL((#1665,#1676,#1687,#1698,#1709,#1720));
#1728= IFCFACETEDBREP(#1724);
#1731= IFCSURFACESTYLE($,.BOTH.,(#616));
#1733= IFCPRESENTATIONSTYLEASSIGNMENT((#1731));
#1735= IFCSURFACESTYLE($,.BOTH.,(#616));
#1737= IFCPRESENTATIONSTYLEASSIGNMENT((#1735));
#1739= IFCSTYLEDITEM(#1728,(#1737),$);
#1743= IFCDIRECTION((6.1230318E-17,1.));
#1747= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Model',3,1.0000000E-5,#44,#1743);
#1751= IFCSHAPEREPRESENTATION(#1747,'Body','Brep',(#612,#700,#786,#871,#957,#1043,#1128,#1214,#1300,#1385,#1471,#1557,#1642,#1728));
#1757= IFCPRODUCTDEFINITIONSHAPE($,$,(#1751));
#1761= IFCCARTESIANPOINT((203.55989,128.43353,0.));
#1765= IFCAXIS2PLACEMENT3D(#1761,#36,#28);
#1768= IFCLOCALPLACEMENT(#77,#1765);
#1771= IFCRAILING('0zUE$sW7XAm9ihLEhV0q1w',#13,'',$,$,#1768,#1757,$,.HANDRAIL.);
#1791= IFCPROPERTYSINGLEVALUE('ArchiCAD_UID',$,IFCINTEGER(235),$);
#1795= IFCCOMPLEXPROPERTY('OBJECT',$,'ArchiCAD',(#1791));
#1800= IFCPROPERTYSET('3KvQZMWnbAZeaaPcRQxbJ7',#13,'Graphisoft AC90 OBJECT','Graphisoft AC90',(#1795,#2119));
#1805= IFCPROPERTYSINGLEVALUE('API_LIBNAME',$,IFCLABEL('Rail Post with Balusters'),$);
#1809= IFCPROPERTYSINGLEVALUE('A',$,IFCNUMERICMEASURE(1.8),$);
#1813= IFCPROPERTYSINGLEVALUE('B',$,IFCNUMERICMEASURE(0.05),$);
#1817= IFCPROPERTYSINGLEVALUE('ZZYZX',$,IFCNUMERICMEASURE(0.9),$);
#1821= IFCPROPERTYSINGLEVALUE('gs_detlevel_3D',$,IFCLABEL('Detailed'),$);
#1825= IFCPROPERTYSINGLEVALUE('cfs',$,IFCNUMERICMEASURE(0.),$);
#1829= IFCPROPERTYSINGLEVALUE('ptyp',$,IFCLABEL('Straight'),$);
#1833= IFCPROPERTYSINGLEVALUE('ani',$,IFCNUMERICMEASURE(0.),$);
#1837= IFCPROPERTYSINGLEVALUE('rom',$,IFCLABEL('Vertical'),$);
#1841= IFCPROPERTYSINGLEVALUE('rmm',$,IFCLABEL('Vertical'),$);
#1845= IFCPROPERTYSINGLEVALUE('LengthSize',$,IFCNUMERICMEASURE(0.),$);
#1849= IFCPROPERTYSINGLEVALUE('lra',$,IFCNUMERICMEASURE(1.5),$);
#1853= IFCPROPERTYSINGLEVALUE('lrah',$,IFCNUMERICMEASURE(1.5),$);
#1857= IFCPROPERTYSINGLEVALUE('hr',$,IFCNUMERICMEASURE(0.75),$);
#1861= IFCPROPERTYSINGLEVALUE('hrPerp',$,IFCNUMERICMEASURE(0.75),$);
#1865= IFCPROPERTYSINGLEVALUE('ab',$,IFCNUMERICMEASURE(1.5707963),$);
#1869= IFCPROPERTYSINGLEVALUE('rb',$,IFCNUMERICMEASURE(1.),$);
#1873= IFCPROPERTYSINGLEVALUE('ds',$,IFCNUMERICMEASURE(0.15),$);
#1877= IFCPROPERTYSINGLEVALUE('dsPerp',$,IFCNUMERICMEASURE(0.15),$);
#1881= IFCPROPERTYSINGLEVALUE('gs_AngleL',$,IFCNUMERICMEASURE(90.),$);
#1885= IFCPROPERTYSINGLEVALUE('gs_AngleR',$,IFCNUMERICMEASURE(90.),$);
#1889= IFCPROPERTYSINGLEVALUE('hrt',$,IFCLABEL('Rectangular'),$);
#1893= IFCPROPERTYSINGLEVALUE('hrd',$,IFCNUMERICMEASURE(0.05),$);
#1897= IFCPROPERTYSINGLEVALUE('hrw',$,IFCNUMERICMEASURE(0.05),$);
#1902= IFCPROPERTYSINGLEVALUE('hrh',$,IFCNUMERICMEASURE(0.025),$);
#1906= IFCPROPERTYSINGLEVALUE('hro',$,IFCNUMERICMEASURE(0.),$);
#1910= IFCPROPERTYSINGLEVALUE('hroL',$,IFCNUMERICMEASURE(0.15),$);
#1914= IFCPROPERTYSINGLEVALUE('hroR',$,IFCNUMERICMEASURE(0.15),$);
#1918= IFCPROPERTYSINGLEVALUE('hroLDiagonal',$,IFCNUMERICMEASURE(0.15),$);
#1922= IFCPROPERTYSINGLEVALUE('hroRDiagonal',$,IFCNUMERICMEASURE(0.15),$);
#1926= IFCPROPERTYSINGLEVALUE('pt',$,IFCLABEL('Rectangular'),$);
#1930= IFCPROPERTYSINGLEVALUE('np',$,IFCNUMERICMEASURE(3.),$);
#1934= IFCPROPERTYSINGLEVALUE('pd',$,IFCNUMERICMEASURE(0.035),$);
#1938= IFCPROPERTYSINGLEVALUE('pw',$,IFCNUMERICMEASURE(0.035),$);
#1942= IFCPROPERTYSINGLEVALUE('pth',$,IFCNUMERICMEASURE(0.035),$);
#1946= IFCPROPERTYSINGLEVALUE('gs_PostBaseOverhang',$,IFCNUMERICMEASURE(0.),$);
#1950= IFCPROPERTYSINGLEVALUE('gs_PostBaseOverhangPerp',$,IFCNUMERICMEASURE(0.),$);
#1954= IFCPROPERTYSINGLEVALUE('bt',$,IFCLABEL('Rectangular'),$);
#1958= IFCPROPERTYSINGLEVALUE('nb',$,IFCNUMERICMEASURE(4.),$);
#1962= IFCPROPERTYSINGLEVALUE('bd',$,IFCNUMERICMEASURE(0.02),$);
#1966= IFCPROPERTYSINGLEVALUE('bth',$,IFCNUMERICMEASURE(0.02),$);
#1970= IFCPROPERTYSINGLEVALUE('bw',$,IFCNUMERICMEASURE(0.02),$);
#1974= IFCPROPERTYSINGLEVALUE('res',$,IFCNUMERICMEASURE(32.),$);
#1978= IFCPROPERTYSINGLEVALUE('rescs',$,IFCNUMERICMEASURE(12.),$);
#1982= IFCPROPERTYSINGLEVALUE('gs_shadow',$,IFCNUMERICMEASURE(1.),$);
#1986= IFCPROPERTYSINGLEVALUE('AC_show2DHotspotsIn3D',$,IFCNUMERICMEASURE(0.),$);
#1990= IFCPROPERTYSINGLEVALUE('gs_cont_pen',$,IFCNUMERICMEASURE(2.),$);
#1994= IFCPROPERTYSINGLEVALUE('gs_fill_type',$,IFCNUMERICMEASURE(65.),$);
#1998= IFCPROPERTYSINGLEVALUE('gs_fill_pen',$,IFCNUMERICMEASURE(91.),$);
#2002= IFCPROPERTYSINGLEVALUE('gs_back_pen',$,IFCNUMERICMEASURE(91.),$);
#2006= IFCPROPERTYSINGLEVALUE('sa',$,IFCNUMERICMEASURE(1.),$);
#2010= IFCPROPERTYSINGLEVALUE('al',$,IFCNUMERICMEASURE(6.),$);
#2014= IFCPROPERTYSINGLEVALUE('fmat',$,IFCNUMERICMEASURE(15.),$);
#2018= IFCPROPERTYSINGLEVALUE('pmat',$,IFCNUMERICMEASURE(15.),$);
#2022= IFCPROPERTYSINGLEVALUE('bmat',$,IFCNUMERICMEASURE(15.),$);
#2026= IFCPROPERTYSINGLEVALUE('gs_list_cost',$,IFCNUMERICMEASURE(0.),$);
#2031= IFCPROPERTYSINGLEVALUE('gs_list_manufacturer',$,IFCLABEL(''),$);
#2035= IFCPROPERTYSINGLEVALUE('gs_list_note',$,IFCLABEL(''),$);
#2039= IFCPROPERTYSINGLEVALUE('gs_list_location',$,IFCLABEL(''),$);
#2043= IFCPROPERTYSINGLEVALUE('gs_list_accessories',$,IFCLABEL(''),$);
#2047= IFCPROPERTYSINGLEVALUE('FM_Type',$,IFCLABEL('Others'),$);
#2051= IFCPROPERTYSINGLEVALUE('FM_InventoryNumber',$,IFCLABEL(''),$);
#2055= IFCPROPERTYSINGLEVALUE('FM_SerialNumber',$,IFCLABEL(''),$);
#2059= IFCPROPERTYSINGLEVALUE('FM_ProductionYear',$,IFCLABEL(''),$);
#2063= IFCPROPERTYSINGLEVALUE('FM_ObjectWeight',$,IFCNUMERICMEASURE(0.),$);
#2067= IFCPROPERTYSINGLEVALUE('FM_ObjectWeightUnit',$,IFCLABEL('kg'),$);
#2071= IFCPROPERTYSINGLEVALUE('gs_list_custom1',$,IFCLABEL(''),$);
#2075= IFCPROPERTYSINGLEVALUE('gs_list_custom2',$,IFCLABEL(''),$);
#2079= IFCPROPERTYSINGLEVALUE('gs_list_custom3',$,IFCLABEL(''),$);
#2083= IFCPROPERTYSINGLEVALUE('gs_list_custom4',$,IFCLABEL(''),$);
#2087= IFCPROPERTYSINGLEVALUE('gs_list_custom5',$,IFCLABEL(''),$);
#2091= IFCPROPERTYSINGLEVALUE('gs_detlevel_3d_m',$,IFCNUMERICMEASURE(2.),$);
#2095= IFCPROPERTYSINGLEVALUE('ptyp_m',$,IFCNUMERICMEASURE(1.),$);
#2099= IFCPROPERTYSINGLEVALUE('rom_m',$,IFCNUMERICMEASURE(1.),$);
#2103= IFCPROPERTYSINGLEVALUE('rmm_m',$,IFCNUMERICMEASURE(1.),$);
#2107= IFCPROPERTYSINGLEVALUE('hrt_m',$,IFCNUMERICMEASURE(1.),$);
#2111= IFCPROPERTYSINGLEVALUE('pt_m',$,IFCNUMERICMEASURE(1.),$);
#2115= IFCPROPERTYSINGLEVALUE('bt_m',$,IFCNUMERICMEASURE(1.),$);
#2119= IFCCOMPLEXPROPERTY('LIBPARAM',$,'ArchiCAD',(#1805,#1809,#1813,#1817,#1821,#1825,#1829,#1833,#1837,#1841,#1845,#1849,#1853,#1857,#1861,#1865,#1869,#1873,#1877,#1881,#1885,#1889,#1893,#1897,#1902,#1906,#1910,#1914,#1918,#1922,#1926,#1930,#1934,#1938,#1942,#1946,#1950,#1954,#1958,#1962,#1966,#1970,#1974,#1978,#1982,#1986,#1990,#1994,#1998,#2002,#2006,#2010,#2014,#2018,#2022,#2026,#2031,#2035,#2039,#2043,#2047,#2051,#2055,#2059,#2063,#2067,#2071,#2075,#2079,#2083,#2087,#2091,#2095,#2099,#2103,#2107,#2111,#2115));
#2125= IFCRELDEFINESBYPROPERTIES('3kqdyy5N1ETPObqZJVJPbH',#13,'ArchiCAD','ExtendedProperties',(#1771),#1800);
#2127= IFCPROPERTYSINGLEVALUE('Layername',$,IFCLABEL('Furniture & Equipment'),$);
#2131= IFCPROPERTYSINGLEVALUE('Red',$,IFCINTEGER(2),$);
#2135= IFCPROPERTYSINGLEVALUE('Green',$,IFCINTEGER(157),$);
#2139= IFCPROPERTYSINGLEVALUE('Blue',$,IFCINTEGER(33),$);
#2143= IFCCOMPLEXPROPERTY('Color',$,'Color',(#2131,#2135,#2139));
#2148= IFCPROPERTYSET('2Sp8gWvUj9yBMn9klggsIS',#13,'PSet_Draughting',$,(#2127,#2143));
#2153= IFCRELDEFINESBYPROPERTIES('1PVkHJilvBSxvjXlwjL27x',#13,$,$,(#1771),#2148);
#2155= IFCMATERIAL('50 %');
#2159= IFCRELASSOCIATESMATERIAL('1GhOPX9UHE9fErZyyZyEzx',#13,$,$,(#1771),#2155);
#2161= IFCSLAB('1_g0UhnsD11uLx01z1JWyj',#13,'Slab-004',$,$,#2224,#2213,$,.FLOOR.);
#2180= IFCCARTESIANPOINT((0.,0.));
#2184= IFCCARTESIANPOINT((1939.3575,0.));
#2188= IFCCARTESIANPOINT((1939.3575,945.02591));
#2192= IFCCARTESIANPOINT((0.,945.02591));
#2196= IFCPOLYLINE((#2180,#2184,#2188,#2192,#2180));
#2200= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#2196);
#2201= IFCAXIS2PLACEMENT3D(#40,#36,#28);
#2204= IFCEXTRUDEDAREASOLID(#2200,#2201,#36,300.);
#2207= IFCSHAPEREPRESENTATION(#51,'Body','SweptSolid',(#2204));
#2213= IFCPRODUCTDEFINITIONSHAPE($,$,(#2207));
#2217= IFCCARTESIANPOINT((0.,0.,-300.));
#2221= IFCAXIS2PLACEMENT3D(#2217,#36,#28);
#2224= IFCLOCALPLACEMENT(#77,#2221);
#2227= IFCAXIS2PLACEMENT3D(#40,#36,#28);
#2230= IFCMATERIAL('Wood');
#2233= IFCMATERIALLAYER(#2230,20.,.U.);
#2235= IFCMATERIAL('Lightweight Concrete');
#2238= IFCMATERIALLAYER(#2235,50.,.U.);
#2240= IFCMATERIAL('Batt Insulation');
#2243= IFCMATERIALLAYER(#2240,30.,.U.);
#2245= IFCMATERIAL('Structural Concrete');
#2248= IFCMATERIALLAYER(#2245,200.,.U.);
#2250= IFCMATERIALLAYERSET((#2233,#2238,#2243,#2248),'concrete floor, insul+parquet');
#2252= IFCMATERIALLAYERSETUSAGE(#2250,.AXIS3.,.NEGATIVE.,300.);
#2253= IFCRELASSOCIATESMATERIAL('3eTSGAwqf69xWG27aIAN0$',#13,$,$,(#2161),#2252);
#2255= IFCPROPERTYSINGLEVALUE('ArchiCAD_UID',$,IFCINTEGER(237),$);
#2259= IFCCOMPLEXPROPERTY('SLAB',$,'ArchiCAD',(#2255));
#2264= IFCPROPERTYSET('1EehdkAGTAR8C1OYr_OYbq',#13,'Graphisoft AC90 SLAB','Graphisoft AC90',(#2259));
#2269= IFCRELDEFINESBYPROPERTIES('2fg$8mogD3RuMTkPATPAUY',#13,'ArchiCAD','ExtendedProperties',(#2161),#2264);
#2271= IFCPROPERTYSINGLEVALUE('Layername',$,IFCLABEL('Floors'),$);
#2275= IFCPROPERTYSINGLEVALUE('Red',$,IFCINTEGER(204),$);
#2279= IFCPROPERTYSINGLEVALUE('Green',$,IFCINTEGER(101),$);
#2283= IFCPROPERTYSINGLEVALUE('Blue',$,IFCINTEGER(0),$);
#2288= IFCCOMPLEXPROPERTY('Color',$,'Color',(#2275,#2279,#2283));
#2293= IFCPROPERTYSET('1iB4HrvuP5BQ0yWb3LjOaM',#13,'PSet_Draughting',$,(#2271,#2288));
#2298= IFCRELDEFINESBYPROPERTIES('2dvo0puPT2wPMPbhcPM1zN',#13,$,$,(#2161),#2293);
#2300= IFCRELCONTAINEDINSPATIALSTRUCTURE('2fZhnPphjAfunQkj2tDz6W',#13,'BuildingStoreyContainer','BuildingStoreyContainer for Building Elements',(#1771,#2161),#80);
#2302= IFCRELAGGREGATES('1$$MJTMxn3_8AeVGnO8szo',#13,'BuildingContainer','BuildingContainer for BuildigStories',#64,(#80));
#2304= IFCRELAGGREGATES('1c7zmztwX08w$enkKE9lJ6',#13,'ProjectContainer','ProjectContainer for Sites',#54,(#64));
ENDSEC;
END-ISO-10303-21;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,421 @@
ISO-10303-21;
HEADER;
FILE_DESCRIPTION(('IFC2X3.exp'),'2;1');
FILE_NAME('C:\\TeklaStructuresModels\\Acis_Sat\\plate_steel_example-tek_1fix.ifc','2006-05-12T10:07:38',('Steel2 macro version:12.0 Build:179423,2.5.2006'),('Structural Designer'),'EXPRESS Data Manager version:20040806','Tekla Structures 12.0','');
FILE_SCHEMA(('IFC2X3'));
ENDSEC;
DATA;
#1= IFCPERSON('TEKLAAD/lli','Undefined',$,$,$,$,$,$);
#3= IFCORGANIZATION($,'Tekla Corporation',$,$,$);
#7= IFCPERSONANDORGANIZATION(#1,#3,$);
#8= IFCAPPLICATION(#3,'12.0','Tekla Structures','Multi material modeling');
#9= IFCOWNERHISTORY(#7,#8,$,.ADDED.,$,$,$,1147417657);
#10= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);
#11= IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.);
#12= IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.);
#13= IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.);
#14= IFCSIUNIT(*,.SOLIDANGLEUNIT.,$,.STERADIAN.);
#15= IFCSIUNIT(*,.MASSUNIT.,.KILO.,.GRAM.);
#16= IFCSIUNIT(*,.TIMEUNIT.,$,.SECOND.);
#17= IFCSIUNIT(*,.THERMODYNAMICTEMPERATUREUNIT.,$,.DEGREE_CELSIUS.);
#18= IFCSIUNIT(*,.LUMINOUSINTENSITYUNIT.,$,.LUMEN.);
#19= IFCUNITASSIGNMENT((#10,#11,#12,#13,#14,#15,#16,#17,#18));
#21= IFCCARTESIANPOINT((0.,0.,0.));
#25= IFCDIRECTION((1.,0.,0.));
#29= IFCDIRECTION((0.,1.,0.));
#33= IFCDIRECTION((0.,0.,1.));
#37= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#40= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Design',3,1.0000000E-5,#37,$);
#43= IFCGEOMETRICREPRESENTATIONCONTEXT('Plan','Sketch',3,1.0000000E-5,#37,$);
#46= IFCPROJECT('2gPUQOiNz2FR1H6lWQ8j0k',#9,'PROJ: NAME','Description','Object type','LongName','Phase',(#40,#43),#19);
#53= IFCMATERIAL('A36');
#56= IFCMATERIAL('A992');
#59= IFCMATERIAL('A500-GR.B');
#62= IFCPERSON('TEKLAAD/chke','Undefined',$,$,$,$,$,$);
#64= IFCPERSONANDORGANIZATION(#62,#3,$);
#65= IFCOWNERHISTORY(#64,#8,$,.ADDED.,$,$,$,1147417657);
#66= IFCSITE('1_WapmNXfFdOqQ3garaDJn',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$,$,$);
#76= IFCRELAGGREGATES('36yaCMhuT2DxfpF3ieGira',#65,$,$,#46,(#66));
#78= IFCBUILDING('2iHnVT4$n9JQE18R_2cJEI',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$,$,$);
#88= IFCRELAGGREGATES('0A543zcq1Fdv_lsrHHX8Kv',#65,$,$,#66,(#78));
#90= IFCBUILDINGSTOREY('0UguZM0$L8L9OUA114_ZEd',#65,'Undefined',$,$,$,$,$,.ELEMENT.,$);
#100= IFCRELAGGREGATES('09$Ux3Y999qBeEKV8wCsfA',#65,$,$,#78,(#90));
#102= IFCPOLYLINE((#106,#111,#115,#119,#123,#127,#131));
#106= IFCCARTESIANPOINT((0.,318.8494));
#111= IFCCARTESIANPOINT((95.250002,318.8494));
#115= IFCCARTESIANPOINT((120.65,293.4494));
#119= IFCCARTESIANPOINT((120.65,25.4));
#123= IFCCARTESIANPOINT((95.250002,0.));
#127= IFCCARTESIANPOINT((0.,0.));
#131= IFCCARTESIANPOINT((0.,318.8494));
#135= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#102);
#136= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#139= IFCEXTRUDEDAREASOLID(#135,#136,#33,19.1);
#142= IFCCARTESIANPOINT((0.,0.,0.));
#146= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#139));
#152= IFCPRODUCTDEFINITIONSHAPE($,$,(#146));
#156= IFCCARTESIANPOINT((5240.3064,7493.,-337.7327));
#160= IFCDIRECTION((1.,0.,0.));
#164= IFCDIRECTION((0.,1.,0.));
#168= IFCAXIS2PLACEMENT3D(#156,#160,#164);
#171= IFCLOCALPLACEMENT($,#168);
#174= IFCPLATE('13c6Dt0003iJ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#171,#152,$);
#193= IFCRELCONTAINEDINSPATIALSTRUCTURE('0119IaMyb8cxCzo7l7Qnbw',#65,$,$,(#174,#268,#350,#434,#521,#633,#720,#808,#895,#982,#1078,#1193,#1385,#1473),#90);
#195= IFCSTRUCTURALPROFILEPROPERTIES('PL19.1',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#196= IFCPOLYLINE((#200,#204,#208,#212,#216,#220,#224));
#200= IFCCARTESIANPOINT((0.,293.4494));
#204= IFCCARTESIANPOINT((25.4,318.8494));
#208= IFCCARTESIANPOINT((120.65,318.8494));
#212= IFCCARTESIANPOINT((120.65,0.));
#216= IFCCARTESIANPOINT((25.4,0.));
#220= IFCCARTESIANPOINT((0.,25.4));
#224= IFCCARTESIANPOINT((0.,293.4494));
#228= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL19.1',#196);
#229= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#232= IFCEXTRUDEDAREASOLID(#228,#229,#33,19.1);
#235= IFCCARTESIANPOINT((0.,0.,0.));
#240= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#232));
#246= IFCPRODUCTDEFINITIONSHAPE($,$,(#240));
#250= IFCCARTESIANPOINT((5240.3064,7626.35,-337.7327));
#254= IFCDIRECTION((1.,0.,0.));
#258= IFCDIRECTION((0.,1.,0.));
#262= IFCAXIS2PLACEMENT3D(#250,#254,#258);
#265= IFCLOCALPLACEMENT($,#262);
#268= IFCPLATE('13c6Dt0003gZ4nCpGmDZ4q',#65,'PLATE','PL19.1',$,#265,#246,$);
#287= IFCPOLYLINE((#291,#295,#299,#303,#307));
#291= IFCCARTESIANPOINT((0.,0.));
#295= IFCCARTESIANPOINT((0.,318.45248));
#299= IFCCARTESIANPOINT((99.695007,318.45248));
#303= IFCCARTESIANPOINT((99.695007,0.));
#307= IFCCARTESIANPOINT((0.,0.));
#311= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#287);
#312= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#315= IFCEXTRUDEDAREASOLID(#311,#312,#33,9.5);
#318= IFCCARTESIANPOINT((0.,0.,0.));
#322= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#315));
#328= IFCPRODUCTDEFINITIONSHAPE($,$,(#322));
#332= IFCCARTESIANPOINT((4411.98,7492.5555,-342.9125));
#336= IFCDIRECTION((0.,0.,-1.));
#340= IFCDIRECTION((0.,1.,0.));
#344= IFCAXIS2PLACEMENT3D(#332,#336,#340);
#347= IFCLOCALPLACEMENT($,#344);
#350= IFCPLATE('13c6Dt0002ip4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#347,#328,$);
#370= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#371= IFCPOLYLINE((#375,#379,#383,#387,#391));
#375= IFCCARTESIANPOINT((0.,0.));
#379= IFCCARTESIANPOINT((0.,318.45248));
#383= IFCCARTESIANPOINT((99.695007,318.45248));
#387= IFCCARTESIANPOINT((99.695007,0.));
#391= IFCCARTESIANPOINT((0.,0.));
#395= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#371);
#396= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#399= IFCEXTRUDEDAREASOLID(#395,#396,#33,9.5);
#402= IFCCARTESIANPOINT((0.,0.,0.));
#406= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#399));
#412= IFCPRODUCTDEFINITIONSHAPE($,$,(#406));
#416= IFCCARTESIANPOINT((4411.98,7492.5555,-28.5875));
#420= IFCDIRECTION((0.,0.,-1.));
#424= IFCDIRECTION((0.,1.,0.));
#428= IFCAXIS2PLACEMENT3D(#416,#420,#424);
#431= IFCLOCALPLACEMENT($,#428);
#434= IFCPLATE('13c6Dt0002hp4nCpGmDZ4o',#65,'PLATE','PL9.5',$,#431,#412,$);
#453= IFCCARTESIANPOINT((0.,0.));
#457= IFCDIRECTION((1.,0.));
#461= IFCAXIS2PLACEMENT2D(#453,#457);
#464= IFCRECTANGLEPROFILEDEF(.AREA.,$,#461,9.5,234.9);
#465= IFCDIRECTION((0.,0.,-1.));
#469= IFCDIRECTION((-1.,0.,0.));
#473= IFCAXIS2PLACEMENT3D(#21,#469,#33);
#476= IFCEXTRUDEDAREASOLID(#464,#473,#465,304.8);
#479= IFCCARTESIANPOINT((0.,-117.45,-4.75));
#483= IFCBOUNDINGBOX(#479,304.8,234.9,9.5);
#486= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#476));
#492= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#483));
#499= IFCPRODUCTDEFINITIONSHAPE($,$,(#486,#492));
#503= IFCCARTESIANPOINT((4581.8225,7496.2,-342.9));
#507= IFCDIRECTION((-1.,0.,0.));
#511= IFCDIRECTION((0.,0.,1.));
#515= IFCAXIS2PLACEMENT3D(#503,#507,#511);
#518= IFCLOCALPLACEMENT($,#515);
#521= IFCCOLUMN('13c6Dt0002c34nCpGmDZ4o',#65,'PLATE','PL9.5X234.9',$,#518,#499,$);
#540= IFCSTRUCTURALPROFILEPROPERTIES('PL9.5X234.9',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#541= IFCCARTESIANPOINT((0.,0.));
#545= IFCDIRECTION((1.,0.));
#549= IFCAXIS2PLACEMENT2D(#541,#545);
#552= IFCISHAPEPROFILEDEF(.AREA.,$,#549,177.41901,402.84399,9.5249996,10.922,17.653);
#553= IFCDIRECTION((0.,0.,-1.));
#557= IFCDIRECTION((-1.,0.,0.));
#561= IFCAXIS2PLACEMENT3D(#21,#557,#33);
#564= IFCEXTRUDEDAREASOLID(#552,#561,#553,2278.7992);
#567= IFCCARTESIANPOINT((0.,-201.422,-88.709503));
#571= IFCBOUNDINGBOX(#567,778.79918,402.84399,177.41901);
#574= IFCCARTESIANPOINT((778.79918,301.422,0.));
#578= IFCDIRECTION((1.,0.,0.));
#582= IFCDIRECTION((0.,0.,-1.));
#586= IFCAXIS2PLACEMENT3D(#574,#578,#582);
#589= IFCPLANE(#586);
#592= IFCHALFSPACESOLID(#589,.F.);
#595= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#564,#592);
#598= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#595));
#604= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#571));
#610= IFCPRODUCTDEFINITIONSHAPE($,$,(#598,#604));
#614= IFCCARTESIANPOINT((4572.,6688.3563,-201.422));
#618= IFCDIRECTION((1.,0.,0.));
#623= IFCDIRECTION((0.,1.,0.));
#627= IFCAXIS2PLACEMENT3D(#614,#618,#623);
#630= IFCLOCALPLACEMENT($,#627);
#633= IFCBEAM('13c6Dt0001iZ4nCpGmDZ0v',#65,'BEAM','W16X36',$,#630,#610,$);
#652= IFCSTRUCTURALPROFILEPROPERTIES('W16X36',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#653= IFCCARTESIANPOINT((0.,0.));
#657= IFCDIRECTION((1.,0.));
#661= IFCAXIS2PLACEMENT2D(#653,#657);
#664= IFCLSHAPEPROFILEDEF(.AREA.,$,#661,101.6,88.900002,9.5249996,9.525,$,$,$,$);
#665= IFCDIRECTION((0.,0.,-1.));
#669= IFCDIRECTION((-1.,0.,0.));
#673= IFCAXIS2PLACEMENT3D(#21,#669,#33);
#676= IFCEXTRUDEDAREASOLID(#664,#673,#665,254.);
#679= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
#683= IFCBOUNDINGBOX(#679,254.,101.6,88.900002);
#686= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#676));
#692= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#683));
#698= IFCPRODUCTDEFINITIONSHAPE($,$,(#686,#692));
#702= IFCCARTESIANPOINT((4794.758,7562.85,-298.45));
#706= IFCDIRECTION((1.,0.,0.));
#710= IFCDIRECTION((0.,0.,1.));
#714= IFCAXIS2PLACEMENT3D(#702,#706,#710);
#717= IFCLOCALPLACEMENT($,#714);
#720= IFCCOLUMN('13c6Dt0001GZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#717,#698,$);
#739= IFCSTRUCTURALPROFILEPROPERTIES('L4X3-1/2X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#740= IFCCARTESIANPOINT((0.,0.));
#744= IFCDIRECTION((1.,0.));
#748= IFCAXIS2PLACEMENT2D(#740,#744);
#752= IFCLSHAPEPROFILEDEF(.AREA.,$,#748,101.6,88.900002,9.5249996,9.525,$,$,$,$);
#753= IFCDIRECTION((0.,0.,-1.));
#757= IFCDIRECTION((-1.,0.,0.));
#761= IFCAXIS2PLACEMENT3D(#21,#757,#33);
#764= IFCEXTRUDEDAREASOLID(#752,#761,#753,254.);
#767= IFCCARTESIANPOINT((0.,-50.799999,-44.450001));
#771= IFCBOUNDINGBOX(#767,254.,101.6,88.900002);
#774= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#764));
#780= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#771));
#786= IFCPRODUCTDEFINITIONSHAPE($,$,(#774,#780));
#790= IFCCARTESIANPOINT((4794.758,7677.15,-44.45));
#794= IFCDIRECTION((1.,0.,0.));
#798= IFCDIRECTION((0.,0.,-1.));
#802= IFCAXIS2PLACEMENT3D(#790,#794,#798);
#805= IFCLOCALPLACEMENT($,#802);
#808= IFCBEAM('13c6Dt0001FZ4nCpGmDZ0u',#65,'ANGLE','L4X3-1/2X3/8',$,#805,#786,$);
#827= IFCCARTESIANPOINT((0.,0.));
#831= IFCDIRECTION((1.,0.));
#835= IFCAXIS2PLACEMENT2D(#827,#831);
#838= IFCLSHAPEPROFILEDEF(.AREA.,$,#835,101.6,101.60025,9.5249996,9.525,$,$,$,$);
#839= IFCDIRECTION((0.,0.,-1.));
#843= IFCDIRECTION((-1.,0.,0.));
#847= IFCAXIS2PLACEMENT3D(#21,#843,#33);
#850= IFCEXTRUDEDAREASOLID(#838,#847,#839,292.1);
#853= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
#857= IFCBOUNDINGBOX(#853,292.1,101.6,101.60025);
#860= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#850));
#866= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#857));
#872= IFCPRODUCTDEFINITIONSHAPE($,$,(#860,#866));
#876= IFCCARTESIANPOINT((4915.409,7564.4494,50.799999));
#881= IFCDIRECTION((0.,-1.,0.));
#885= IFCDIRECTION((1.,0.,0.));
#889= IFCAXIS2PLACEMENT3D(#876,#881,#885);
#892= IFCLOCALPLACEMENT($,#889);
#895= IFCBEAM('13c6Dt0000F34nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#892,#872,$);
#914= IFCSTRUCTURALPROFILEPROPERTIES('L4X4X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#915= IFCCARTESIANPOINT((0.,0.));
#919= IFCDIRECTION((1.,0.));
#923= IFCAXIS2PLACEMENT2D(#915,#919);
#926= IFCLSHAPEPROFILEDEF(.AREA.,$,#923,101.6,101.60025,9.5249996,9.525,$,$,$,$);
#927= IFCDIRECTION((0.,0.,-1.));
#931= IFCDIRECTION((-1.,0.,0.));
#935= IFCAXIS2PLACEMENT3D(#21,#931,#33);
#938= IFCEXTRUDEDAREASOLID(#926,#935,#927,330.2);
#941= IFCCARTESIANPOINT((0.,-50.799999,-50.800125));
#945= IFCBOUNDINGBOX(#941,330.2,101.6,101.60025);
#948= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#938));
#954= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#945));
#960= IFCPRODUCTDEFINITIONSHAPE($,$,(#948,#954));
#964= IFCCARTESIANPOINT((4801.108,7564.4494,495.301));
#968= IFCDIRECTION((0.,-1.,0.));
#972= IFCDIRECTION((0.,0.,-1.));
#976= IFCAXIS2PLACEMENT3D(#964,#968,#972);
#979= IFCLOCALPLACEMENT($,#976);
#982= IFCBEAM('13c6Dt00006J4nCpGmDZ0s',#65,'BEAM','L4X4X3/8',$,#979,#960,$);
#1001= IFCPOLYLINE((#1006,#1010,#1014,#1018,#1022,#1026));
#1006= IFCCARTESIANPOINT((0.,0.));
#1010= IFCCARTESIANPOINT((-0.001,486.84845));
#1014= IFCCARTESIANPOINT((369.07135,486.84845));
#1018= IFCCARTESIANPOINT((512.75543,343.16437));
#1022= IFCCARTESIANPOINT((512.75543,0.));
#1026= IFCCARTESIANPOINT((0.,0.));
#1030= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL9.5',#1001);
#1031= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#1034= IFCEXTRUDEDAREASOLID(#1030,#1031,#33,9.5);
#1037= IFCCARTESIANPOINT((-0.0010532137,0.,-4.75));
#1041= IFCBOUNDINGBOX(#1037,486.8495,512.75614,9.5);
#1044= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1034));
#1050= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1041));
#1056= IFCPRODUCTDEFINITIONSHAPE($,$,(#1044,#1050));
#1060= IFCCARTESIANPOINT((4763.008,7615.262,12.701));
#1064= IFCDIRECTION((0.,1.,0.));
#1068= IFCDIRECTION((0.,0.,1.));
#1072= IFCAXIS2PLACEMENT3D(#1060,#1064,#1068);
#1075= IFCLOCALPLACEMENT($,#1072);
#1078= IFCPLATE('13c6Dt00005J4nCpGmDZ0s',#65,'PLATE','PL9.5',$,#1075,#1056,$);
#1097= IFCCARTESIANPOINT((0.,0.));
#1101= IFCDIRECTION((1.,0.));
#1105= IFCAXIS2PLACEMENT2D(#1097,#1101);
#1108= IFCRECTANGLEHOLLOWPROFILEDEF(.AREA.,$,#1105,152.39999,152.39999,9.5249996,9.5250004,19.05);
#1109= IFCDIRECTION((0.,0.,-1.));
#1113= IFCDIRECTION((-1.,0.,0.));
#1117= IFCAXIS2PLACEMENT3D(#1130,#1113,#33);
#1120= IFCEXTRUDEDAREASOLID(#1108,#1117,#1109,2177.4228);
#1123= IFCCARTESIANPOINT((-1.2141754E-10,-76.199997,-76.199997));
#1127= IFCBOUNDINGBOX(#1123,677.4228,152.39999,152.39999);
#1130= IFCCARTESIANPOINT((-1500.,0.,0.));
#1135= IFCCARTESIANPOINT((-1.0913936E-10,-9.0949470E-12,4.7379999));
#1139= IFCDIRECTION((-1.,-1.6052583E-13,0.));
#1143= IFCDIRECTION((-1.6052583E-13,1.,0.));
#1147= IFCAXIS2PLACEMENT3D(#1135,#1139,#1143);
#1150= IFCPLANE(#1147);
#1153= IFCHALFSPACESOLID(#1150,.F.);
#1156= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1120,#1153);
#1159= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1156));
#1165= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1127));
#1171= IFCPRODUCTDEFINITIONSHAPE($,$,(#1159,#1165));
#1175= IFCCARTESIANPOINT((5007.3895,7620.,282.98953));
#1179= IFCDIRECTION((0.,-1.,0.));
#1183= IFCDIRECTION((0.70710678,0.,0.70710678));
#1187= IFCAXIS2PLACEMENT3D(#1175,#1179,#1183);
#1190= IFCLOCALPLACEMENT($,#1187);
#1193= IFCBEAM('13c6Dt00003p4nCpGmDZ0r',#65,'BRACE','TS6X6X3/8',$,#1190,#1171,$);
#1212= IFCCARTESIANPOINT((5153.6546,7620.012,307.34937));
#1216= IFCDIRECTION((0.,-1.,0.));
#1220= IFCDIRECTION((-0.70710678,0.,0.70710678));
#1224= IFCAXIS2PLACEMENT3D(#1212,#1216,#1220);
#1227= IFCLOCALPLACEMENT($,#1224);
#1230= IFCCARTESIANPOINT((0.,0.));
#1234= IFCDIRECTION((1.,0.));
#1238= IFCAXIS2PLACEMENT2D(#1230,#1234);
#1241= IFCRECTANGLEPROFILEDEF(.AREA.,$,#1238,9.5,266.7);
#1242= IFCDIRECTION((0.,0.,-1.));
#1246= IFCDIRECTION((-1.,0.,0.));
#1250= IFCAXIS2PLACEMENT3D(#21,#1246,#33);
#1253= IFCEXTRUDEDAREASOLID(#1241,#1250,#1242,172.39997);
#1256= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1253));
#1263= IFCPRODUCTDEFINITIONSHAPE($,$,(#1256));
#1267= IFCOPENINGELEMENT('13c6Dt0000Ip4nCpGmDZ0s',#65,$,$,$,#1227,#1263,$);
#1288= IFCRELVOIDSELEMENT('3gz_1FcMHExhEMJzH8zl$M',#65,$,$,#1193,#1267);
#1289= IFCSTRUCTURALPROFILEPROPERTIES('TS6X6X3/8',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#1290= IFCCARTESIANPOINT((0.,0.));
#1294= IFCDIRECTION((1.,0.));
#1298= IFCAXIS2PLACEMENT2D(#1290,#1294);
#1301= IFCISHAPEPROFILEDEF(.AREA.,$,#1298,254.88901,356.616,12.7,18.288,21.3995);
#1302= IFCDIRECTION((0.,0.,-1.));
#1306= IFCDIRECTION((-1.,0.,0.));
#1310= IFCAXIS2PLACEMENT3D(#1323,#1306,#33);
#1313= IFCEXTRUDEDAREASOLID(#1301,#1310,#1302,2768.2595);
#1316= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
#1320= IFCBOUNDINGBOX(#1316,1268.2595,356.616,254.88901);
#1323= IFCCARTESIANPOINT((-1500.,0.,0.));
#1327= IFCCARTESIANPOINT((0.,278.308,0.));
#1331= IFCDIRECTION((-1.,0.,0.));
#1335= IFCDIRECTION((0.,0.,1.));
#1339= IFCAXIS2PLACEMENT3D(#1327,#1331,#1335);
#1342= IFCPLANE(#1339);
#1345= IFCHALFSPACESOLID(#1342,.F.);
#1348= IFCBOOLEANCLIPPINGRESULT(.DIFFERENCE.,#1313,#1345);
#1351= IFCSHAPEREPRESENTATION(#40,'Body','Clipping',(#1348));
#1357= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1320));
#1363= IFCPRODUCTDEFINITIONSHAPE($,$,(#1351,#1357));
#1367= IFCCARTESIANPOINT((4763.008,7620.,-178.308));
#1371= IFCDIRECTION((0.,-1.,0.));
#1375= IFCDIRECTION((1.,0.,-7.2832534E-15));
#1379= IFCAXIS2PLACEMENT3D(#1367,#1371,#1375);
#1382= IFCLOCALPLACEMENT($,#1379);
#1385= IFCBEAM('13c6Dt00002p4nCpGmDZ0r',#65,'BEAM','W14X68',$,#1382,#1363,$);
#1405= IFCSTRUCTURALPROFILEPROPERTIES('W14X68',$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$,$);
#1406= IFCCARTESIANPOINT((0.,0.));
#1410= IFCDIRECTION((1.,0.));
#1414= IFCAXIS2PLACEMENT2D(#1406,#1410);
#1417= IFCISHAPEPROFILEDEF(.AREA.,$,#1414,254.88901,356.616,12.7,18.288,21.3995);
#1418= IFCDIRECTION((0.,0.,-1.));
#1422= IFCDIRECTION((-1.,0.,0.));
#1426= IFCAXIS2PLACEMENT3D(#21,#1422,#33);
#1429= IFCEXTRUDEDAREASOLID(#1417,#1426,#1418,1524.);
#1432= IFCCARTESIANPOINT((0.,-178.308,-127.4445));
#1436= IFCBOUNDINGBOX(#1432,1524.,356.616,254.88901);
#1439= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1429));
#1445= IFCSHAPEREPRESENTATION(#43,$,'BoundingBox',(#1436));
#1451= IFCPRODUCTDEFINITIONSHAPE($,$,(#1439,#1445));
#1455= IFCCARTESIANPOINT((4572.,7620.,-609.6));
#1459= IFCDIRECTION((0.,-1.,0.));
#1463= IFCDIRECTION((7.1613756E-15,0.,1.));
#1467= IFCAXIS2PLACEMENT3D(#1455,#1459,#1463);
#1470= IFCLOCALPLACEMENT($,#1467);
#1473= IFCCOLUMN('13c6Dt00001p4nCpGmDZ0q',#65,'COLUMN','W14X68',$,#1470,#1451,$);
#1492= IFCCARTESIANPOINT((4661.154,7490.05,308.83104));
#1496= IFCDIRECTION((0.,1.,0.));
#1500= IFCDIRECTION((-1.,0.,7.0134555E-15));
#1504= IFCAXIS2PLACEMENT3D(#1492,#1496,#1500);
#1507= IFCLOCALPLACEMENT($,#1504);
#1510= IFCPOLYLINE((#1514,#1519,#1523,#1527,#1531,#1535,#1539,#1543,#1547,#1551,#1555,#1559,#1563,#1567,#1571,#1575,#1579,#1583,#1587,#1591,#1595,#1599,#1603,#1607,#1611,#1615,#1619,#1623,#1627,#1631,#1635,#1639,#1643,#1648,#1652,#1656,#1660));
#1514= IFCCARTESIANPOINT((0.38429439,16.098194));
#1519= IFCCARTESIANPOINT((0.,20.));
#1523= IFCCARTESIANPOINT((0.,336.616));
#1527= IFCCARTESIANPOINT((0.38429439,340.5178));
#1531= IFCCARTESIANPOINT((1.5224093,344.26967));
#1535= IFCCARTESIANPOINT((3.3706078,347.7274));
#1539= IFCCARTESIANPOINT((5.8578644,350.75813));
#1543= IFCCARTESIANPOINT((8.8885953,353.24539));
#1547= IFCCARTESIANPOINT((12.346331,355.09359));
#1551= IFCCARTESIANPOINT((16.098194,356.2317));
#1555= IFCCARTESIANPOINT((20.,356.616));
#1559= IFCCARTESIANPOINT((158.308,356.616));
#1563= IFCCARTESIANPOINT((162.20981,356.2317));
#1567= IFCCARTESIANPOINT((165.96167,355.09359));
#1571= IFCCARTESIANPOINT((169.4194,353.24539));
#1575= IFCCARTESIANPOINT((172.45013,350.75813));
#1579= IFCCARTESIANPOINT((174.93739,347.7274));
#1583= IFCCARTESIANPOINT((176.78559,344.26967));
#1587= IFCCARTESIANPOINT((177.9237,340.5178));
#1591= IFCCARTESIANPOINT((178.308,336.616));
#1595= IFCCARTESIANPOINT((178.308,20.));
#1599= IFCCARTESIANPOINT((177.9237,16.098194));
#1603= IFCCARTESIANPOINT((176.78559,12.346331));
#1607= IFCCARTESIANPOINT((174.93739,8.8885953));
#1611= IFCCARTESIANPOINT((172.45013,5.8578644));
#1615= IFCCARTESIANPOINT((169.4194,3.3706078));
#1619= IFCCARTESIANPOINT((165.96167,1.5224093));
#1623= IFCCARTESIANPOINT((162.20981,0.38429439));
#1627= IFCCARTESIANPOINT((158.308,0.));
#1631= IFCCARTESIANPOINT((20.,0.));
#1635= IFCCARTESIANPOINT((16.098194,0.38429439));
#1639= IFCCARTESIANPOINT((12.346331,1.5224093));
#1643= IFCCARTESIANPOINT((8.8885953,3.3706078));
#1648= IFCCARTESIANPOINT((5.8578644,5.8578644));
#1652= IFCCARTESIANPOINT((3.3706078,8.8885953));
#1656= IFCCARTESIANPOINT((1.5224093,12.346331));
#1660= IFCCARTESIANPOINT((0.38429439,16.098194));
#1664= IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,'PL259.9',#1510);
#1665= IFCAXIS2PLACEMENT3D(#21,#33,#25);
#1668= IFCEXTRUDEDAREASOLID(#1664,#1665,#33,259.9);
#1671= IFCSHAPEREPRESENTATION(#40,'Body','SweptSolid',(#1668));
#1677= IFCPRODUCTDEFINITIONSHAPE($,$,(#1671));
#1681= IFCOPENINGELEMENT('13c6Dt0002m34nCpGmDZ4o',#65,$,$,$,#1507,#1677,$);
#1702= IFCRELVOIDSELEMENT('2WI1gpRhj1dwB5Pft5vGvO',#65,$,$,#1473,#1681);
#1703= IFCRELASSOCIATESMATERIAL('1GOaWfy957kRCtp2R28$a9',#9,$,$,(#1193),#59);
#1705= IFCRELASSOCIATESMATERIAL('3Vqwp2DmT3TeKWHZyFzO7N',#9,$,$,(#1473,#1385,#633),#56);
#1707= IFCRELASSOCIATESMATERIAL('0SMrmHf_5BRAYDVih2VNTN',#9,$,$,(#1078,#982,#895,#808,#720,#521,#434,#350,#268,#174),#53);
#1709= IFCRELASSOCIATESPROFILEPROPERTIES('2mzP0x3pj2GwUm7Wy7Xfs0',#9,$,$,(#633),#652,$,$);
#1711= IFCRELASSOCIATESPROFILEPROPERTIES('1iUJLoCsfD6ug6keUDRGVW',#9,$,$,(#808),#739,$,$);
#1713= IFCRELASSOCIATESPROFILEPROPERTIES('1LidMEK_vEQuG8Js1MD1ND',#9,$,$,(#895,#982),#914,$,$);
#1715= IFCRELASSOCIATESPROFILEPROPERTIES('22dp9maVb0sAxZG8F2xTIR',#9,$,$,(#1193),#1289,$,$);
#1717= IFCRELASSOCIATESPROFILEPROPERTIES('36LPEeuXn6VgX3frK_urjy',#9,$,$,(#1385),#1405,$,$);
ENDSEC;
END-ISO-10303-21;
@@ -0,0 +1,47 @@
const fs = require( 'fs' )
const TMP_RESULTS_PATH = '/tmp/import_result.json'
const { parseAndCreateCommit } = require( './index' )
async function main() {
let cmdArgs = process.argv.slice( 2 )
let [ filePath, userId, streamId, branchName, commitMessage ] = cmdArgs
console.log( 'ARGV: ', filePath, userId, streamId, branchName, commitMessage )
const data = fs.readFileSync( filePath )
let ifcInput = {
data,
streamId: streamId,
userId: userId,
message: commitMessage || 'Imported file'
}
if ( branchName ) ifcInput.branchName = branchName
let output = {
success: false,
error: 'Unknown error'
}
try {
let commitId = await parseAndCreateCommit( ifcInput )
output = {
success: true,
commitId
}
} catch ( err ) {
output = {
success: false,
error: err.toString()
}
}
fs.writeFileSync( TMP_RESULTS_PATH, JSON.stringify( output ) )
process.exit( 0 )
}
main()
+54
View File
@@ -0,0 +1,54 @@
const fetch = require( 'node-fetch' )
const Parser = require( './parser' )
const ServerAPI = require( './api.js' )
async function parseAndCreateCommit( { data, streamId, branchName = 'uploads', userId, message = 'Manual IFC file upload' } ) {
const serverApi = new ServerAPI( { streamId } )
const myParser = new Parser( { serverApi } )
const { id, tCount } = await myParser.parse( data )
let commit = {
streamId: streamId,
branchName: branchName,
objectId: id,
message: message,
sourceApplication: 'IFC',
totalChildrenCount: tCount
}
let branch = await serverApi.getBranchByNameAndStreamId( { streamId: streamId, name: branchName } )
if( !branch ) {
await serverApi.createBranch( {
name: branchName,
streamId: streamId,
description: branchName === 'uploads' ? 'File upload branch' : null,
authorId: userId
} )
}
let userToken = process.env.USER_TOKEN
let server_base_url = process.env.SPECKLE_SERVER_URL || 'http://localhost:3000'
const response = await fetch( server_base_url + '/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${userToken}`
},
body: JSON.stringify( {
query: 'mutation createCommit( $myCommitInput: CommitCreateInput!) { commitCreate( commit: $myCommitInput ) }',
variables:{
myCommitInput: commit
}
} )
} )
let json = await response.json()
console.log( json )
return json.data.commitCreate
}
module.exports = { parseAndCreateCommit }
+251
View File
@@ -0,0 +1,251 @@
const WebIFC = require( 'web-ifc/web-ifc-api-node' )
const ServerAPI = require( './api.js' )
module.exports = class IFCParser {
constructor( { serverApi } ) {
this.api = new WebIFC.IfcAPI()
this.serverApi = serverApi || new ServerAPI()
}
async parse( data ) {
if ( this.api.wasmModule === undefined ) await this.api.Init()
this.modelId = this.api.OpenModel( data, { COORDINATE_TO_ORIGIN: true, USE_FAST_BOOLS: true } )
this.projectId = this.api.GetLineIDsWithType( this.modelId, WebIFC.IFCPROJECT ).get( 0 )
this.project = this.api.GetLine( this.modelId, this.projectId, true )
this.project.__closure = {}
this.cache = {}
this.closureCache = {}
// Steps: create and store in speckle all the geometries (meshes) from this project and store them
// as reference objects in this.productGeo
this.productGeo = {}
await this.createGeometries()
console.log( `Geometries created: ${Object.keys( this.productGeo ).length} meshes.` )
// Lastly, traverse the ifc project object and parse it into something friendly; as well as
// replace all its geometries with actual references to speckle meshes from the productGeo map
await this.traverse( this.project, true, 0 )
let id = await this.serverApi.saveObject( this.project )
return { id, tCount: Object.keys( this.project.__closure ).length }
}
async createGeometries() {
this.rawGeo = this.api.LoadAllGeometry( this.modelId )
for( let i = 0; i < this.rawGeo.size(); i++ ) {
const mesh = this.rawGeo.get( i )
const prodId = mesh.expressID
this.productGeo[prodId ] = []
for( let j = 0; j < mesh.geometries.size(); j++ ) {
let placedGeom = mesh.geometries.get( j )
let geom = this.api.GetGeometry( this.modelId, placedGeom.geometryExpressID )
let matrix = placedGeom.flatTransformation
let raw = {
color: geom.color, // NOTE: material: x, y, z = rgb, w = opacity
vertices: this.api.GetVertexArray( geom.GetVertexData(), geom.GetVertexDataSize() ),
indices: this.api.GetIndexArray( geom.GetIndexData(), geom.GetIndexDataSize() )
}
const { vertices } = this.extractVertexData( raw.vertices )
for( let k = 0; k < vertices.length; k += 3 ) {
let x = vertices[k], y = vertices[k + 1], z = vertices[k + 2]
vertices[k] = matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12]
vertices[k + 1] = ( matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14] ) * -1
vertices[k + 2] = matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13]
}
// Since all faces are triangles, we must add a `0` before each group of 3.
let spcklFaces = [ ]
for ( let i = 0; i < raw.indices.length; i++ ) {
if( i % 3 === 0 )
spcklFaces.push( 0 )
spcklFaces.push( raw.indices[i] )
}
// Create a propper Speckle Mesh
let spcklMesh = {
speckle_type: 'Objects.Geometry.Mesh',
units: 'm',
volume: 0,
area: 0,
faces: spcklFaces,
vertices: Array.from( vertices ),
renderMaterial: placedGeom.color ? this.colorToMaterial( placedGeom.color ) : null
}
let id = await this.serverApi.saveObject( spcklMesh )
let ref = { speckle_type: 'reference', referencedId: id }
this.productGeo[prodId].push( ref )
}
}
}
async traverse( element, recursive = true, depth = 0 ) {
// Fast exit if null/undefined
if ( !element ) return
// If array, traverse all items in it.
if( Array.isArray( element ) ) {
return element.map( async el => await this.traverse( el,recursive, depth + 1 ) )
}
// If it has no expressID, its either a simple type or a { type, value } object.
if( !element.expressID ) {
return element.value !== null && element.value !== undefined ? element.value : element
}
if( this.cache[element.expressID.toString()] ) return this.cache[element.expressID.toString()]
// If you got here -> It's an IFC Element: create base object, upload and return ref.
// console.log( `Traversing element ${element.expressID}; Recurse: ${recursive}; Stack ${depth}` )
// Traverse all key/value pairs first.
for( let key of Object.keys( element ) ) {
element[key] = await this.traverse( element[key], recursive, depth + 1 )
}
// Assign speckle_type and empty closure table.
element.speckle_type = element.constructor.name
element.__closure = {}
// Find spatial children and assign to element
const spatialChildrenIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELAGGREGATES, 'RelatingObject', 'RelatedObjects' )
if( spatialChildrenIds.length > 0 ) element.rawSpatialChildren = spatialChildrenIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) )
// Find children and populate element
const childrenIds = this.getAllRelatedItemsOfType( element.expressID, WebIFC.IFCRELCONTAINEDINSPATIALSTRUCTURE, 'RelatingStructure', 'RelatedElements' )
if( childrenIds.length > 0 ) element.rawChildren = childrenIds.map( ( childId ) => this.api.GetLine( this.modelId, childId, true ) )
// Lookup geometry in generated geometries object
if( this.productGeo[element.expressID] ) {
element['@displayValue'] = this.productGeo[element.expressID]
this.productGeo[element.expressID].forEach( ref => {
this.project.__closure[ref.referencedId.toString()] = depth
element.__closure[ref.referencedId.toString()] = 1
} )
}
// Recurse all children
if ( recursive ) {
if( element.rawSpatialChildren ) {
element.spatialChildren = []
for( let child of element.rawSpatialChildren ) {
let res = await this.traverse( child, recursive, depth + 1 )
if( res.referencedId ) {
element.spatialChildren.push( res )
this.project.__closure[res.referencedId.toString()] = depth
element.__closure[res.referencedId.toString()] = 1
// adds to parent (this element) the child's closure tree.
if( this.closureCache[child.expressID.toString()] ) {
for( let key of Object.keys( this.closureCache[child.expressID.toString()] ) ) {
element.__closure[key] = this.closureCache[child.expressID.toString()][key] + 1
}
}
}
}
delete element.rawSpatialChildren
}
if ( element.rawChildren ) {
element.children = []
for( let child of element.rawChildren ) {
let res = await this.traverse( child, recursive, depth + 1 )
if( res.referencedId ) {
element.children.push( res )
this.project.__closure[res.referencedId.toString()] = depth
element.__closure[res.referencedId.toString()] = 1
// adds to parent (this element) the child's closure tree.
if( this.closureCache[child.expressID.toString()] ) {
for( let key of Object.keys( this.closureCache[child.expressID.toString()] ) ) {
element.__closure[key] = this.closureCache[child.expressID.toString()][key] + 1
}
}
}
}
delete element.rawChildren
}
if( element.children || element.spatialChildren ) {
console.log( `${element.constructor.name} ${element.GlobalId}: children count: ${ element.children ? element.children.length : '0'}; spatial children count: ${element.spatialChildren ? element.spatialChildren.length : '0'} ` )
}
}
if( this.productGeo[element.expressID] || element.spatialChildren || element.children ) {
let id = await this.serverApi.saveObject( element )
let ref = { speckle_type: 'reference', referencedId: id }
this.cache[element.expressID.toString()] = ref
this.closureCache[element.expressID.toString()] = element.__closure
return ref
} else {
this.cache[element.expressID.toString()] = element
this.closureCache[element.expressID.toString()] = element.__closure
return element
}
}
// (c) https://github.com/agviegas/web-ifc-three
extractVertexData( vertexData ) {
const vertices = []
const normals = []
let isNormalData = false
for ( let i = 0; i < vertexData.length; i++ ) {
isNormalData ? normals.push( vertexData[i] ) : vertices.push( vertexData[i] )
if ( ( i + 1 ) % 3 === 0 ) isNormalData = !isNormalData
}
return { vertices, normals }
}
// (c) https://github.com/agviegas/web-ifc-three/blob/907e08b5673d5e1c18261a4fceade7189d6b2db7/src/IFC/PropertyManager.ts#L110
getAllRelatedItemsOfType( elementID, type, relation, relatedProperty ) {
const lines = this.api.GetLineIDsWithType( this.modelId, type )
const IDs = []
for ( let i = 0; i < lines.size(); i++ ) {
const relID = lines.get( i )
const rel = this.api.GetLine( this.modelId, relID )
const relatedItems = rel[relation]
let foundElement = false
if ( Array.isArray( relatedItems ) ) {
const values = relatedItems.map( ( item ) => item.value )
foundElement = values.includes( elementID )
} else foundElement = ( relatedItems.value === elementID )
if ( foundElement ) {
const element = rel[relatedProperty]
if ( !Array.isArray( element ) ) IDs.push( element.value )
else element.forEach( ( ele ) => IDs.push( ele.value ) )
}
}
return IDs
}
colorToMaterial( color ) {
let intColor = ( color.w << 24 ) + ( ( color.x * 255 ) << 16 ) + ( ( color.y * 255 ) << 8 ) + ( ( color.z * 255 ) )
return {
diffuse: intColor,
opacity: color.w,
metalness: 0,
roughness: 1,
speckle_type: 'Objects.Other.RenderMaterial'
}
}
}
+8
View File
@@ -0,0 +1,8 @@
'use strict'
module.exports = require( 'knex' )( {
client: 'pg',
connection: process.env.PG_CONNECTION_STRING || 'postgres://speckle:speckle@localhost/speckle',
pool: { min: 1, max: 1 }
// migrations are in managed in the server package
} )
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@speckle/fileimport-service",
"version": "2.0.0",
"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",
"license": "SEE LICENSE IN readme.md",
"main": "index.js",
"repository": {
"type": "git",
"url": "git+https://github.com/specklesystems/speckle-server.git"
},
"scripts": {
"dev": "nodemon ./src/daemon.js"
},
"bugs": {
"url": "https://github.com/specklesystems/speckle-server/issues"
},
"dependencies": {
"aws-sdk": "^2.996.0",
"bcrypt": "^5.0.1",
"crypto-random-string": "^3.3.1",
"eslint": "^7.29.0",
"knex": "^0.95.11",
"node-fetch": "^2.6.5",
"pg": "^8.7.1",
"web-ifc": "0.0.24"
},
"devDependencies": {
"nodemon": "^2.0.13"
}
}
+181
View File
@@ -0,0 +1,181 @@
'use strict'
const crypto = require( 'crypto' )
const knex = require( '../knex' )
const { getFileStream } = require( './filesApi' )
const fs = require( 'fs' )
const { spawn } = require( 'child_process' )
const ServerAPI = require( '../ifc/api' )
const TMP_FILE_PATH = '/tmp/file_to_import'
const TMP_RESULTS_PATH = '/tmp/import_result.json'
async function startTask() {
let { rows } = await knex.raw( `
UPDATE file_uploads
SET
"convertedStatus" = 1,
"convertedLastUpdate" = NOW()
FROM (
SELECT "id" FROM file_uploads
WHERE "convertedStatus" = 0 AND "uploadComplete" = 't'
ORDER BY "convertedLastUpdate" ASC
LIMIT 1
) as task
WHERE file_uploads."id" = task."id"
RETURNING file_uploads."id"
` )
return rows[0]
}
async function doTask( task ) {
let tempUserToken = null
let serverApi = null
try {
console.log( 'Doing task ', task )
let { rows } = await knex.raw( `
SELECT
id as "fileId", "streamId", "branchName", "userId", "fileName", "fileType"
FROM file_uploads
WHERE id = ?
LIMIT 1
`, [ task.id ] )
let info = rows[0]
if ( !info ) {
throw new Error( 'Internal error: DB inconsistent' )
}
let upstreamFileStream = await getFileStream( { fileId: info.fileId } )
let diskFileStream = fs.createWriteStream( TMP_FILE_PATH )
upstreamFileStream.pipe( diskFileStream )
await new Promise( fulfill => diskFileStream.on( 'finish' , fulfill ) )
serverApi = new ServerAPI( { streamId: info.streamId } )
let { token } = await serverApi.createToken( { userId: info.userId, name: 'temp upload token', scopes: [ 'streams:write', 'streams:read' ], lifespan: 1000000 } )
tempUserToken = token
await runProcessWithTimeout(
'node',
[
'./ifc/import_file.js',
TMP_FILE_PATH,
info.userId,
info.streamId,
info.branchName,
`File upload: ${info.fileName}`
],
{
USER_TOKEN: tempUserToken
},
10 * 60 * 1000
)
let output = JSON.parse( fs.readFileSync( TMP_RESULTS_PATH ) )
if ( !output.success )
throw new Error( output.error )
let commitId = output.commitId
await knex.raw( `
UPDATE file_uploads
SET
"convertedStatus" = 2,
"convertedLastUpdate" = NOW(),
"convertedMessage" = 'File converted successfully',
"convertedCommitId" = ?
WHERE "id" = ?
`, [ commitId, task.id ] )
} catch ( err ) {
console.log( 'Error: ', err )
await knex.raw( `
UPDATE file_uploads
SET
"convertedStatus" = 3,
"convertedLastUpdate" = NOW(),
"convertedMessage" = ?
WHERE "id" = ?
`, [ err.toString(), task.id ] )
}
if ( fs.existsSync( TMP_FILE_PATH ) ) fs.unlinkSync( TMP_FILE_PATH )
if ( fs.existsSync( TMP_RESULTS_PATH ) ) fs.unlinkSync( TMP_RESULTS_PATH )
if ( tempUserToken ) {
await serverApi.revokeTokenById( tempUserToken )
}
}
function runProcessWithTimeout( cmd, cmdArgs, extraEnv, timeoutMs ) {
return new Promise( ( resolve, reject ) => {
console.log( `Starting process: ${cmd} ${cmdArgs}` )
const childProc = spawn( cmd, cmdArgs, { env: { ...process.env, ...extraEnv } } )
childProc.stdout.on( 'data', ( data ) => {
console.log( 'Parser: ', data.toString() )
} )
childProc.stderr.on( 'data', ( data ) => {
console.error( 'Parser: ', data.toString() )
} )
let timedOut = false
let timeout = setTimeout( () => {
console.log( 'Process timeout. Killing process...' )
timedOut = true
childProc.kill( 9 )
reject( `Timeout: Process took longer than ${timeoutMs} ms to execute` )
}, timeoutMs )
childProc.on( 'close', ( code ) => {
console.log( `Process exited with code ${code}` )
if ( timedOut ) return // ignore `close` calls after killing (the promise was already rejected)
clearTimeout( timeout )
if ( code === 0 ) {
resolve()
} else {
reject( `Parser exited with code ${code}` )
}
} )
} )
}
async function tick() {
try {
let task = await startTask()
if ( !task ) {
setTimeout( tick, 1000 )
return
}
await doTask( task )
// Check for another task very soon
setTimeout( tick, 10 )
} catch ( err ) {
console.log( 'Error executing task: ', err )
setTimeout( tick, 5000 )
}
}
async function main() {
console.log( 'Starting FileUploads Service...' )
tick()
}
main()
@@ -0,0 +1,40 @@
/* istanbul ignore file */
'use strict'
const S3 = require( 'aws-sdk/clients/s3' )
function getS3Config()
{
// TODO: use ENV
return {
accessKeyId: process.env.S3_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.S3_SECRET_KEY || 'minioadmin',
endpoint: process.env.S3_ENDPOINT || 'http://127.0.0.1:9000',
s3ForcePathStyle: true,
signatureVersion: 'v4'
}
}
module.exports = {
async getFileStream( { fileId } ) {
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET
let Key = `files/${fileId}`
let fileStream = s3.getObject( { Key, Bucket } ).createReadStream()
return fileStream
},
async readFile( { fileId } ) {
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET
let Key = `files/${fileId}`
let s3Data = await s3.getObject( { Key, Bucket } ).promise()
return s3Data.Body
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"include": [
"./src/**/*"
]
}
@@ -0,0 +1,130 @@
<template>
<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-tooltip="`Download the original file`"
@click="downloadOriginalFile()"
>
<v-icon>mdi-download</v-icon>
</v-app-bar-nav-icon>
<v-toolbar-title>
{{ file.fileName }}
</v-toolbar-title>
<v-spacer></v-spacer>
<template v-if="file.convertedStatus === 0">
<v-btn text disabled>
<span class="mr-2">Queued</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
</v-btn>
</template>
<template v-if="file.convertedStatus === 1">
<v-btn text>
<span class="mr-2">Converting</span>
<v-progress-circular indeterminate :size="20" :width="2"></v-progress-circular>
</v-btn>
</template>
<template v-if="file.convertedStatus === 2">
<v-btn
text
color="primary"
:to="`/streams/${$route.params.streamId}/commits/${file.convertedCommitId}`"
>
<span class="mr-2">View Commit</span>
<v-icon class="">mdi-open-in-new</v-icon>
</v-btn>
</template>
<template v-if="file.convertedStatus === 3">
<v-btn v-tooltip="file.convertedMessage" text>
<span class="mr-2 error--text">Error</span>
<v-icon color="error">mdi-bug</v-icon>
</v-btn>
</template>
</v-toolbar>
</div>
<div v-else>
<v-skeleton-loader
class="mx-auto"
max-width="300"
type="list-item-one-line"
></v-skeleton-loader>
</div>
</v-card>
</template>
<script>
import gql from 'graphql-tag'
export default {
props: {
fileId: {
type: String,
default: null
}
},
data: () => ({
percentCompleted: -1,
error: null,
file: null
}),
apollo: {
file: {
query: gql`
query File($id: String!, $streamId: String!) {
stream(id: $streamId) {
id
fileUpload(id: $id) {
id
convertedCommitId
userId
convertedStatus
convertedMessage
fileName
fileType
uploadComplete
uploadDate
convertedLastUpdate
}
}
}
`,
variables() {
return {
id: this.fileId,
streamId: this.$route.params.streamId
}
},
skip() {
return !this.fileId
},
update: (data) => data.stream.fileUpload
}
},
watch: {
file(val) {
if (val.convertedStatus >= 2) this.$apollo.queries.file.stopPolling()
}
},
mounted() {
this.$apollo.queries.file.startPolling(1000)
},
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>
@@ -0,0 +1,96 @@
<template>
<v-card v-if="file" class="my-4 elevation-1" :loading="percentCompleted != -1">
<template slot="progress">
<v-progress-linear color="primary" height="4" :value="percentCompleted"></v-progress-linear>
</template>
<v-toolbar flat color="transparent">
<v-toolbar-title>
{{ file.name }}
<span class="caption">{{ file.size }}kb</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<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>
export default {
props: ['file', 'branches'],
data: () => ({
percentCompleted: -1,
error: null,
selectedBranch: 'main'
}),
methods: {
upload() {
let data = new FormData()
this.error = null
data.append('file', this.file)
let request = new XMLHttpRequest()
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) {
this.$emit('done', this.file.name)
}
}.bind(this)
)
// request finished event
request.addEventListener(
'load',
function () {
if (request.status !== 200) {
this.error = request.response
}
this.$emit('done', this.file.name)
}.bind(this)
)
request.addEventListener(
'error',
function () {
if (request.status !== 200) {
this.error = request.response
}
}.bind(this)
)
try {
request.send(data)
} catch (e) {
this.error = 'There was an error: ' + e
}
}
}
}
</script>
@@ -3,10 +3,10 @@
<v-row justify="center" style="margin-top: 50px" dense>
<v-col cols="12" lg="6" md="6" xl="6" class="d-flex flex-column justify-center align-center">
<v-card flat tile color="transparent" class="pa-0">
<div class="d-flex flex-column justify-space-between align-center mb-10">
<div class="d-flex flex-column justify-space-between align-center mb-10" v-if="showImage">
<v-img contain max-height="200" src="@/assets/emptybox.png"></v-img>
</div>
<div class=" text-center mb-2 space-grotesk">
<div class="text-center mb-2 space-grotesk">
<slot name="default"></slot>
</div>
<v-container style="max-width: 500px">
@@ -48,12 +48,12 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`${hasManager ? 'primary' : ''} mb-4`"
dark
href="https://speckle.systems/features/connectors"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-swap-horizontal</v-icon>
@@ -67,11 +67,11 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
href="https://speckle.systems/tutorials"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-school</v-icon>
@@ -85,11 +85,11 @@
</v-list-item>
<v-list-item
v-if="hasManager"
link
:class="`grey ${$vuetify.theme.dark ? 'darken-4' : 'lighten-4'} mb-4`"
href="https://speckle.guide"
target="_blank"
v-if="hasManager"
>
<v-list-item-icon>
<v-icon>mdi-book-open-variant</v-icon>
@@ -130,6 +130,12 @@
<script>
import gql from 'graphql-tag'
export default {
props: {
showImage: {
type: Boolean,
default: true
}
},
apollo: {
user: {
query: gql`
@@ -146,7 +152,7 @@ export default {
}
},
data() {
return{}
return {}
},
computed: {
rootUrl() {
@@ -160,14 +166,13 @@ export default {
mounted() {
this.checkAccountTimer = setInterval(
function () {
if(!this.hasManager)
this.$apollo.queries.user.refetch()
if (!this.hasManager) this.$apollo.queries.user.refetch()
}.bind(this),
3000
)
},
beforeDestroy() {
clearInterval( this.checkAccountTimer )
clearInterval(this.checkAccountTimer)
},
methods: {}
}
+17 -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,15 @@ 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(`/streams/${val.id}`)
},
search(val) {
console.log(val)
if (val === '42') this.liff = true
}
},
+23 -24
View File
@@ -2,29 +2,23 @@
<div style="display: inline-block">
<v-menu v-if="loggedIn" offset-x open-on-hover>
<template #activator="{ on, attrs }">
<v-avatar
v-if="userById"
class="ma-1"
color="grey lighten-3"
:size="size"
v-bind="attrs"
v-on="on"
>
<v-img v-if="avatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
</v-avatar>
<div v-if="userById" v-on="on">
<user-avatar-icon
:size="size"
:avatar="avatar"
:seed="id"
v-bind="attrs"
class="ma-1"
></user-avatar-icon>
</div>
<v-avatar v-else class="ma-1" :size="size" v-bind="attrs" v-on="on">
<v-img contain src="/logo.svg"></v-img>
</v-avatar>
</template>
<v-card v-if="userById" style="width: 200px" :to="isSelf ? '/profile' : '/profile/' + id">
<v-card-text v-if="!$apollo.loading" class="text-center">
<v-avatar class="my-4" color="grey lighten-3" :size="40">
<v-img v-if="avatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
</v-avatar>
<user-avatar-icon class="my-4" :size="40" :avatar="avatar" :seed="id"></user-avatar-icon>
<!-- Uncomment when email verification is in place -->
<!-- <div v-if="userById.verified" class="mb-1">
<v-chip color="primary" small>
@@ -37,9 +31,9 @@
<b>{{ userById.name }}</b>
</div>
<div class="caption">
{{ userById.company }}
<br/>
{{ userById.bio ? 'Bio: ' + userById.bio : ''}}
{{ userById.company }}
<br />
{{ userById.bio ? 'Bio: ' + userById.bio : '' }}
</div>
</v-card-text>
</v-card>
@@ -51,16 +45,21 @@
</v-card-text>
</v-card>
</v-menu>
<v-avatar v-else class="ma-1" color="grey lighten-3" :size="size">
<v-img v-if="avatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
</v-avatar>
<user-avatar-icon
v-else
class="ma-1"
:size="size"
:avatar="avatar"
:seed="id"
></user-avatar-icon>
</div>
</template>
<script>
import userByIdQuery from '../graphql/userById.gql'
import UserAvatarIcon from '@/components/UserAvatarIcon'
export default {
components: { UserAvatarIcon },
props: {
avatar: String,
name: String,
@@ -1,9 +1,6 @@
<template>
<div v-if="user" style="display: inline-block" class="text-center">
<v-avatar class="ma-1" color="grey lighten-3" :size="size">
<v-img v-if="user.avatar" :src="user.avatar" />
<v-img v-else :src="`https://robohash.org/` + id + `.png?size=40x40`" />
</v-avatar>
<user-avatar-icon :size="size" :avatar="user.avatar" :seed="user.id"></user-avatar-icon>
<p class="text-h6 mt-4">
{{ user.name }}
<br />
@@ -14,8 +11,10 @@
<script>
import { signOut } from '@/auth-helpers'
import userQuery from '../graphql/userById.gql'
import UserAvatarIcon from '@/components/UserAvatarIcon'
export default {
components: { UserAvatarIcon },
props: {
size: {
type: Number,
@@ -0,0 +1,25 @@
<template>
<v-avatar :size="size" color="grey lighten-3">
<v-img v-if="avatar" :src="avatar" />
<v-img v-else :src="`https://robohash.org/${seed}.png?size=${size}x${size}`" />
</v-avatar>
</template>
<script>
export default {
props: {
size: {
type: Number,
required: true
},
seed: {
type: String,
required: true
},
avatar: {
type: String,
default: null
}
}
}
</script>
@@ -35,7 +35,11 @@
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
>
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
<v-toolbar-title><span v-if="isSelf">Hi </span>{{ user.name }}<span v-if="isSelf">!</span></v-toolbar-title>
<v-toolbar-title>
<span v-if="isSelf">Hi</span>
{{ user.name }}
<span v-if="isSelf">!</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn v-if="isSelf" small rounded color="primary" @click="editUser">
<v-icon small class="mr-2">mdi-cog-outline</v-icon>
@@ -53,16 +57,15 @@
<br />
</v-col>
<v-col cols="12" sm="4" class="d-flex justify-center">
<v-avatar
class="elevation-0 align-self-center"
size="100"
@click="avatarDialog = isSelf ? true : false"
v-tooltip="`${isSelf ? 'Change your profile picture' : ''}` "
:style="`${isSelf ? 'cursor: pointer;' : ''}`"
>
<v-img v-if="user.avatar" :src="user.avatar" />
<v-img v-else :src="`https://robohash.org/` + user.id + `.png?size=64x64`" />
</v-avatar>
<div @click="avatarDialog = isSelf ? true : false">
<user-avatar-icon
v-tooltip="`${isSelf ? 'Change your profile picture' : ''}`"
:style="`${isSelf ? 'cursor: pointer;' : ''}`"
:size="100"
:avatar="user.avatar"
:seed="user.id"
></user-avatar-icon>
</div>
</v-col>
</v-row>
<v-card-actions></v-card-actions>
@@ -75,9 +78,10 @@
import gql from 'graphql-tag'
import UserEditDialog from '../components/dialogs/UserEditDialog'
import VImageInput from 'vuetify-image-input/a-la-carte'
import UserAvatarIcon from '@/components/UserAvatarIcon'
export default {
components: { UserEditDialog, VImageInput },
components: { UserAvatarIcon, UserEditDialog, VImageInput },
props: {
user: {
type: Object,
@@ -69,13 +69,13 @@ import userSearchQuery from '@/graphql/userSearch.gql'
export default {
name: 'ServerAdminsCard',
components: { SearchBar, ServerAdminsUser },
props: ['adminUsers'],
data() {
return {
selectedSearchItem: [],
search: '',
userSearch: { items: [] },
addUserMode: false,
adminUsers: []
addUserMode: false
}
},
apollo: {
@@ -1,7 +1,13 @@
<template>
<div class="d-flex align-center pa-3 admin-user-view">
<div class="d-flex flex-grow-1 align-center">
<img height="60pt" width="60pt" class="rounded-circle overflow-hidden elevation-1" contain :src="admin.avatar"/>
<img
height="60pt"
width="60pt"
class="rounded-circle overflow-hidden elevation-1"
contain
:src="admin.avatar"
/>
<div class="d-flex flex-column flex-grow-1 ml-2" style="min-width: 30%">
<span class="subtitle-1">{{ admin.name }}</span>
<span class="caption">
@@ -52,7 +58,7 @@ export default {
admin: {},
widgets: {},
},
data(){
data() {
return {
}
},
@@ -1,6 +1,11 @@
<template>
<v-dialog v-model="show" width="500" @keydown.esc="cancel" :fullscreen="$vuetify.breakpoint.smAndDown">
<v-card :loading="loading" v-if="branch && branch.name !== 'main'">
<v-dialog
v-model="show"
width="500"
:fullscreen="$vuetify.breakpoint.smAndDown"
@keydown.esc="cancel"
>
<v-card v-if="branch && branch.name !== 'main'" :loading="loading">
<v-toolbar color="primary" dark flat>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-pencil</v-icon>
@@ -9,7 +14,9 @@
<v-spacer></v-spacer>
<v-btn icon @click="show = false"><v-icon>mdi-close</v-icon></v-btn>
</v-toolbar>
<v-alert v-show="error" dismissible type="error">
{{ error }}
</v-alert>
<v-form ref="form" v-model="valid" lazy-validation @submit.prevent="agree">
<v-card-text>
<v-text-field
@@ -20,6 +27,10 @@
required
autofocus
></v-text-field>
<p class="caption">
Tip: you can create nested branches by using "/" as a separator in their names. E.g.,
"mep/stage-1" or "arch/sketch-design".
</p>
<v-textarea v-model="branch.description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
@@ -64,6 +75,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',
@@ -72,7 +85,8 @@ export default {
],
isEdit: false,
pendingDelete: false,
allBranchNames: []
allBranchNames: [],
error: null
}
},
apollo: {
@@ -118,6 +132,7 @@ export default {
methods: {
async deleteBranch() {
this.loading = true
this.error = null
this.$matomo && this.$matomo.trackPageView('branch/delete')
try {
await this.$apollo.mutate({
@@ -134,16 +149,18 @@ export default {
}
})
} catch (e) {
console.log(e)
this.error = e.message
}
this.loading = false
this.resolve({
result: true,
deleted: true
})
this.dialog = false
this.showDelete = false
if (!this.error) {
this.resolve({
result: true,
deleted: true
})
this.dialog = false
}
},
open(branch) {
this.dialog = true
@@ -22,6 +22,10 @@
required
autofocus
></v-text-field>
<p class="caption">
Tip: you can create nested branches by using "/" as a separator in their names. E.g.,
"mep/stage-1" or "arch/sketch-design".
</p>
<v-textarea v-model="description" rows="2" label="Description"></v-textarea>
</v-card-text>
<v-card-actions>
@@ -42,18 +46,17 @@ export default {
showError: false,
error: null,
streamId: null,
branchNames: ['main', 'globals'],
reservedBranchNames: ['main', 'globals'],
valid: false,
loading: false,
name: null,
name: '',
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 +64,11 @@ export default {
}
},
computed: {},
watch: {
name(val) {
this.name = val.toLowerCase()
}
},
methods: {
show() {
this.showDialog = true
@@ -80,7 +88,7 @@ export default {
variables: {
params: {
streamId: this.$route.params.streamId,
name: this.name,
name: this.name.toLowerCase(),
description: this.description
}
}
@@ -90,7 +98,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'))
@@ -14,13 +14,11 @@
v-model="name"
:rules="nameRules"
validate-on-blur
required
autofocus
label="Stream Name"
label="Stream Name (optional)"
/>
<v-textarea v-model="description" rows="1" row-height="15" label="Description (optional)" />
<v-switch
inset
v-model="isPublic"
v-tooltip="
isPublic
@@ -28,6 +26,7 @@
can edit it.`
: `Only collaborators can access this stream.`
"
inset
:label="`${isPublic ? 'Public stream' : 'Private stream'}`"
/>
@@ -155,9 +154,12 @@ export default {
},
mounted() {
this.nameRules = [
(v) => !!v || 'Stream name is required.',
(v) => (v && v.length <= 150) || 'Name must be less than 150 characters',
(v) => (v && v.length >= 3) || 'Name must be at least 3 characters'
(v) =>
!v ||
(v.length <= 150 && v.length >= 3) ||
'Stream name must be between 3 and 150 characters.'
// (v) => (!v && v.length <= 150) || 'Name must be less than 150 characters',
// (v) => (!v && v.length >= 3) || 'Name must be at least 3 characters'
]
},
methods: {
+33 -11
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,13 +88,13 @@ const routes = [
meta: {
title: 'Stream | Speckle'
},
component: () => import('@/views/stream/Details_re.vue')
component: () => import('@/views/stream/Details.vue')
},
{
path: 'branches/',
name: 'branches',
redirect: 'branches/main',
redirect: 'branches/main'
},
{
path: 'branches/:branchName*',
@@ -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',
@@ -156,6 +163,15 @@ const routes = [
props: true,
component: () => import('@/views/stream/Webhooks.vue')
},
{
path: 'uploads/',
name: 'uploads',
meta: {
title: 'Stream Uploads | Speckle'
},
props: true,
component: () => import('@/views/stream/Uploads.vue')
},
{
path: 'globals/',
name: 'globals',
@@ -204,11 +220,12 @@ const routes = [
path: 'dashboard',
component: () => import('@/views/admin/AdminOverview.vue')
},
// {
// name: 'Admin | Users',
// path: 'users',
// component: () => import('@/views/admin/AdminUsers.vue')
// },
{
name: 'Admin | Users',
path: 'users',
component: () => import('@/views/admin/AdminUsers.vue'),
props: (route) => ({ ...route.query, ...route.props })
},
// {
// name: 'Admin | Streams',
// path: 'streams',
@@ -218,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')
}
]
}
@@ -253,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 }
}
})
@@ -55,12 +55,9 @@
</v-list-item-content>
</v-list-item>
<v-list-item link to="/profile" v-if="user" style="height: 59px">
<v-list-item v-if="user" link to="/profile" style="height: 59px">
<v-list-item-icon>
<v-avatar size="25">
<v-img v-if="user.avatar" :src="user.avatar" />
<v-img v-else :src="`https://robohash.org/` + user.id + `.png?size=38x38`" />
</v-avatar>
<user-avatar-icon :size="24" :avatar="user.avatar" :seed="user.id"></user-avatar-icon>
</v-list-item-icon>
<v-list-item-content>
@@ -74,7 +71,7 @@
<v-list-item v-if="serverInfo">
<v-list-item-icon>
<v-icon
v-if="serverInfo && isDevServer"
v-if="isDevServer"
v-tooltip="`This is a test server and should not be used in production!`"
color="red"
>
@@ -87,12 +84,14 @@
<v-list-item-subtitle class="caption">
{{ serverInfo.version }}
</v-list-item-subtitle>
<div class="caption">This is a test server and should not be used in production!</div>
<div class="caption">
{{ serverInfo.description }}
</div>
</v-list-item-content>
</v-list-item>
</v-list>
<template v-slot:append>
<template #append>
<v-list dense>
<v-list-item
link
@@ -109,7 +108,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item link @click="signOut()" color="primary" v-if="user">
<v-list-item v-if="user" link color="primary" @click="signOut()">
<v-list-item-icon>
<v-icon small class="ml-1">mdi-account-off</v-icon>
</v-list-item-icon>
@@ -118,7 +117,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item link to="/admin" color="primary" v-if="user && user.role === 'server:admin'">
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
<v-list-item-icon>
<v-icon small class="ml-1">mdi-cog</v-icon>
</v-list-item-icon>
@@ -140,22 +139,22 @@
</v-navigation-drawer>
<v-bottom-navigation fixed xxx-hide-on-scroll class="hidden-sm-and-up elevation-20">
<v-btn color="primary" text to="/" style="height: 100%;">
<v-btn color="primary" text to="/" style="height: 100%">
<span>Feed</span>
<v-icon>mdi-clock-fast</v-icon>
</v-btn>
<v-btn color="primary" text to="/streams" style="height: 100%;">
<v-btn color="primary" text to="/streams" style="height: 100%">
<span>Streams</span>
<v-icon>mdi-folder</v-icon>
</v-btn>
<v-btn color="primary" text to="/profile" style="height: 100%;">
<v-btn color="primary" text to="/profile" style="height: 100%">
<span>Profile</span>
<v-icon>mdi-account</v-icon>
</v-btn>
<v-btn text @click="bottomSheet = true" style="height: 100%;">
<v-btn text style="height: 100%" @click="bottomSheet = true">
<span>More</span>
<v-icon>mdi-dots-horizontal</v-icon>
</v-btn>
@@ -198,7 +197,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item link @click="signOut()" color="primary" v-if="user">
<v-list-item v-if="user" link color="primary" @click="signOut()">
<v-list-item-icon>
<v-icon small class="ml-1">mdi-account-off</v-icon>
</v-list-item-icon>
@@ -207,7 +206,7 @@
</v-list-item-content>
</v-list-item>
<v-list-item link to="/admin" color="primary" v-if="user && user.role === 'server:admin'">
<v-list-item v-if="user && user.role === 'server:admin'" link to="/admin" color="primary">
<v-list-item-icon>
<v-icon small class="ml-1">mdi-cog</v-icon>
</v-list-item-icon>
@@ -252,17 +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: { 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
@@ -308,11 +323,6 @@ export default {
}
}
},
watch: {
$route(to, from) {
this.bottomSheet = false
}
},
computed: {
background() {
let theme = this.$vuetify.theme.dark ? 'dark' : 'light'
@@ -325,6 +335,14 @@ export default {
return localStorage.getItem('uuid') !== null
}
},
watch: {
$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
}
},
methods: {
signOut() {
signOut()
@@ -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) {
+36 -14
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>
@@ -40,29 +39,54 @@
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-item link to="/admin/users">
<v-list-item-icon>
<v-icon small class="mt-1">mdi-account-group</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Users</v-list-item-title>
<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>
@@ -70,7 +94,7 @@ import gql from 'graphql-tag'
export default {
name: 'AdminPanel',
components:{
components: {
ErrorPlaceholder: () => import('@/components/ErrorPlaceholder')
},
data() {
@@ -98,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>
@@ -1,41 +1,41 @@
<template>
<div id="admin-settings">
<v-card rounded="lg" v-if="serverInfo">
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
<v-toolbar-title>{{ serverInfo.name }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<div key="viewPanel">
<div class="d-flex align-center mb-2" v-for="(value, name) in serverDetails" :key="name">
<div class="flex-grow-1">
<div v-if="value.type == 'boolean'">
<p class="mt-2">{{value.label}}</p>
<v-switch
inset
persistent-hint
<v-card v-if="serverInfo" rounded="lg">
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
<v-toolbar-title>{{ serverInfo.name }}</v-toolbar-title>
</v-toolbar>
<v-card-text>
<div key="viewPanel">
<div v-for="(value, name) in serverDetails" :key="name" class="d-flex align-center mb-2">
<div class="flex-grow-1">
<div v-if="value.type == 'boolean'">
<p class="mt-2">{{ value.label }}</p>
<v-switch
v-model="serverModifications[name]"
inset
persistent-hint
class="pa-1 ma-1 caption"
>
<template #label>
<span class="caption">{{ value.hint }}</span>
</template>
</v-switch>
</div>
<v-text-field
v-else
v-model="serverModifications[name]"
class="pa-1 ma-1 caption"
>
<template v-slot:label>
<span class="caption">{{ value.hint }}</span>
</template>
</v-switch>
persistent-hint
:hint="value.hint"
class="ma-0 body-2"
></v-text-field>
</div>
<v-text-field
persistent-hint
v-else
:hint="value.hint"
v-model="serverModifications[name]"
class="ma-0 body-2"
></v-text-field>
</div>
</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn block color="primary" @click="saveEdit" :loading="loading">Save</v-btn>
</v-card-actions>
</v-card>
</v-card-text>
<v-card-actions>
<v-btn block color="primary" :loading="loading" @click="saveEdit">Save</v-btn>
</v-card-actions>
</v-card>
</div>
</template>
+226 -139
View File
@@ -1,155 +1,242 @@
<template>
<div>
<v-card title="Users">
<template slot="menu">
<span class="caption mr-2">Showing XX of YYY users.</span>
<v-menu offset-y left class="rounded-circle">
<template #activator="{ attrs, on }">
<v-btn v-bind="attrs" v-on="on" icon outlined small color="primary" class="mr-2">
<v-icon small>mdi-plus</v-icon>
</v-btn>
</template>
<v-list class="rounded-circle">
<v-list-item>Hi</v-list-item>
<v-list-item>Hi</v-list-item>
<v-list-item>Hi</v-list-item>
</v-list>
</v-menu>
<v-card>
<v-toolbar flat>
<v-toolbar-title>
Server Users Admin
<span v-if="users">(found {{ users.totalCount }} users)</span>
</v-toolbar-title>
</v-toolbar>
<v-text-field
v-model="searchQuery"
class="mx-4 mt-4"
:prepend-inner-icon="'mdi-magnify'"
:loading="$apollo.loading"
label="Search users"
type="text"
single-line
clearable
rounded
filled
dense
></v-text-field>
<v-list v-if="!$apollo.loading" rounded>
<v-list-item-group v-if="users.totalCount > 0" color="primary">
<v-list-item v-for="user in users.items" :key="user.id">
<v-list-item-avatar :size="55">
<user-avatar-icon
class="ml-n2"
:avatar="user.avatar"
:seed="user.id"
:size="50"
></user-avatar-icon>
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
{{ user.name }}
</v-list-item-title>
<v-btn icon outlined small color="primary" class="mr-2">
<v-icon small>mdi-filter</v-icon>
</v-btn>
<v-btn icon outlined small color="primary">
<v-icon small>mdi-sort</v-icon>
</v-btn>
</template>
<user-list-item v-for="user in users" :key="user.id" :admin="user" :widgets="widgets(user)">
<v-menu offset-y left rounded>
<template v-slot:activator="{ attrs, on }">
<v-btn icon small v-bind="attrs" v-on="on" class="ml-2">
<v-icon small>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list nav dense>
<v-tooltip
left
max-width="200pt"
open-delay="500"
v-for="opt in menuOptions"
:key="opt.text"
:disabled="!opt.hint"
>
<template v-slot:activator="{ attrs, on }">
<v-list-item
v-bind="attrs"
v-on="on"
link
@click="opt.action ? opt.action(user) : null"
>
<v-list-item-icon class="mr-3">
<v-icon
small
v-text="opt.icon"
:class="`${opt.color || 'dark'}--text`"
></v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
v-text="opt.text"
:class="`${opt.color || 'dark'}--text`"
></v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
{{ opt.hint }}
</v-tooltip>
</v-list>
</v-menu>
</user-list-item>
<div class="text-center subtitle-2 pt-3">
<v-btn :loading="loading" text width="100%" color="primary" @click="loadMoreUsers">
Load more
</v-btn>
</div>
</v-card>
</div>
<span class="caption">
<v-icon x-small>mdi-email-outline</v-icon>
{{ user.email }}
</span>
<span v-if="user.company" class="caption">
<v-icon x-small>mdi-domain</v-icon>
{{ user.company }}
</span>
<span v-else class="caption">
<v-icon x-small>mdi-help-circle</v-icon>
No company info
</span>
</v-list-item-content>
<v-list-item-action>
<v-select
:value="user.role"
:items="availableRoles"
label="user role"
@change="changeUserRole(user, ...arguments)"
></v-select>
</v-list-item-action>
</v-list-item>
<div class="text-center">
<v-pagination
v-model="currentPage"
:length="numberOfPages"
:total-visible="7"
circle
></v-pagination>
</div>
</v-list-item-group>
</v-list>
<v-skeleton-loader v-else class="mx-auto" type="card"></v-skeleton-loader>
<v-dialog v-model="showConfirmDialog" persistent max-width="600px">
<v-card v-if="showConfirmDialog">
<v-toolbar flat class="mb-6">
<v-toolbar-title>Confirm user role change</v-toolbar-title>
</v-toolbar>
<v-card-text>
<v-alert v-if="newRole === 'server:admin'" type="error">
Make sure you trust {{ manipulatedUser.name }}!
<br />
An admin on the server has access to every resource.
</v-alert>
You are changing {{ manipulatedUser.name }}'s server access role from
{{ roleLookupTable[manipulatedUser.role] }} to {{ roleLookupTable[newRole] }}.
<br />
Proceed?
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="error" text @click="cancelRoleChange">Cancel</v-btn>
<v-btn color="primary" text @click="proceedRoleChange">Proceed</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</template>
<script>
import UserListItem from '@/components/admin/UserListItem'
import gql from 'graphql-tag'
import UserAvatarIcon from '@/components/UserAvatarIcon'
import debounce from 'lodash.debounce'
export default {
name: 'AdminUsers',
components: { UserListItem },
methods: {
viewProfile(user) {
console.log('requesting profile for user with id ', user.id)
},
widgets(user) {
return [
{
icon: 'mdi-eye-outline',
hint: 'Last seen',
value: '< 1 day',
type: 'text',
color: null
},
{
icon: 'mdi-cloud-outline',
hint: 'Streams',
value: user.streams.totalCount,
type: 'number'
},
{
icon: 'mdi-cloud-upload-outline',
hint: 'Commits',
value: user.commits.totalCount,
type: 'number'
},
{
icon: 'mdi-calendar-outline',
hint: 'Joined in',
value: 'Feb/21',
type: 'text'
},
{
icon: 'mdi-decagram-outline',
hint: 'Badges',
value: 55
}
]
},
loadMoreUsers() {
console.log('requested more users!')
this.loading = true
those
setTimeout(() => {
this.loading = false
}, 700)
}
name: 'UserAdmin',
components: { UserAvatarIcon },
props: {
limit: { type: [Number, String], required: false, default: 10 },
page: { type: [Number, String], required: false, default: 1 },
q: { type: String, required: false, default: null }
},
data() {
return {
loading: false,
menuOptions: [
{
text: 'View profile',
icon: 'mdi-account',
action: this.viewProfile
roleLookupTable: {
'server:user': 'User',
'server:admin': 'Admin'
// 'server:archived_user': 'Archived'
},
users: {
items: [],
totalCount: 0
},
currentPage: 1,
searchQuery: null,
showConfirmDialog: false,
manipulatedUser: null,
newRole: null
}
},
computed: {
queryLimit() {
return parseInt(this.limit)
},
queryOffset() {
return (this.page - 1) * this.queryLimit
},
numberOfPages() {
return Math.ceil(this.users.totalCount / this.limit)
},
availableRoles() {
let roleItems = []
for (let role in this.roleLookupTable) {
roleItems.push({ text: this.roleLookupTable[role], value: role })
}
return roleItems
}
},
watch: {
currentPage: function (newPage) {
this.paginateNext(newPage)
},
searchQuery: debounce(function (newQuery) {
this.applySearch(newQuery)
}, 1000)
},
methods: {
changeUserRole(user, args) {
this.manipulatedUser = user
this.newRole = args
console.log(user.role)
console.log(this.newRole)
this.showConfirmDialog = true
},
resetManipulatedUser() {
this.manipulatedUser = null
this.newRole = null
},
cancelRoleChange() {
this.showConfirmDialog = false
this.$apollo.queries.users.refetch()
this.resetManipulatedUser()
},
async proceedRoleChange() {
await this.updateUserRole(this.manipulatedUser.id, this.newRole)
this.resetManipulatedUser()
this.showConfirmDialog = false
},
async updateUserRole(userId, newRole) {
await this.$apollo.mutate({
mutation: gql`
mutation($userId: String!, $newRole: String!) {
userRoleChange(userRoleInput: { id: $userId, role: $newRole })
}
`,
variables: {
userId,
newRole
},
{
text: 'Delete user',
icon: 'mdi-delete',
color: 'error',
hint: "Removes this user's admin privileges. This will not delete the user's account."
update: () => {
this.$apollo.queries.users.refetch()
},
error: (err) => {
console.log(err)
}
],
users:[],
})
},
paginateNext(newPage) {
this.$router.push(this._prepareRoute(newPage, this.limit, this.searchQuery))
},
applySearch(searchQuery) {
this.$router.push(this._prepareRoute(1, this.limit, searchQuery))
},
_prepareRoute(page, limit, query) {
let newRoute = `users?page=${page}&limit=${limit}`
if (query) newRoute = `${newRoute}&q=${query}`
return newRoute
}
},
apollo: {
users: {
query: gql`
query Users($limit: Int, $offset: Int, $query: String) {
users(limit: $limit, offset: $offset, query: $query) {
totalCount
items {
id
suuid
email
name
bio
company
avatar
verified
profiles
role
authorizedApps {
name
}
}
}
}
`,
variables() {
return {
limit: this.queryLimit,
offset: this.queryOffset,
query: this.q
}
}
}
}
}
</script>
<style scoped></style>
@@ -0,0 +1,182 @@
<template>
<div>
<v-card title="Users">
<template slot="menu">
<span class="caption mr-2">Showing XX of YYY users.</span>
<v-menu offset-y left class="rounded-circle">
<template #activator="{ attrs, on }">
<v-btn v-bind="attrs" v-on="on" icon outlined small color="primary" class="mr-2">
<v-icon small>mdi-plus</v-icon>
</v-btn>
</template>
<v-list class="rounded-circle">
<v-list-item>Hi</v-list-item>
<v-list-item>Hi</v-list-item>
<v-list-item>Hi</v-list-item>
</v-list>
</v-menu>
<v-btn icon outlined small color="primary" class="mr-2">
<v-icon small>mdi-filter</v-icon>
</v-btn>
<v-btn icon outlined small color="primary">
<v-icon small>mdi-sort</v-icon>
</v-btn>
</template>
<user-list-item v-for="user in users" :key="user.id" :admin="user" :widgets="widgets(user)">
<v-menu offset-y left rounded>
<template v-slot:activator="{ attrs, on }">
<v-btn icon small v-bind="attrs" v-on="on" class="ml-2">
<v-icon small>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list nav dense>
<v-tooltip
left
max-width="200pt"
open-delay="500"
v-for="opt in menuOptions"
:key="opt.text"
:disabled="!opt.hint"
>
<template v-slot:activator="{ attrs, on }">
<v-list-item
v-bind="attrs"
v-on="on"
link
@click="opt.action ? opt.action(user) : null"
>
<v-list-item-icon class="mr-3">
<v-icon
small
v-text="opt.icon"
:class="`${opt.color || 'dark'}--text`"
></v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
v-text="opt.text"
:class="`${opt.color || 'dark'}--text`"
></v-list-item-title>
</v-list-item-content>
</v-list-item>
</template>
{{ opt.hint }}
</v-tooltip>
</v-list>
</v-menu>
</user-list-item>
<div class="text-center subtitle-2 pt-3">
<v-btn :loading="loading" text width="100%" color="primary" @click="loadMoreUsers">
Load more
</v-btn>
</div>
</v-card>
</div>
</template>
<script>
import UserListItem from '@/components/admin/UserListItem'
import gql from 'graphql-tag'
export default {
name: 'AdminUsers',
components: { UserListItem },
data() {
return {
loading: false,
menuOptions: [
{
text: 'View profile',
icon: 'mdi-account',
action: this.viewProfile
},
{
text: 'Delete user',
icon: 'mdi-delete',
color: 'error',
hint: "Removes this user's admin privileges. This will not delete the user's account."
}
],
users: []
}
},
apollo: {
users: {
prefetch: true,
query: gql`
query Users($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
totalCount
items {
id
suuid
email
name
bio
company
avatar
verified
profiles
role
authorizedApps {
name
}
}
}
}
`
}
},
methods: {
viewProfile(user) {
console.log('requesting profile for user with id ', user.id)
},
widgets(user) {
return [
{
icon: 'mdi-eye-outline',
hint: 'Last seen',
value: '< 1 day',
type: 'text',
color: null
},
{
icon: 'mdi-cloud-outline',
hint: 'Streams',
value: user.streams.totalCount,
type: 'number'
},
{
icon: 'mdi-cloud-upload-outline',
hint: 'Commits',
value: user.commits.totalCount,
type: 'number'
},
{
icon: 'mdi-calendar-outline',
hint: 'Joined in',
value: 'Feb/21',
type: 'text'
},
{
icon: 'mdi-decagram-outline',
hint: 'Badges',
value: 55
}
]
},
loadMoreUsers() {
console.log('requested more users!')
this.loading = true
setTimeout(() => {
this.loading = false
}, 700)
}
}
}
</script>
<style scoped></style>
@@ -18,13 +18,15 @@
</template>
</v-expansion-panel-header>
<v-expansion-panel-content class="">
<p>
<b>Author:</b>
<p class="d-flex align-center">
<b class="mr-1">Author:</b>
{{ app.author.name }}
<v-avatar class="ma-1 ml-2" color="grey lighten-3" :size="20">
<v-img v-if="app.author.avatar" :src="app.author.avatar" />
<v-img v-else :src="`https://robohash.org/` + app.author.id + `.png?size=40x40`" />
</v-avatar>
<user-avatar-icon
:avatar="app.author.avatar"
:seed="app.author.id"
:size="20"
class="ml-1"
></user-avatar-icon>
</p>
<p>
<b>Description:</b>
@@ -65,10 +67,11 @@
<script>
import gql from 'graphql-tag'
import UserAvatar from '../../components/UserAvatarAuthoriseApp'
import UserAvatarIcon from '@/components/UserAvatarIcon'
export default {
name: 'AuthorizeApp',
components: { UserAvatar },
components: { UserAvatar, UserAvatarIcon },
apollo: {
app: {
query: gql`
+11 -13
View File
@@ -6,9 +6,9 @@
<span class="space-grotesk" style="max-width: 80%">{{ stream.branch.name }}</span>
<span class="caption ml-2 mb-2 pb-2">{{ stream.branch.description }}</span>
<v-chip
v-tooltip="`Branch ${stream.branch.name} has ${stream.branch.commits.totalCount} commits`"
class="ml-2 pl-2"
small
v-tooltip="`Branch ${stream.branch.name} has ${stream.branch.commits.totalCount} commits`"
>
<v-icon small>mdi-source-commit</v-icon>
{{ stream.branch.commits.totalCount }}
@@ -17,7 +17,6 @@
</portal>
<portal to="streamActionsBar">
<v-btn
elevation="0"
v-if="
loggedInUserId &&
stream &&
@@ -25,12 +24,13 @@
stream.branch &&
stream.branch.name !== 'main'
"
v-tooltip="'Edit branch'"
elevation="0"
color="primary"
small
rounded
:fab="$vuetify.breakpoint.mdAndDown"
dark
v-tooltip="'Edit branch'"
@click="editBranch()"
>
<v-icon small :class="`${$vuetify.breakpoint.mdAndDown ? '' : 'mr-2'}`">mdi-pencil</v-icon>
@@ -44,11 +44,11 @@
<v-col v-else-if="stream && stream.branch" cols="12" class="pa-0 ma-0">
<branch-edit-dialog ref="editBranchDialog" />
<div style="height: 60vh" v-if="latestCommitObjectUrl">
<div v-if="latestCommitObjectUrl" style="height: 60vh">
<renderer :object-url="latestCommitObjectUrl" show-selection-helper />
</div>
<v-list class="pa-0 ma-0" v-if="stream.branch.commits.items.length > 0">
<v-list v-if="stream.branch.commits.items.length > 0" class="pa-0 ma-0">
<list-item-commit
:commit="latestCommit"
:stream-id="streamId"
@@ -66,19 +66,18 @@
></list-item-commit>
<!-- TODO: pagination -->
</v-list>
</v-col>
<no-data-placeholder
v-if="!$apollo.loading && stream.branch && stream.branch.commits.totalCount === 0"
>
<h2 class="space-grotesk">Branch "{{stream.branch.name}}" has no commits.</h2>
<h2 class="space-grotesk">Branch "{{ stream.branch.name }}" has no commits.</h2>
</no-data-placeholder>
</v-row>
<error-placeholder
error-type="404"
v-if="!$apollo.loading && (error || stream.branch === null)"
error-type="404"
>
<h2>{{ error || `Branch "${$route.params.branchName}" does not exist.` }}</h2>
</error-placeholder>
@@ -162,6 +161,10 @@ export default {
else return null
}
},
mounted() {
if (this.$route.params.branchName === 'globals')
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
},
methods: {
editBranch() {
this.$refs.editBranchDialog.open(this.stream.branch).then((dialog) => {
@@ -181,11 +184,6 @@ export default {
}
})
}
},
mounted() {
console.log(this.$route.params)
if (this.$route.params.branchName === 'globals')
this.$router.push(`/streams/${this.$route.params.streamId}/globals`)
}
}
</script>
@@ -1,5 +1,5 @@
<template>
<v-container style="max-width: 768px;">
<v-container style="max-width: 768px">
<portal to="streamTitleBar">
<div>
<v-icon small class="mr-2 hidden-xs-only">mdi-account-multiple</v-icon>
@@ -7,62 +7,64 @@
</div>
</portal>
<v-alert type="warning" v-if="stream.role !== 'stream:owner'">
<v-alert v-if="stream.role !== 'stream:owner'" type="warning">
Your permission level ({{ stream.role }}) is not high enough to edit this stream's
collaborators.
</v-alert>
<v-card
v-if="serverInfo"
elevation="0"
color="transparent"
:class="`mb-4 py-4`"
>
<v-card v-if="serverInfo" elevation="0" color="transparent" :class="`mb-4 py-4`">
<v-row align="stretch">
<v-col cols="12" sm="4" v-for="role in roles" :key="role.name">
<v-card rounded="lg" style="height: 100%" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} d-flex flex-column`">
<v-toolbar style="flex:none;" flat>
<v-col v-for="role in roles" :key="role.name" cols="12" sm="4">
<v-card
rounded="lg"
style="height: 100%"
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''} d-flex flex-column`"
>
<v-toolbar style="flex: none" flat>
<v-toolbar-title class="text-capitalize">
{{ role.name.split(':')[1] }}s
</v-toolbar-title>
<v-spacer></v-spacer>
<v-badge inline :content="getRoleCount(role.name)" :color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"></v-badge>
<v-badge
inline
:content="getRoleCount(role.name)"
:color="`grey ${$vuetify.theme.dark ? 'darken-1' : 'lighten-1'}`"
></v-badge>
</v-toolbar>
<v-card-text class="flex-grow-1">{{ role.description }}</v-card-text>
<v-card-text class="mt-auto">
<div v-if="role.name === 'stream:reviewer'" class="align-self-end">
<user-avatar
v-for="user in reviewers"
:key="user.id"
:id="user.id"
:key="user.id"
:avatar="user.avatar"
:name="user.name"
:size="30"
/>
<span v-if="reviewers.length===0">No users with this role.</span>
<span v-if="reviewers.length === 0">No users with this role.</span>
</div>
<div v-if="role.name === 'stream:contributor'">
<user-avatar
v-for="user in contributors"
:key="user.id"
:id="user.id"
:key="user.id"
:avatar="user.avatar"
:name="user.name"
:size="30"
/>
<span v-if="contributors.length===0">No users with this role.</span>
<span v-if="contributors.length === 0">No users with this role.</span>
</div>
<div v-if="role.name === 'stream:owner'">
<user-avatar
v-for="user in owners"
:key="user.id"
:id="user.id"
:key="user.id"
:avatar="user.avatar"
:name="user.name"
:size="30"
/>
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
@@ -79,9 +81,9 @@
</template>
<v-toolbar
v-if="stream.role === 'stream:owner'"
flat
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
v-if="stream.role === 'stream:owner'"
>
<v-toolbar-title>
<v-icon small class="mr-2">mdi-account-plus</v-icon>
@@ -152,9 +154,9 @@
:class="`mt-5 ${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
>
<v-toolbar
v-if="stream.role === 'stream:owner'"
flat
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`"
v-if="stream.role === 'stream:owner'"
>
<v-toolbar-title>
<v-icon small class="mr-2">mdi-account-group</v-icon>
@@ -179,8 +181,8 @@
item-value="name"
:items="roles"
class="py-0 my-0"
@change="setUserPermissions(user)"
:disabled="stream.role !== 'stream:owner'"
@change="setUserPermissions(user)"
>
<template #selection="{ item }">
{{ item.name }}
@@ -201,8 +203,8 @@
icon
small
color="error"
@click="removeUser(user)"
:disabled="stream.role !== 'stream:owner'"
@click="removeUser(user)"
>
<v-icon>mdi-close</v-icon>
</v-btn>
@@ -294,10 +296,10 @@ export default {
}
},
methods: {
getRoleCount( role ) {
if(role === 'stream:owner') return this.owners.length || '0'
if(role === 'stream:contributor') return this.contributors.length || '0'
if(role === 'stream:reviewer') return this.reviewers.length || '0'
getRoleCount(role) {
if (role === 'stream:owner') return this.owners.length || '0'
if (role === 'stream:contributor') return this.contributors.length || '0'
if (role === 'stream:reviewer') return this.reviewers.length || '0'
},
async removeUser(user) {
this.loading = true
@@ -368,4 +370,4 @@ export default {
}
}
}
</script>
</script>
@@ -2,13 +2,13 @@
<v-container fluid pa-0 ma-0>
<!-- Stream Page Navigation Drawer -->
<v-navigation-drawer
v-if="!error"
v-model="streamNav"
app
fixed
clipped
:permanent="streamNav && !$vuetify.breakpoint.smAndDown"
v-model="streamNav"
:style="`${!$vuetify.breakpoint.xsOnly ? 'left: 56px' : ''}`"
v-if="!error"
>
<!-- Toolbar holds link to stream home page -->
<v-app-bar
@@ -19,9 +19,9 @@
>
<v-toolbar-title>
<router-link
v-tooltip="stream.name"
:to="`/streams/${stream.id}`"
class="text-decoration-none space-grotesk"
v-tooltip="stream.name"
>
<v-icon class="mr-2 primary--text" style="font-size: 20px">mdi-folder</v-icon>
<b>{{ stream.name }}</b>
@@ -32,18 +32,18 @@
<!-- <v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader> -->
<!-- Top padding hack -->
<div style="display: block; height: 65px" v-if="$vuetify.breakpoint.smAndDown"></div>
<div class="px-4 mt-2" v-if="!loggedIn">
<div v-if="$vuetify.breakpoint.smAndDown" style="display: block; height: 65px"></div>
<div v-if="!loggedIn" class="px-4 mt-2">
<v-btn large block color="primary" to="/authn/login">Log In</v-btn>
</div>
<!-- Various Stream Details -->
<v-card elevation="0" v-if="stream" class="pa-1 mb-0" color="transparent">
<v-card v-if="stream" elevation="0" class="pa-1 mb-0" color="transparent">
<v-card-text class="caption">
<span v-html="parsedDescription"></span>
<router-link
v-if="stream.role === 'stream:owner'"
:to="`/streams/${$route.params.streamId}/settings`"
class="text-decoration-none"
v-if="stream.role === 'stream:owner'"
>
Edit
</router-link>
@@ -78,19 +78,19 @@
:name="collab.name"
></user-avatar>
<v-btn
v-if="stream.collaborators.length > 5"
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
icon
:to="`/streams/${stream.id}/collaborators`"
v-tooltip="`${stream.collaborators.length - 4} more collaborators`"
v-if="stream.collaborators.length > 5"
>
<span class="text-subtitle-1">+{{ stream.collaborators.length - 4 }}</span>
</v-btn>
<v-btn
v-if="stream.collaborators.length <= 5"
v-tooltip="'Manage collaborators'"
icon
:to="`/streams/${stream.id}/collaborators`"
class="ml-2"
v-tooltip="'Manage collaborators'"
v-if="stream.collaborators.length <= 5"
>
<v-avatar>
<v-icon>mdi-account-plus</v-icon>
@@ -106,7 +106,7 @@
</v-card>
<!-- Stream menu options -->
<v-list style="padding-left: 10px" rounded dense class="mt-0 pt-0" v-if="stream">
<v-list v-if="stream" style="padding-left: 10px" rounded dense class="mt-0 pt-0" expand>
<v-list-item link :to="`/streams/${stream.id}`" class="no-overlay">
<v-list-item-icon>
<v-icon small>mdi-home</v-icon>
@@ -117,9 +117,8 @@
</v-list-item>
<!-- Branch menu group -->
<!-- TODO: group by "/", eg. dim/a, dim/b, dim/c should be under a sub-group called "dim". -->
<v-list-group v-model="branchMenuOpen" class="my-2">
<template v-slot:activator>
<template #activator>
<v-list-item-icon>
<v-icon small>mdi-source-branch</v-icon>
</v-list-item-icon>
@@ -127,9 +126,9 @@
</template>
<v-divider class="mb-1"></v-divider>
<v-list-item
link
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
v-if="stream.role !== 'stream:reviewer'"
v-tooltip.bottom="'Create a new branch to help categorise your commits.'"
link
@click="showNewBranchDialog()"
>
<v-list-item-icon>
@@ -143,28 +142,77 @@
</v-list-item-content>
</v-list-item>
<v-list-item
v-if="!$apollo.queries.branchQuery.loading"
v-for="(branch, i) in sortedBranches"
:key="i"
link
:to="`/streams/${stream.id}/branches/${branch.name}`"
>
<v-list-item-icon>
<v-icon small style="padding-top: 10px" v-if="branch.name !== 'main'">
mdi-source-branch
</v-icon>
<v-icon small style="padding-top: 10px" class="primary--text" v-else>mdi-star</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ branch.name }} ({{ branch.commits.totalCount }})
</v-list-item-title>
<v-list-item-subtitle class="caption">
{{ branch.description ? branch.description : 'no description' }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<!-- TODO -->
<div v-if="!$apollo.queries.branchQuery.loading">
<template v-for="(item, i) in groupedBranches">
<v-list-item
v-if="item.type === 'item'"
:key="i"
:to="`/streams/${stream.id}/branches/${item.name}`"
exact
>
<v-list-item-icon>
<v-icon v-if="item.name !== 'main'" small style="padding-top: 10px">
mdi-source-branch
</v-icon>
<v-icon v-else small style="padding-top: 10px" class="primary--text">
mdi-star
</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ item.displayName }} ({{ item.commits.totalCount }})
</v-list-item-title>
<v-list-item-subtitle class="caption">
{{ item.description ? item.description : 'no description' }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-list-group
v-else
:key="i"
sub-group
:value="item.expand"
prepend-icon=""
:group="item.name"
>
<template #activator>
<v-list-item style="overflow: visible">
<v-list-item-icon style="position: relative; left: -26px">
<v-icon style="padding-top: 10px">
{{ item.expand ? 'mdi-chevron-down' : 'mdi-chevron-down' }}
</v-icon>
</v-list-item-icon>
<v-list-item-content style="position: relative; left: -8px">
<v-list-item-title>{{ item.name }}</v-list-item-title>
<v-list-item-subtitle class="caption">
{{ item.children.length }} branches
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</template>
<v-list-item
v-for="(kid, j) in item.children"
:key="j"
:to="`/streams/${stream.id}/branches/${kid.name}`"
exact
>
<v-list-item-icon>
<v-icon small style="padding-top: 10px">mdi-source-branch</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ kid.displayName }} ({{ kid.commits.totalCount }})
</v-list-item-title>
<v-list-item-subtitle class="caption">
{{ kid.description ? kid.description : 'no description' }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
</v-list-group>
</template>
</div>
<v-skeleton-loader v-else type="list-item-two-line"></v-skeleton-loader>
<v-divider class="mb-2"></v-divider>
</v-list-group>
@@ -180,6 +228,15 @@
</v-list-item-content>
</v-list-item>
<v-list-item link :to="`/streams/${stream.id}/uploads`">
<v-list-item-icon>
<v-icon small>mdi-arrow-up</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>Import IFC</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-list-item link :to="`/streams/${stream.id}/webhooks`">
<v-list-item-icon>
<v-icon small>mdi-webhook</v-icon>
@@ -211,25 +268,25 @@
<!-- Stream Page App Bar -->
<v-app-bar
v-if="!error"
app
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`"
flat
clipped-left
v-if="!error"
>
<v-app-bar-nav-icon @click="streamNav = !streamNav" v-if="true || !streamNav">
<v-app-bar-nav-icon v-if="true || !streamNav" @click="streamNav = !streamNav">
<!-- <v-icon v-if="streamNav">mdi-chevron-left</v-icon> -->
</v-app-bar-nav-icon>
<v-toolbar-title class="pl-0">
<router-link
v-if="stream"
v-show="true || !streamNav && !$vuetify.breakpoint.smAndDown"
v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)"
class="text-decoration-none space-grotesk"
:to="`/streams/${stream.id}`"
>
<b>{{ stream.name }}</b>
</router-link>
<span class="mx-2" v-show="true || !streamNav && !$vuetify.breakpoint.smAndDown">/</span>
<span v-show="true || (!streamNav && !$vuetify.breakpoint.smAndDown)" class="mx-2">/</span>
<portal-target name="streamTitleBar" slim style="display: inline-block">
<!-- child routes can teleport things here -->
</portal-target>
@@ -239,17 +296,17 @@
<!-- child routes can teleport buttons here -->
</portal-target>
<v-toolbar-items style="margin-right: -20px">
<v-btn large color="primary" to="/authn/login" v-if="!loggedIn && stream && !streamNav">
<v-btn v-if="!loggedIn && stream && !streamNav" large color="primary" to="/authn/login">
Log In
</v-btn>
<v-btn
elevation="0"
v-if="loggedIn && stream"
@click="openShareStreamDialog()"
v-tooltip="'Share this stream'"
elevation="0"
@click="openShareStreamDialog()"
>
<v-icon small class="mr-2 grey--text" v-if="!stream.isPublic">mdi-lock</v-icon>
<v-icon small class="mr-2 grey--text" v-else>mdi-lock-open</v-icon>
<v-icon v-if="!stream.isPublic" small class="mr-2 grey--text">mdi-lock</v-icon>
<v-icon v-else small class="mr-2 grey--text">mdi-lock-open</v-icon>
<v-icon small class="mr-2">mdi-share-variant</v-icon>
<span class="hidden-md-and-down">Share</span>
</v-btn>
@@ -258,18 +315,18 @@
<!-- Stream Child Routes -->
<v-container
v-if="!error"
:style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px;' : ''}`"
:class="`${$vuetify.breakpoint.xsOnly ? 'pl-0' : ''}`"
fluid
pt-0
pr-0
v-if="!error"
>
<transition name="fade">
<router-view v-if="stream" @refetch-branches="refetchBranches"></router-view>
</transition>
</v-container>
<v-container :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`" v-else>
<v-container v-else :style="`${!$vuetify.breakpoint.xsOnly ? 'padding-left: 56px' : ''}`">
<error-placeholder :error-type="error.toLowerCase().includes('not found') ? '404' : 'access'">
<h2>{{ error }}</h2>
</error-placeholder>
@@ -290,8 +347,8 @@
</v-toolbar>
<v-card-text class="mt-0 mb-0 px-2">
<v-text-field
dark
ref="streamUrl"
dark
filled
rounded
hint="Stream url copied to clipboard. Use it in a connector, or just share it with colleagues!"
@@ -302,8 +359,8 @@
></v-text-field>
<v-text-field
v-if="$route.params.branchName"
dark
ref="branchUrl"
dark
filled
rounded
hint="Branch url copied to clipboard. Most connectors can receive the latest commit from a branch by using this url."
@@ -314,8 +371,8 @@
></v-text-field>
<v-text-field
v-if="$route.params.commitId"
dark
ref="commitUrl"
dark
filled
rounded
hint="Commit url copied to clipboard. Most connectors can receive a specific commit by using this url."
@@ -327,10 +384,10 @@
</v-card-text>
</v-sheet>
<v-sheet
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
v-if="stream"
:class="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
>
<v-toolbar class="transparent" rounded v-if="stream.role === 'stream:owner'" flat>
<v-toolbar v-if="stream.role === 'stream:owner'" class="transparent" rounded flat>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>{{ stream.isPublic ? 'mdi-lock-open' : 'mdi-lock' }}</v-icon>
</v-app-bar-nav-icon>
@@ -339,9 +396,9 @@
</v-toolbar-title>
<v-spacer></v-spacer>
<v-switch
v-model="stream.isPublic"
inset
class="mt-4"
v-model="stream.isPublic"
:loading="swapPermsLoading"
@click="changeVisibility"
></v-switch>
@@ -356,7 +413,6 @@
</v-sheet>
<v-sheet v-if="stream">
<v-toolbar
flat
v-tooltip="
`${
stream.role !== 'stream:owner'
@@ -366,6 +422,7 @@
: ''
}`
"
flat
>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-account-group</v-icon>
@@ -401,8 +458,6 @@
:xxxclass="`${!$vuetify.theme.dark ? 'grey lighten-4' : 'grey darken-4'}`"
>
<v-toolbar
flat
class="transparent"
v-if="!stream.isPublic"
v-tooltip="
`${
@@ -413,6 +468,8 @@
: ''
}`
"
flat
class="transparent"
>
<v-app-bar-nav-icon style="pointer-events: none">
<v-icon>mdi-email</v-icon>
@@ -423,8 +480,8 @@
color="primary"
text
rounded
@click="showStreamInviteDialog()"
:disabled="stream.role !== 'stream:owner'"
@click="showStreamInviteDialog()"
>
Send Invite
</v-btn>
@@ -438,6 +495,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>
@@ -456,8 +534,8 @@ export default {
return {
streamNav: true,
error: '',
commitSnackbar: false,
commitSnackbarInfo: {},
snackbar: false,
snackbarInfo: {},
editStreamDialog: false,
shareStream: false,
branchMenuOpen: false,
@@ -509,6 +587,10 @@ export default {
items {
name
description
author {
id
name
}
commits {
totalCount
}
@@ -521,9 +603,33 @@ export default {
return {
id: this.$route.params.streamId
}
},
update: (data) => {
// console.log(data.branchQuery.branches.items)
return data.branchQuery
}
},
$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!) {
@@ -537,8 +643,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
@@ -547,6 +654,43 @@ export default {
}
},
computed: {
groupedBranches() {
if (!this.branchQuery) return
let branches = this.branchQuery.branches.items
let items = []
for (let b of branches) {
if (b.name === 'globals') continue
let parts = b.name.split('/')
if (parts.length === 1) {
items.push({ ...b, displayName: b.name, type: 'item', children: [] })
} else {
let existing = items.find((i) => i.name === parts[0] && i.type === 'group')
if (!existing) {
existing = { name: parts[0], type: 'group', children: [], expand: false }
items.push(existing)
}
existing.children.push({
...b,
displayName: parts.slice(1).join('/'),
type: 'item'
})
if (this.$route.path.includes(b.name)) existing.expand = true
}
}
let sorted = items.sort((a, b) => {
const nameA = a.name.toLowerCase()
const nameB = b.name.toLowerCase()
if (nameA < nameB) return -1
if (nameA > nameB) return 1
return 0
})
return [
...sorted.filter((it) => it.name === 'main'),
...sorted.filter((it) => it.name !== 'main')
]
// return items
},
streamUrl() {
return `${window.location.origin}/streams/${this.$route.params.streamId}`
},
@@ -580,13 +724,20 @@ export default {
}
},
watch: {
$route(to, from) {
$route(to) {
// Ensures branch menu is open when navigating to a branch url
if (this.$route.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
if (to.name.toLowerCase().includes('branch') && !this.branchMenuOpen)
this.branchMenuOpen = true
// closes any share dialog
this.shareStream = false
this.snackbar = false
}
// branchMenuOpen(val) {
// if (this.$route.name.toLowerCase().includes('branch') && !val)
// this.$nextTick(() => {
// this.branchMenuOpen = true
// })
// }
},
mounted() {
setTimeout(
@@ -606,6 +757,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')
@@ -0,0 +1,229 @@
<template>
<div>
<v-container style="max-width: 768px">
<portal to="streamTitleBar">
<div>
<v-icon small class="mr-2 hidden-xs-only">mdi-arrow-up</v-icon>
<span class="space-grotesk">Import IFC</span>
</div>
</portal>
<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>
<v-icon class="mr-2" small>mdi-arrow-up</v-icon>
<span class="d-inline-block">Import IFC Files - Alpha</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text>
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.
<!-- </v-card-text>
<v-card-text> -->
Thanks to the Open Source
<a href="https://ifcjs.github.io/info/docs/Guide/web-ifc/Introduction" target="_blank">
IFC.js Project
</a>
for making this possible.
</v-card-text>
</v-card>
<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"
>
{{ 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>
<!-- <v-icon class="mr-2" small>mdi-arrow-up</v-icon> -->
<span class="d-inline-block">Previous Uploads</span>
</v-toolbar-title>
</v-toolbar>
<v-card-text>
Here are the previously uploaded files in this stream. Please note, currently processing
time is restricted to 5 minutes - if a file takes longer to process, it will be ignored.
</v-card-text>
</v-card>
<template v-for="file in streamUploads" v-if="!$apollo.loading">
<file-processing-item :key="file.id" :file-id="file.id" />
</template>
<v-card v-if="!$apollo.loading && streamUploads.length === 0" class="my-4 elevation-1">
<v-toolbar dense flat color="transparent">
<v-toolbar-title>No uploads yet.</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
</v-card>
</v-container>
</div>
</template>
<script>
import gql from 'graphql-tag'
export default {
name: 'Webhooks',
components: {
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
}
}
}
`,
update: (data) => data.stream.fileUploads,
variables() {
return {
streamId: this.$route.params.streamId
}
}
}
},
data() {
return {
dragover: false,
loading: false,
stream: null,
files: [],
showUploadDialog: false,
error: null,
dragError: null
}
},
computed: {},
methods: {
onFileSelect(e) {
this.parseFiles(e.target.files)
},
onFileDrop(e) {
this.parseFiles(e.dataTransfer.files)
},
parseFiles(files) {
this.dragover = false
this.dragError = null
for (const file of files) {
console.log(file.name.split('.')[1])
let extension = file.name.split('.')[1]
if (!extension || extension !== 'ifc') {
this.dragError = 'Only IFC file extensions are supported.'
return
}
if (file.size > 50626997) {
this.dragError = 'Your files are too powerful (for now). Maximum upload size is 50mb!'
return
}
if (this.files.findIndex((f) => f.name === file.name) !== -1) {
this.dragError = 'This file is already primed for upload.'
return
}
}
if (files.length > 5) {
this.dragError = 'Maximum five files at a time allowed.'
return
}
this.dragError = null
for (const file of files) {
this.files.push(file)
}
},
uploadCompleted(file) {
const index = this.files.findIndex((f) => f.name === file)
this.files.splice(index, 1)
this.$apollo.queries.streamUploads.refetch()
}
}
}
</script>
+12 -14
View File
@@ -1,15 +1,15 @@
<template>
<div>
<no-data-placeholder
:show-message="false"
v-if="!$apollo.loading && webhooks.length === 0 && stream && stream.role === 'stream:owner'"
:show-message="false"
>
<h2>This stream has no webhooks.</h2>
<p class="caption">
Webhooks allow you to subscribe to a stream's events and get notified of them in real time.
You can then use this to trigger ci apps, automation workflows, and more.
</p>
<template v-slot:actions>
<template #actions>
<v-list rounded class="transparent">
<v-list-item link class="primary mb-4" dark @click="newWebhookDialog = true">
<v-list-item-icon>
@@ -39,24 +39,22 @@
</v-list>
</template>
</no-data-placeholder>
<error-placeholder error-type="access" v-if="error">
<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>
<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" v-if="!$apollo.loading && webhooks.length !== 0">
<v-container v-if="!$apollo.loading && webhooks.length !== 0" style="max-width: 768px">
<portal to="streamTitleBar">
<div>
<v-icon small class="mr-2 hidden-xs-only">mdi-webhook</v-icon>
<span class="space-grotesk">Webhooks</span>
</div>
</portal>
<v-card
elevation="0"
rounded="lg"
:class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`"
>
<v-card elevation="0" rounded="lg" :class="`${!$vuetify.theme.dark ? 'grey lighten-5' : ''}`">
<v-toolbar flat :class="`${!$vuetify.theme.dark ? 'grey lighten-4' : ''}`">
<v-toolbar-title>
<v-icon class="mr-2" small>mdi-webhook</v-icon>
@@ -85,7 +83,7 @@
<span class="d-inline-block">Existing Webhooks</span>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn @click="newWebhookDialog = true" small class="primary" dark>New Webhook</v-btn>
<v-btn small class="primary" dark @click="newWebhookDialog = true">New Webhook</v-btn>
</v-toolbar>
<v-list subheader class="transparent pa-0 ma-0">
<v-list-item v-for="wh in webhooks" :key="wh.id" link style="cursor: default">
@@ -107,12 +105,12 @@
</v-list-item-content>
<v-list-item-action v-if="wh.history.items.length != 0">
<v-btn
v-tooltip="'View status reports'"
icon
@click="
selectedWebhook = wh
statusReportsDialog = true
"
icon
v-tooltip="'View status reports'"
>
<v-icon>mdi-information</v-icon>
</v-btn>
+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' },
+17
View File
@@ -28,6 +28,23 @@ POSTGRES_PASSWORD="speckle"
# this overrides the default database name in postgres
POSTGRES_DB="speckle"
############################################################
# Object storage (S3)
############################################################
# Uncomment to disable file uploads
# DISABLE_FILE_UPLOADS="true"
# S3 Endpoint and credentials
S3_ENDPOINT="http://127.0.0.1:9000"
S3_ACCESS_KEY="minioadmin"
S3_SECRET_KEY="minioadmin"
# Bucket where to store the files
S3_BUCKET="speckle-server"
# Try to create bucket at startup if it doesn't exist
S3_CREATE_BUCKET="true"
############################################################
# Emails
############################################################
@@ -17,11 +17,9 @@ module.exports = async () => {
await registerOrUpdateApp( { ...SpeckleApiExplorer } )
await registerOrUpdateApp( { ...SpeckleDesktopApp } )
await registerOrUpdateApp( { ...SpeckleExcel } )
}
async function registerOrUpdateApp( app ) {
if ( app.scopes && app.scopes === 'all' ) {
// let scopes = await Scopes( ).select( '*' )
// console.log( allScopes.length )
@@ -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 } )
@@ -41,7 +41,7 @@ module.exports = {
Branch: {
async author( parent, args, context, info ) {
if ( parent.userId )
if ( parent.authorId && context.auth )
return await getUserById( { userId: parent.authorId } )
else return null
}
@@ -1,7 +1,7 @@
'use strict'
const appRoot = require( 'app-root-path' )
const { UserInputError } = require( 'apollo-server-express' )
const { getUser, getUserRole, updateUser, deleteUser, searchUsers, getUserById } = require( '../../services/users' )
const { getUser, getUsers, countUsers, getUserRole, updateUser, deleteUser, searchUsers, getUserById, makeUserAdmin, unmakeUserAdmin } = require( '../../services/users' )
const { saveActivity } = require( `${appRoot}/modules/activitystream/services` )
const { validateServerRole, validateScopes } = require( `${appRoot}/modules/shared` )
const zxcvbn = require( 'zxcvbn' )
@@ -28,6 +28,14 @@ module.exports = {
return await getUser( args.id || context.userId )
},
async users( parent, args, context, info ){
await validateServerRole( context, 'server:admin' )
await validateScopes( context.scopes, 'users:read' )
let users = await getUsers ( args.limit, args.offset, args.query )
let totalCount = await countUsers( args.query )
return { totalCount, items: users }
},
async userSearch( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
await validateScopes( context.scopes, 'profile:read' )
@@ -99,6 +107,12 @@ module.exports = {
return true
},
async userRoleChange( parent, args, context, info ) {
let roleChanger = args.userRoleInput.role === 'server:admin' ? makeUserAdmin: unmakeUserAdmin
await roleChanger( { userId: args.userRoleInput.id } )
return true
},
async userDelete( parent, args, context, info ) {
let user = await getUser( context.userId )
@@ -3,6 +3,11 @@ extend type Query {
Gets the profile of a user. If no id argument is provided, will return the current authenticated user's profile (as extracted from the authorization header).
"""
user(id: String): User
"""
Get users from the server in a paginated view. The query search for matches in name, company and email.
"""
users(limit: Int!=25, offset: Int!=0, query: String=null) : UserCollection
userSearch(
query: String!
limit: Int! = 25
@@ -27,6 +32,12 @@ type User {
role: String
}
type UserCollection {
totalCount: Int!
items: [ User ]
}
type UserSearchResultCollection {
cursor: String
items: [UserSearchResult]
@@ -53,6 +64,14 @@ extend type Mutation {
userDelete(userConfirmation: UserDeleteInput!): Boolean!
@hasRole(role: "server:user")
@hasScope(scope: "profile:delete")
userRoleChange(userRoleInput: UserRoleInput!): Boolean!
@hasRole(role: "server:admin")
}
input UserRoleInput {
id: String!
role: String!
}
input UserUpdateInput {
@@ -14,8 +14,8 @@ const { getObjectsStream } = require( '../services/objects' )
const { pipeline, PassThrough } = require( 'stream' )
module.exports = ( app ) => {
app.options( '/api/getobjects/:streamId', cors() )
app.post( '/api/getobjects/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
let hasStreamAccess = await validatePermissionsReadStream( req.params.streamId, req )
if ( !hasStreamAccess.result ) {
@@ -46,6 +46,5 @@ module.exports = ( app ) => {
}
}
)
} )
}
@@ -1,5 +1,6 @@
'use strict'
const zlib = require( 'zlib' )
const cors = require( 'cors' )
const Busboy = require( 'busboy' )
const debug = require( 'debug' )
const appRoot = require( 'app-root-path' )
@@ -11,7 +12,9 @@ const { validatePermissionsWriteStream } = require( './authUtils' )
const { hasObjects } = require( '../services/objects' )
module.exports = ( app ) => {
app.post( '/api/diff/:streamId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
app.options( '/api/diff/:streamId', cors() )
app.post( '/api/diff/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
let hasStreamAccess = await validatePermissionsWriteStream( req.params.streamId, req )
if ( !hasStreamAccess.result ) {
return res.status( hasStreamAccess.status ).end()
@@ -14,7 +14,6 @@ const { SpeckleObjectsStream } = require( './speckleObjectsStream' )
const { pipeline, PassThrough } = require( 'stream' )
module.exports = ( app ) => {
app.options( '/objects/:streamId/:objectId', cors() )
app.get( '/objects/:streamId/:objectId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
@@ -54,7 +53,6 @@ module.exports = ( app ) => {
}
}
)
} )
app.options( '/objects/:streamId/:objectId/single', cors() )
+4 -1
View File
@@ -1,5 +1,6 @@
'use strict'
const zlib = require( 'zlib' )
const cors = require( 'cors' )
const Busboy = require( 'busboy' )
const debug = require( 'debug' )
const appRoot = require( 'app-root-path' )
@@ -13,7 +14,9 @@ const { createObjects, createObjectsBatched } = require( '../services/objects' )
const MAX_FILE_SIZE = 50 * 1024 * 1024
module.exports = ( app ) => {
app.post( '/objects/:streamId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
app.options( '/objects/:streamId', cors() )
app.post( '/objects/:streamId', cors(), contextMiddleware, matomoMiddleware, async ( req, res ) => {
let hasStreamAccess = await validatePermissionsWriteStream( req.params.streamId, req )
if ( !hasStreamAccess.result ) {
return res.status( hasStreamAccess.status ).end()
@@ -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
},
@@ -14,7 +14,7 @@ module.exports = {
async createStream( { name, description, isPublic, ownerId } ) {
let stream = {
id: crs( { length: 10 } ),
name: name || 'Random Stream',
name: name || generateStreamName(),
description: description || '',
isPublic: isPublic !== false,
updatedAt: knex.fn.now( )
@@ -170,3 +170,15 @@ module.exports = {
return await query
}
}
const adjectives = [
'Tall', 'Curved', 'Stacked', 'Purple', 'Pink', 'Rectangular', 'Circular', 'Oval', 'Shiny', 'Speckled', 'Blue', 'Stretched', 'Round', 'Spherical', 'Majestic', 'Symmetrical'
]
const nouns = [
'Building', 'House', 'Treehouse', 'Tower', 'Tunnel', 'Bridge', 'Pyramid', 'Structure', 'Edifice', 'Palace', 'Castle', 'Villa'
]
const generateStreamName = () => {
return `${adjectives[Math.floor( Math.random()*adjectives.length )]} ${nouns[Math.floor( Math.random()*nouns.length )]}`
}
+67 -10
View File
@@ -11,6 +11,14 @@ const Acl = ( ) => knex( 'server_acl' )
const debug = require( 'debug' )
const { deleteStream } = require( './streams' )
const changeUserRole = async ( { userId, role } ) => await Acl().where( { userId: userId } ).update( { role:role } )
const countAdminUsers = async ( ) => {
let [ { count } ] = await Acl( ).where( { role: 'server:admin' } ).count( )
return parseInt ( count )
}
module.exports = {
/*
@@ -20,9 +28,8 @@ module.exports = {
*/
async createUser( user ) {
let [ { count } ] = await Acl( ).where( { role: 'server:admin' } ).count( )
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.' )
@@ -34,12 +41,10 @@ module.exports = {
if ( usr ) throw new Error( 'Email taken. Try logging in?' )
let res = await Users( ).returning( 'id' ).insert( user )
let userRole = await countAdminUsers () === 0 ? 'server:admin' : 'server:user'
if ( parseInt( count ) === 0 ) {
await Acl( ).insert( { userId: res[ 0 ], role: 'server:admin' } )
} else {
await Acl( ).insert( { userId: res[ 0 ], role: 'server:user' } )
}
await Acl( ).insert( { userId: res[ 0 ], role: userRole } )
let loggedUser = { ...user }
delete loggedUser.passwordDigest
@@ -60,7 +65,6 @@ module.exports = {
let existingUser = await Users( ).select( 'id' ).where( { email: user.email } ).first( )
if ( existingUser ) {
if ( user.suuid ) {
await module.exports.updateUser( existingUser.id, { suuid: user.suuid } )
}
@@ -90,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
@@ -135,11 +139,12 @@ 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 )
},
async deleteUser( id ) {
//TODO: check for the last admin user to survive
debug( 'speckle:db' )( 'Deleting user ' + id )
let streams = await knex.raw(
`
@@ -167,5 +172,57 @@ module.exports = {
}
return await Users( ).where( { id: id } ).del( )
},
async getUsers ( limit = 10, offset = 0, searchQuery = null ) {
// sanitize limit
const maxLimit = 200
if ( limit > maxLimit ) limit = maxLimit
let query = Users ( )
if ( searchQuery ) {
query.where( queryBuilder => {
queryBuilder
.where( 'email', 'ILIKE', `%${searchQuery}%` )
.orWhere( 'name', 'ILIKE', `%${searchQuery}%` )
.orWhere( 'company', 'ILIKE', `%${searchQuery}%` )
} )
}
let users = await query.limit( limit ).offset( offset )
users.map( user => delete user.passwordDigest )
return users
},
async makeUserAdmin( { userId } ){
await changeUserRole( { userId, role:'server:admin' } )
},
async unmakeUserAdmin( { userId } ){
// dont delete last admin role
if ( await countAdminUsers() === 1 ){
let currentAdmin = await Acl( ).where( { role: 'server:admin' } ).first()
if ( currentAdmin.userId == userId ) {
throw new Error( 'Cannot remove the last admin role from the server' )
}
}
await changeUserRole( { userId, role:'server:user' } )
},
async countUsers ( searchQuery=null ){
let query = Users()
if ( searchQuery ) {
query.where( queryBuilder => {
queryBuilder
.where( 'email', 'ILIKE', `%${searchQuery}%` )
.orWhere( 'name', 'ILIKE', `%${searchQuery}%` )
.orWhere( 'company', 'ILIKE', `%${searchQuery}%` )
} )
}
let [ userCount ] = await query.count()
return parseInt( userCount.count )
}
}
@@ -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
}
} )
} )
@@ -137,6 +137,29 @@ describe( 'GraphQL API Core @core-api', ( ) => {
} )
} )
describe ( 'User role change', () => {
it ( 'User role is changed', async () => {
let queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
await sendRequest( userA.token, { query } )
queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
expect( queriedUserB.body.data.user.role ).to.equal( 'server:admin' )
expect( queriedUserB.body.data )
query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:user"})}`
await sendRequest( userA.token, { query } )
queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
} )
it ( 'Onyl admins can change user role', async () => {
let query = `mutation { userRoleChange(userRoleInput: {id: "${userB.id}", role: "server:admin"})}`
let res = await sendRequest( userB.token, { query } )
let queriedUserB = await sendRequest( userA.token, { query: ` { user(id:"${userB.id}") { id name email role } }` } )
expect( res.body.errors ).to.exist
expect( queriedUserB.body.data.user.role ).to.equal( 'server:user' )
} )
} )
describe( 'Streams', ( ) => {
it( 'Should create some streams', async ( ) => {
const resS1 = await sendRequest( userA.token, { query: 'mutation { streamCreate(stream: { name: "TS1 (u A) Private", description: "Hello World", isPublic:false } ) }' } )
@@ -635,16 +658,29 @@ describe( 'GraphQL API Core @core-api', ( ) => {
} )
it( 'Should not search for some users if bad request', async ( ) => {
const query_lim = 'query { userSearch( query: "mi" ) { cursor items { id name } } } '
let res = await sendRequest( userB.token, { query: query_lim } )
const queryLim = 'query { userSearch( query: "mi" ) { cursor items { id name } } } '
let res = await sendRequest( userB.token, { query: queryLim } )
expect( res ).to.be.json
expect( res.body.errors ).to.exist
const query_pagination = 'query { userSearch( query: "matteo", limit: 200 ) { cursor items { id name } } } '
res = await sendRequest( userB.token, { query: query_pagination } )
const queryPagination = 'query { userSearch( query: "matteo", limit: 200 ) { cursor items { id name } } } '
res = await sendRequest( userB.token, { query: queryPagination } )
expect( res ).to.be.json
expect( res.body.errors ).to.exist
} )
it ( 'Query users', async () => {
const queryUsers = 'query { users( limit: 2, query: "matteo") {totalCount, items {id name}}}'
let res = await sendRequest( userA.token, { query: queryUsers } )
expect( res ).to.be.json
expect( res.body.errors ).to.not.exist
expect( res.body.data.users.items.length ).to.equal( 2 )
expect( res.body.data.users.totalCount ).to.equal( 10 )
res = await sendRequest( userC.token, { query: queryUsers } )
expect( res ).to.be.json
expect( res.body.errors ).to.exist
} )
} )
describe( 'Streams', ( ) => {
+162 -51
View File
@@ -7,11 +7,12 @@ const appRoot = require( 'app-root-path' )
const { init } = require( `${appRoot}/app` )
const expect = chai.expect
chai.use( chaiHttp )
const knex = require( `${appRoot}/db/knex` )
const { createUser, findOrCreateUser, getUser, searchUsers, updateUser, deleteUser, validatePasssword, updateUserPassword } = 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' )
@@ -30,7 +31,7 @@ const {
const { createObject, createObjects } = require( '../services/objects' )
describe( 'Actors & Tokens @user-services', ( ) => {
describe( 'Actors & Tokens @user-services', () => {
let myTestActor = {
name: 'Dimitrie Stefanescu',
email: 'didimitrie@gmail.com',
@@ -39,28 +40,22 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let otherUser = {}
before( async ( ) => {
await knex.migrate.rollback( )
await knex.migrate.latest( )
before( async () => {
await knex.migrate.rollback()
await knex.migrate.latest()
await init()
let actorId = await createUser( myTestActor )
myTestActor.id = actorId
} )
after( async ( ) => {
await knex.migrate.rollback( )
after( async () => {
await knex.migrate.rollback()
} )
describe( 'Users @core-users', ( ) => {
it( 'First created user should be a server admin', async ( ) => {
} )
it( 'Should create an user', async ( ) => {
describe( 'Users @core-users', () => {
it( 'Should create an user', async () => {
let newUser = { ...myTestActor }
newUser.name = 'Bill Gates'
newUser.email = 'bill@gates.com'
@@ -73,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' } )
@@ -82,9 +95,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
assert.fail( 'short pwd' )
} )
it( 'Should not create an user with the same email', async ( ) => {
let newUser = { }
it( 'Should not create an user with the same email', async () => {
let newUser = {}
newUser.name = 'Bill Gates'
newUser.email = 'bill@gates.com'
newUser.password = 'testthebest'
@@ -99,9 +111,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let ballmerUserId = null
it( 'Find or create should create a user', async ( ) => {
let newUser = { }
it( 'Find or create should create a user', async () => {
let newUser = {}
newUser.name = 'Steve Ballmer Balls'
newUser.email = 'ballmer@balls.com'
newUser.password = 'testthebest'
@@ -109,12 +120,10 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let { id } = await findOrCreateUser( { user: newUser } )
ballmerUserId = id
expect( id ).to.be.a( 'string' )
} )
it( 'Find or create should NOT create a user', async ( ) => {
let newUser = { }
it( 'Find or create should NOT create a user', async () => {
let newUser = {}
newUser.name = 'Steve Ballmer Balls'
newUser.email = 'ballmer@balls.com'
newUser.password = 'testthebest'
@@ -122,19 +131,18 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let { id } = await findOrCreateUser( { user: newUser } )
expect( id ).to.equal( ballmerUserId )
} )
// Note: deletion is more complicated.
it( 'Should delete a user', async ( ) => {
it( 'Should delete a user', async () => {
let soloOwnerStream = { name: 'Test Stream 01', description: 'wonderful test stream', isPublic: true }
let multiOwnerStream = { name: 'Test Stream 02', description: 'another test stream', isPublic: true }
soloOwnerStream.id = await createStream( { ...soloOwnerStream, ownerId: ballmerUserId } )
multiOwnerStream.id = await createStream( { ...multiOwnerStream, ownerId: ballmerUserId } )
await grantPermissionsStream( { streamId: multiOwnerStream.id, userId: myTestActor.id, role: 'stream:owner' } )
// create a branch for ballmer on the multiowner stream
let branch = { name: 'ballmer/dev' }
branch.id = await createBranch( { ...branch, streamId: multiOwnerStream.id, authorId: ballmerUserId } )
@@ -144,8 +152,8 @@ describe( 'Actors & Tokens @user-services', ( ) => {
// create an object and a commit around it on the multiowner stream
let objId = await createObject( multiOwnerStream.id, { pie: 'in the sky' } )
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId:objId, authorId: ballmerUserId } )
let commitId = await createCommitByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev', message: 'breakfast commit', sourceApplication: 'tests', objectId: objId, authorId: ballmerUserId } )
await deleteUser( ballmerUserId )
if ( await getStream( { streamId: soloOwnerStream.id } ) !== undefined ) {
@@ -160,7 +168,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let branches = await getBranchesByStreamId( { streamId: multiOwnerStream.id } )
expect( branches.items.length ).to.equal( 3 )
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName:'ballmer/dev' } )
let branchCommits = await getCommitsByBranchName( { streamId: multiOwnerStream.id, branchName: 'ballmer/dev' } )
expect( branchCommits.commits.length ).to.equal( 1 )
let commit = await getCommitById( { id: commitId } )
@@ -174,18 +182,18 @@ describe( 'Actors & Tokens @user-services', ( ) => {
assert.fail( 'user not deleted' )
} )
it( 'Should get a user', async ( ) => {
it( 'Should get a user', async () => {
let actor = await getUser( myTestActor.id )
expect( actor ).to.not.have.property( 'passwordDigest' )
} )
it( 'Should search and get users', async ( ) => {
it( 'Should search and get users', async () => {
let { users } = await searchUsers( 'gates', 20, null )
expect( users ).to.have.lengthOf( 1 )
expect( users[ 0 ].name ).to.equal( 'Bill Gates' )
expect( users[0].name ).to.equal( 'Bill Gates' )
} )
it( 'Should update a user', async ( ) => {
it( 'Should update a user', async () => {
let updatedActor = { ...myTestActor }
updatedActor.name = 'didimitrie'
@@ -193,10 +201,9 @@ describe( 'Actors & Tokens @user-services', ( ) => {
let actor = await getUser( myTestActor.id )
expect( actor.name ).to.equal( updatedActor.name )
} )
it( 'Should not update password', async ( ) => {
it( 'Should not update password', async () => {
let updatedActor = { ...myTestActor }
updatedActor.password = 'failwhale'
@@ -206,7 +213,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
expect( match ).to.equal( false )
} )
it( 'Should validate user password', async ( ) => {
it( 'Should validate user password', async () => {
let actor = {}
actor.password = 'super-test-200'
actor.email = 'e@ma.il'
@@ -218,11 +225,10 @@ describe( 'Actors & Tokens @user-services', ( ) => {
expect( match ).to.equal( true )
let match_wrong = await validatePasssword( { email: actor.email, password: 'super-test-2000' } )
expect( match_wrong ).to.equal( false )
} )
it( 'Should update the password of a user', async() => {
let id = await createUser( { name: 'D', email:'tester@mcbester.com', password:'H4!b5at+kWls-8yh4Guq' } ) // https://mostsecure.pw
it( 'Should update the password of a user', async () => {
let id = await createUser( { name: 'D', email: 'tester@mcbester.com', password: 'H4!b5at+kWls-8yh4Guq' } ) // https://mostsecure.pw
await updateUserPassword( { id, newPassword: 'Hello Dogs and Cats' } )
let match = await validatePasssword( { email: 'tester@mcbester.com', password: 'Hello Dogs and Cats' } )
@@ -230,21 +236,21 @@ describe( 'Actors & Tokens @user-services', ( ) => {
} )
} )
describe( 'API Tokens @core-apitokens', ( ) => {
describe( 'API Tokens @core-apitokens', () => {
let myFirstToken
let pregeneratedToken
let revokedToken
let someOtherToken
let expireSoonToken
before( async ( ) => {
before( async () => {
pregeneratedToken = await createPersonalAccessToken( myTestActor.id, 'Whabadub', [ 'streams:read', 'streams:write', 'profile:read', 'users:email' ] )
revokedToken = await createPersonalAccessToken( myTestActor.id, 'Mr. Revoked', [ 'streams:read' ] )
someOtherToken = await createPersonalAccessToken( otherUser.id, 'Hello World', [ 'streams:write' ] )
expireSoonToken = await createPersonalAccessToken( myTestActor.id, 'Mayfly', [ 'streams:read' ], 1 ) // 1ms lifespan
} )
it( 'Should create an personal api token', async ( ) => {
it( 'Should create an personal api token', async () => {
let scopes = [ 'streams:write', 'profile:read' ]
let name = 'My Test Token'
@@ -257,7 +263,7 @@ describe( 'Actors & Tokens @user-services', ( ) => {
// expect( test ).to.have.lengthOf( 42 )
// } )
it( 'Should validate a token', async ( ) => {
it( 'Should validate a token', async () => {
let res = await validateToken( pregeneratedToken )
expect( res ).to.have.property( 'valid' )
expect( res.valid ).to.equal( true )
@@ -266,25 +272,130 @@ describe( 'Actors & Tokens @user-services', ( ) => {
expect( res ).to.have.property( 'role' )
} )
it( 'Should revoke an api token', async ( ) => {
it( 'Should revoke an api token', async () => {
await revokeToken( revokedToken, myTestActor.id )
let res = await validateToken( revokedToken )
expect( res ).to.have.property( 'valid' )
expect( res.valid ).to.equal( false )
} )
it( 'Should refuse an expired token', async ( ) => {
it( 'Should refuse an expired token', async () => {
let res = await validateToken( expireSoonToken )
expect( res.valid ).to.equal( false )
// assert.fail( )
} )
it( 'Should get the tokens of an user', async ( ) => {
it( 'Should get the tokens of an user', async () => {
let userTokens = await getUserTokens( myTestActor.id )
expect( userTokens ).to.be.an( 'array' )
expect( userTokens ).to.have.lengthOf( 2 )
} )
} )
} )
describe( 'User admin @user-services', () => {
let myTestActor = {
name: 'Gergo Jedlicska',
email: 'gergo@jedlicska.com',
password: 'sn3aky-1337-b1m'
}
before( async () => {
await knex.migrate.rollback()
await knex.migrate.latest()
await init()
let actorId = await createUser( myTestActor )
myTestActor.id = actorId
} )
after( async () => {
await knex.migrate.rollback()
} )
it( 'First created user should be admin', async () => {
let users = await getUsers( 100, 0 )
expect( users ).to.be.an( 'array' )
expect( users ).to.have.lengthOf( 1 )
let firstUser = users[0]
let userRole = await getUserRole( firstUser.id )
expect( userRole ).to.equal( 'server:admin' )
} )
it( 'Count user knows how to count', async () => {
expect( await countUsers() ).to.equal( 1 )
let newUser = { ...myTestActor }
newUser.name = 'Bill Gates'
newUser.email = 'bill@gates.com'
newUser.password = 'testthebest'
let actorId = await createUser( newUser )
expect( await countUsers() ).to.equal( 2 )
await deleteUser( actorId )
expect( await countUsers() ).to.equal( 1 )
} )
it( 'Get users query limit is sanitized to upper limit', async () => {
let createNewDroid = ( number ) => {
return {
name: `${number}`,
email: `${number}@droidarmy.com`,
password: 'sn3aky-1337-b1m'
}
}
let userInputs = Array( 250 ).fill().map( ( v, i ) => createNewDroid( i ) )
expect( await countUsers() ).to.equal( 1 )
await Promise.all( userInputs.map( userInput => createUser( userInput ) ) )
expect( await countUsers() ).to.equal( 251 )
let users = await getUsers( 2000000 )
expect( users ).to.have.lengthOf( 200 )
} )
it( 'Get users offset is applied', async () => {
let users = await getUsers( 200, 200 )
expect( users ).to.have.lengthOf( 51 )
} )
it( 'User query filters', async () => {
let users = await getUsers( 100, 0, 'gergo' )
expect( users ).to.have.lengthOf( 1 )
let [ user ] = users
expect( user.email ).to.equal( 'gergo@jedlicska.com' )
} )
it( 'Count users applies query', async () => {
expect( await countUsers( 'droid' ) ).to.equal( 250 )
} )
it( 'Change user role modifies role', async () => {
let [ user ] = await getUsers( 1, 10 )
let oldRole = await getUserRole( user.id )
expect( oldRole ).to.equal( 'server:user' )
await makeUserAdmin( { userId: user.id } )
let newRole = await getUserRole( user.id )
expect( newRole ).to.equal( 'server:admin' )
await unmakeUserAdmin( { userId: user.id } )
newRole = await getUserRole( user.id )
expect( newRole ).to.equal( 'server:user' )
} )
it( 'Ensure at least one admin remains in the server', async () => {
try {
await unmakeUserAdmin( { userId: myTestActor.id, role: 'server:admin' } )
assert.fail( 'This should have failed' )
} catch ( err ) {
expect( err.message ).to.equal( 'Cannot remove the last admin role from the server' )
}
} )
} )
@@ -0,0 +1,13 @@
const { getStreamFileUploads, getFileInfo } = require( '../../services/fileuploads' )
module.exports = {
Stream: {
async fileUploads( parent, args, context, info ) {
return await getStreamFileUploads( { streamId:parent.id } )
},
async fileUpload( parent, args, context, info ) {
return await getFileInfo( { fileId: args.id } )
}
}
}
@@ -0,0 +1,39 @@
extend type Stream {
"""
Returns a list of all the file uploads for this stream.
"""
fileUploads: [FileUpload]
"""
Returns a specific file upload that belongs to this stream.
"""
fileUpload(id:String!): FileUpload
}
type FileUpload {
id: String!
streamId: String!
branchName: String
"""
If present, the conversion result is stored in this commit.
"""
convertedCommitId: String
"""
The user's id that uploaded this file.
"""
userId: String!
"""
0 = queued, 1 = processing, 2 = success, 3 = error
"""
convertedStatus: Int!
"""
Holds any errors or info.
"""
convertedMessage: String
fileName: String!
fileType: String!
fileSize: Int!
uploadComplete: Boolean!
uploadDate: DateTime!
convertedLastUpdate: DateTime!
}
@@ -0,0 +1,133 @@
/* istanbul ignore file */
'use strict'
const debug = require( 'debug' )
const express = require( 'express' )
const appRoot = require( 'app-root-path' )
const Busboy = require( 'busboy' )
const cors = require( 'cors' )
const { matomoMiddleware } = require( `${appRoot}/logging/matomoHelper` )
const { contextMiddleware, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
const { checkBucket, uploadFile, getFileInfo, getFileStream } = require( './services/fileuploads' )
const { getStream } = require ( '../core/services/streams' )
exports.init = async ( app, options ) => {
if ( process.env.DISABLE_FILE_UPLOADS ) {
debug( 'speckle:modules' )( '📄 FileUploads module is DISABLED' )
return
} else {
debug( 'speckle:modules' )( '📄 Init FileUploads module' )
}
if ( !process.env.S3_BUCKET ) {
debug( 'speckle:modules' )( 'ERROR: S3_BUCKET env variable was not specified. File uploads will be DISABLED.' )
return
}
await checkBucket()
let checkStreamPermissions = async ( req ) => {
if ( !req.context || !req.context.auth ) {
return { hasPermissions: false, httpErrorCode: 401 }
}
try {
await validateScopes( req.context.scopes, 'streams:write' )
} catch ( err ) {
return { hasPermissions: false, httpErrorCode: 401 }
}
try {
await authorizeResolver( req.context.userId, req.params.streamId, 'stream:contributor' )
} catch ( err ) {
return { hasPermissions: false, httpErrorCode: 401 }
}
return { hasPermissions: true, httpErrorCode: 200 }
}
app.get( '/api/file/:fileId', contextMiddleware, matomoMiddleware, async ( req, res ) => {
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' )
// Check stream read access
let streamId = fileInfo.streamId
const stream = await getStream( { streamId: streamId, userId: req.context.userId } )
if ( !stream ) {
return res.status( 404 ).send( 'File stream not found' )
}
if ( !stream.isPublic && req.context.auth === false ) {
return res.status( 401 ).send( 'You must be logged in to access private streams' )
}
if ( !stream.isPublic ) {
try {
await validateScopes( req.context.scopes, 'streams:read' )
} catch ( err ) {
return res.status( 401 ).send( 'The provided auth token can\'t read streams' )
}
try {
await authorizeResolver( req.context.userId, streamId, 'stream:reviewer' )
} catch ( err ) {
return res.status( 401 ).send( 'You don\'t have access to this private stream' )
}
}
let fileStream = await getFileStream( { fileId: req.params.fileId } )
res.writeHead( 200, { 'Content-Type': 'application/octet-stream', 'Content-Disposition': `attachment; filename="${fileInfo.fileName}"`, } )
fileStream.pipe( res )
} ),
app.post( '/api/file/:fileType/:streamId/:branchName?', contextMiddleware, matomoMiddleware, async ( req, res ) => {
if ( process.env.DISABLE_FILE_UPLOADS ) {
return res.status( 503 ).send( 'File uploads are disabled on this server' )
}
let { hasPermissions, httpErrorCode } = await checkStreamPermissions( req )
if ( !hasPermissions ) {
return res.status( httpErrorCode ).end()
}
let fileUploadPromises = []
let busboy = new Busboy( { headers: req.headers } )
busboy.on( 'file', function( fieldname, file, filename, encoding, mimetype ) {
let promise = uploadFile( {
streamId: req.params.streamId,
branchName: req.params.branchName || '',
userId: req.context.userId,
fileName: filename,
fileType: req.params.fileType,
fileStream: file
} )
fileUploadPromises.push( promise )
} )
busboy.on( 'finish', async function() {
let fileIds = []
for ( let promise of fileUploadPromises )
{
let fileId = await promise
fileIds.push( fileId )
}
res.send( fileIds )
} )
req.pipe( busboy )
} )
}
exports.finalize = () => {}
@@ -0,0 +1,27 @@
/* istanbul ignore file */
'use strict'
exports.up = async knex => {
await knex.schema.createTable( 'file_uploads', table => {
table.string( 'id' ).primary( )
table.string( 'streamId', 10 ).references( 'id' ).inTable( 'streams' ).onDelete( 'cascade' )
table.string( 'branchName' )
table.string( 'userId' )
table.string( 'fileName' ).notNullable( )
table.string( 'fileType' ).notNullable( )
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' )
table.string( 'convertedCommitId' )
table.index( [ 'streamId' ] )
} )
}
exports.down = async knex => {
await knex.schema.dropTableIfExists( 'file_uploads' )
}
@@ -0,0 +1,93 @@
/* istanbul ignore file */
'use strict'
const appRoot = require( 'app-root-path' )
const crs = require( 'crypto-random-string' )
const knex = require( `${appRoot}/db/knex` )
const S3 = require( 'aws-sdk/clients/s3' )
const stream = require( 'stream' )
const FileUploads = ( ) => knex( 'file_uploads' )
function getS3Config()
{
return {
accessKeyId: process.env.S3_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.S3_SECRET_KEY || 'minioadmin',
endpoint: process.env.S3_ENDPOINT || 'http://127.0.0.1:9000' ,
s3ForcePathStyle: true,
signatureVersion: 'v4'
}
}
module.exports = {
async checkBucket() {
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET
try {
let data = await s3.headBucket( { Bucket } ).promise()
return
} catch ( err ) {
if ( err.statusCode === 403 ) {
throw new Error( 'Access denied to S3 bucket ' )
}
if ( process.env.S3_CREATE_BUCKET === 'true' ) {
await s3.createBucket( { Bucket } ).promise()
} else {
throw new Error( `Can't open S3 bucket '${Bucket}': ${err.toString()}` )
}
}
},
async getFileInfo( { fileId } ) {
let fileInfo = await FileUploads( ).where( { id: fileId } ).select( '*' ).first( )
return fileInfo
},
async getStreamFileUploads( { streamId } ) {
let fileInfos = await FileUploads().where( { streamId: streamId } ).select( '*' ).orderBy( [ { column: 'uploadDate', order: 'desc' } ] )
return fileInfos
},
async getFileStream( { fileId } ) {
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET
let Key = `files/${fileId}`
let fileStream = s3.getObject( { Key, Bucket } ).createReadStream()
return fileStream
},
async uploadFile( { streamId, branchName, userId, fileName, fileType, fileStream } ) {
// Create ID and db entry
let fileId = crs( { length: 10 } )
let dbFile = {
id: fileId,
streamId,
branchName,
userId,
fileName,
fileType,
}
await FileUploads( ).insert( dbFile )
// Upload stream
const s3 = new S3( getS3Config() )
let Bucket = process.env.S3_BUCKET
// TODO: error if missing
let Key = `files/${fileId}`
let uploadResponse = await s3.upload( { Bucket, Key, Body: fileStream } ).promise()
// Get file size and update db entry
let headResponse = await s3.headObject( { Key, Bucket } ).promise()
let fileSize = headResponse.ContentLength
await FileUploads().where( { id: fileId } ).update( { uploadComplete: true, fileSize } )
return fileId
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ const { scalarResolvers, scalarSchemas } = require( './core/graph/scalars' )
exports.init = async ( app ) => {
let dirs = fs.readdirSync( `${appRoot}/modules` )
let moduleDirs = [ './core', './auth', './apiexplorer', './emails', './pwdreset', './serverinvites', './previews' ] // TODO: add './invites'
let moduleDirs = [ './core', './auth', './apiexplorer', './emails', './pwdreset', './serverinvites', './previews', './fileuploads' ] // TODO: add './invites'
// Stage 1: initialise all modules
for ( let dir of moduleDirs ) {
@@ -16,12 +16,12 @@ 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!' )
if ( message ) {
if ( message.length >= 1024 ) {
throw new Error( 'Personal message too long.' )
}
@@ -105,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 } ) {
@@ -120,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 ]
}
+108 -3
View File
@@ -2786,6 +2786,49 @@
"resolved": "https://registry.npmjs.org/auto-load/-/auto-load-3.0.4.tgz",
"integrity": "sha512-ufENezHsnouUiIgwCMuqzcdiABBucBb8CV/5uchw9XuMhf8KXIqF3PgxRzhIuW3C470gjb5niq6zaaF9nhjPIQ=="
},
"aws-sdk": {
"version": "2.989.0",
"resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.989.0.tgz",
"integrity": "sha512-sMjvqeF9mEOxXkhOAUjCrBt2iYafclkmaIbgSdjJ+te7zKXeReqrc6P3VgIGUxU8kwmdSro0n1NjrXbzKQJhcw==",
"requires": {
"buffer": "4.9.2",
"events": "1.1.1",
"ieee754": "1.1.13",
"jmespath": "0.15.0",
"querystring": "0.2.0",
"sax": "1.2.1",
"url": "0.10.3",
"uuid": "3.3.2",
"xml2js": "0.4.19"
},
"dependencies": {
"buffer": {
"version": "4.9.2",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz",
"integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==",
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4",
"isarray": "^1.0.0"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
},
"sax": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz",
"integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o="
},
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
}
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
@@ -2796,6 +2839,23 @@
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"dev": true,
"requires": {
"follow-redirects": "^1.14.0"
},
"dependencies": {
"follow-redirects": {
"version": "1.14.4",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz",
"integrity": "sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==",
"dev": true
}
}
},
"backo2": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz",
@@ -5243,6 +5303,11 @@
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz",
"integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q=="
},
"events": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
},
"execa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
@@ -7815,6 +7880,11 @@
"resolved": "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz",
"integrity": "sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg=="
},
"jmespath": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz",
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -9586,9 +9656,9 @@
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
},
"object-path": {
"version": "0.11.7",
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.7.tgz",
"integrity": "sha512-T4evaK9VfGGQskXBDILcn6F90ZD+WO3OwRFFQ2rmZdUH4vQeDBpiolTpVlPY2yj5xSepyILTjDyM6UvbbdHMZw=="
"version": "0.11.8",
"resolved": "https://registry.npmjs.org/object-path/-/object-path-0.11.8.tgz",
"integrity": "sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA=="
},
"object-visit": {
"version": "1.0.1",
@@ -10483,6 +10553,11 @@
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
},
"quick-lru": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz",
@@ -12640,6 +12715,22 @@
"resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz",
"integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI="
},
"url": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz",
"integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=",
"requires": {
"punycode": "1.3.2",
"querystring": "0.2.0"
},
"dependencies": {
"punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
"integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
}
}
},
"url-parse-lax": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz",
@@ -13008,6 +13099,20 @@
"resolved": "https://registry.npmjs.org/xml-escape/-/xml-escape-1.1.0.tgz",
"integrity": "sha1-OQTBQ/qOs6ADDsZG0pAqLxtwbEQ="
},
"xml2js": {
"version": "0.4.19",
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz",
"integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==",
"requires": {
"sax": ">=0.6.0",
"xmlbuilder": "~9.0.1"
}
},
"xmlbuilder": {
"version": "9.0.7",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz",
"integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0="
},
"xss": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz",
+2
View File
@@ -23,6 +23,7 @@
"apollo-server-testing": "^2.19.0",
"app-root-path": "^3.0.0",
"auto-load": "^3.0.4",
"aws-sdk": "^2.989.0",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"busboy": "^0.3.1",
@@ -73,6 +74,7 @@
"apollo-link": "^1.2.14",
"apollo-link-http": "^1.5.17",
"apollo-link-ws": "^1.0.20",
"axios": "^0.21.4",
"chai": "^4.2.0",
"chai-http": "^4.3.0",
"concurrently": "^5.2.0",
+21
View File
@@ -0,0 +1,21 @@
const appRoot = require( 'app-root-path' )
const { createUser } = require( `${appRoot}/modules/core/services/users` )
const https = require( 'https' )
const axios = require( 'axios' )
const { fetch } = require( 'node-fetch' )
const main = async () => {
let userInputs = ( await fetch( 'https://randomuser.me/api/?results=250' ) ).json().results.map( user => {
return {
name: `${user.name.first} ${user.name.last}`,
email: user.email,
password: `${user.login.password}${user.login.password}`
}
} )
await Promise.all( userInputs.map( userInput => createUser( userInput ) ) )
}
main()
.then( console.log( 'created' ) )
.catch( console.log( 'failed' ) )
+3 -3
View File
@@ -10514,9 +10514,9 @@
"dev": true
},
"tmpl": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz",
"integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
"integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==",
"dev": true
},
"to-fast-properties": {
+1 -1
View File
@@ -74,7 +74,7 @@ export default class Coverter {
let val = await this.resolveReference( element )
if ( !val.units ) val.units = obj.units
let { bufferGeometry } = await this.convert( val, scale )
callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial } ) )
callback( new ObjectWrapper( bufferGeometry, { renderMaterial: val.renderMaterial, ...obj } ) )
}
}
}
+1 -1
View File
@@ -189,5 +189,5 @@ export default class Viewer extends EventEmitter {
dispose() {
// TODO
}l
}
}