From f075b80b0dc6d959d0899a19bcf97986fcc778de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20Jedlicska?= Date: Tue, 14 Sep 2021 10:31:30 +0200 Subject: [PATCH] feat((server) users view): add view users grapql query --- .../modules/core/graph/resolvers/users.js | 10 ++- .../modules/core/graph/schemas/user.graphql | 7 ++ .../server/modules/core/services/users.js | 25 ++++-- .../server/modules/core/tests/users.spec.js | 89 +++++++++++++++---- .../modules/serverinvites/services/index.js | 1 - 5 files changed, 108 insertions(+), 24 deletions(-) diff --git a/packages/server/modules/core/graph/resolvers/users.js b/packages/server/modules/core/graph/resolvers/users.js index 6b15a6fbf..32bc13ec6 100644 --- a/packages/server/modules/core/graph/resolvers/users.js +++ b/packages/server/modules/core/graph/resolvers/users.js @@ -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 } = 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 ) + let totalCount = await countUsers() + return { totalCount, items: users } + }, + async userSearch( parent, args, context, info ) { await validateServerRole( context, 'server:user' ) await validateScopes( context.scopes, 'profile:read' ) diff --git a/packages/server/modules/core/graph/schemas/user.graphql b/packages/server/modules/core/graph/schemas/user.graphql index b3d3985e7..b0b28e544 100644 --- a/packages/server/modules/core/graph/schemas/user.graphql +++ b/packages/server/modules/core/graph/schemas/user.graphql @@ -3,6 +3,7 @@ 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 + users(limit: Int!=25, offset: Int!=0) : UserCollection userSearch( query: String! limit: Int! = 25 @@ -27,6 +28,12 @@ type User { role: String } + +type UserCollection { + totalCount: Int! + items: [ User ] +} + type UserSearchResultCollection { cursor: String items: [UserSearchResult] diff --git a/packages/server/modules/core/services/users.js b/packages/server/modules/core/services/users.js index 8c4b4c454..84d4e06a0 100644 --- a/packages/server/modules/core/services/users.js +++ b/packages/server/modules/core/services/users.js @@ -34,12 +34,10 @@ module.exports = { if ( usr ) throw new Error( 'Email taken. Try logging in?' ) let res = await Users( ).returning( 'id' ).insert( user ) + + let userRole = parseInt( count ) === 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 +58,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 } ) } @@ -140,6 +137,7 @@ module.exports = { }, 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 +165,20 @@ module.exports = { } return await Users( ).where( { id: id } ).del( ) + }, + + async getUsers ( limit = 10, offset = 0 ) { + // sanitize limit + const maxLimit = 200 + if ( limit > maxLimit ) limit = maxLimit + + let users =await Users ( ).limit( limit ).offset( offset ) + users.map( user => delete user.passwordDigest ) + return users + }, + + async countUsers (){ + let [ userCount ] = await Users().count() + return parseInt( userCount.count ) } } diff --git a/packages/server/modules/core/tests/users.spec.js b/packages/server/modules/core/tests/users.spec.js index 568a75faa..504c7af55 100644 --- a/packages/server/modules/core/tests/users.spec.js +++ b/packages/server/modules/core/tests/users.spec.js @@ -11,7 +11,7 @@ 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, getUsers, searchUsers, countUsers, updateUser, deleteUser, validatePasssword, updateUserPassword, getUserRole } = require( '../services/users' ) const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../services/tokens' ) const { grantPermissionsStream, createStream, getStream } = require( '../services/streams' ) @@ -46,7 +46,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { let actorId = await createUser( myTestActor ) myTestActor.id = actorId - } ) after( async ( ) => { @@ -55,11 +54,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { describe( 'Users @core-users', ( ) => { - - it( 'First created user should be a server admin', async ( ) => { - - } ) - it( 'Should create an user', async ( ) => { let newUser = { ...myTestActor } newUser.name = 'Bill Gates' @@ -83,7 +77,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { } ) it( 'Should not create an user with the same email', async ( ) => { - let newUser = { } newUser.name = 'Bill Gates' newUser.email = 'bill@gates.com' @@ -100,7 +93,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { let ballmerUserId = null it( 'Find or create should create a user', async ( ) => { - let newUser = { } newUser.name = 'Steve Ballmer Balls' newUser.email = 'ballmer@balls.com' @@ -109,11 +101,9 @@ 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 = { } newUser.name = 'Steve Ballmer Balls' newUser.email = 'ballmer@balls.com' @@ -122,7 +112,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { let { id } = await findOrCreateUser( { user: newUser } ) expect( id ).to.equal( ballmerUserId ) - } ) // Note: deletion is more complicated. @@ -193,7 +182,6 @@ describe( 'Actors & Tokens @user-services', ( ) => { let actor = await getUser( myTestActor.id ) expect( actor.name ).to.equal( updatedActor.name ) - } ) it( 'Should not update password', async ( ) => { @@ -218,7 +206,6 @@ 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() => { @@ -285,6 +272,76 @@ describe( 'Actors & Tokens @user-services', ( ) => { 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 ) + } ) } ) diff --git a/packages/server/modules/serverinvites/services/index.js b/packages/server/modules/serverinvites/services/index.js index 8bb16c089..75b6cfb4b 100644 --- a/packages/server/modules/serverinvites/services/index.js +++ b/packages/server/modules/serverinvites/services/index.js @@ -21,7 +21,6 @@ module.exports = { if ( existingUser ) throw new Error( 'This email is already associated with an account on this server!' ) if ( message ) { - if ( message.length >= 1024 ) { throw new Error( 'Personal message too long.' ) }