Skip to content

Commit

Permalink
feat(gql): adds UserSearchResults and improves user search
Browse files Browse the repository at this point in the history
teocomi committed Jul 27, 2020
1 parent 8df402e commit c6e08b2
Showing 5 changed files with 109 additions and 81 deletions.
1 change: 1 addition & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -7,3 +7,4 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = both
74 changes: 39 additions & 35 deletions modules/core/graph/resolvers/user.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use strict'
const appRoot = require('app-root-path')
const { ApolloError, AuthenticationError, UserInputError } = require('apollo-server-express')
const { createUser, getUser, getUserByEmail, getUserRole, updateUser, deleteUser, findUsers, validatePasssword } = require('../../services/users')
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require('../../services/tokens')
const { validateServerRole, validateScopes, authorizeResolver } = require(`${appRoot}/modules/shared`)
const setupCheck = require(`${appRoot}/setupcheck`)
const zxcvbn = require('zxcvbn')
const appRoot = require( 'app-root-path' )
const { ApolloError, AuthenticationError, UserInputError } = require( 'apollo-server-express' )
const { createUser, getUser, getUserByEmail, getUserRole, updateUser, deleteUser, searchUsers, validatePasssword } = require( '../../services/users' )
const { createPersonalAccessToken, createAppToken, revokeToken, revokeTokenById, validateToken, getUserTokens } = require( '../../services/tokens' )
const { validateServerRole, validateScopes, authorizeResolver } = require( `${appRoot}/modules/shared` )
const setupCheck = require( `${appRoot}/setupcheck` )
const zxcvbn = require( 'zxcvbn' )

module.exports = {
Query: {
@@ -14,75 +14,79 @@ module.exports = {
return `Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn.`
},

async user(parent, args, context, info) {
async user( parent, args, context, info ) {

await validateServerRole(context, 'server:user')
await validateServerRole( context, 'server:user' )

if (!args.id)
await validateScopes(context.scopes, 'profile:read')
if ( !args.id )
await validateScopes( context.scopes, 'profile:read' )
else
await validateScopes(context.scopes, 'users:read')
await validateScopes( context.scopes, 'users:read' )

if (!args.id && !context.userId) {
throw new UserInputError('You must provide an user id.')
if ( !args.id && !context.userId ) {
throw new UserInputError( 'You must provide an user id.' )
}

return await getUser(args.id || context.userId)
return await getUser( args.id || context.userId )
},

async users(parent, args, context, info) {
async userSearchResults( parent, args, context, info ) {

await validateServerRole(context, 'server:user')
await validateScopes(context.scopes, 'profile:read')
await validateScopes(context.scopes, 'users:read')
await validateServerRole( context, 'server:user' )
await validateScopes( context.scopes, 'profile:read' )
await validateScopes( context.scopes, 'users:read' )

if (!args.query) {
throw new UserInputError('You must provide a search query.')
if ( !args.query ) {
throw new UserInputError( 'You must provide a search query.' )
}

return await findUsers(args.query)
if ( args.query.length < 3 ) {
throw new UserInputError( 'Search query must be at least 3 carachters.' )
}

return await searchUsers( args.query, args.limit )
},

async userPwdStrength(parent, args, context, info) {
let res = zxcvbn(args.pwd)
async userPwdStrength( parent, args, context, info ) {
let res = zxcvbn( args.pwd )
return { score: res.score, feedback: res.feedback }
}

},

User: {

async email(parent, args, context, info) {
async email( parent, args, context, info ) {
// NOTE: we're redacting the field (returning null) rather than throwing a full error which would invalidate the request.
if (context.userId === parent.id) {
if ( context.userId === parent.id ) {
try {
await validateScopes(context.scopes, 'profile:email')
await validateScopes( context.scopes, 'profile:email' )
return parent.email
} catch (err) {
} catch ( err ) {
return null
}
}

try {
await validateScopes(context.scopes, 'users:email')
await validateScopes( context.scopes, 'users:email' )
return parent.email
} catch (err) {
} catch ( err ) {
return null
}
},

async role(parent, args, context, info) {
return await getUserRole(parent.id)
async role( parent, args, context, info ) {
return await getUserRole( parent.id )
}

},



Mutation: {
async userEdit(parent, args, context, info) {
await validateServerRole(context, 'server:user')
await updateUser(context.userId, args.user)
async userEdit( parent, args, context, info ) {
await validateServerRole( context, 'server:user' )
await updateUser( context.userId, args.user )
return true
}
}
14 changes: 13 additions & 1 deletion modules/core/graph/schemas/user.graphql
Original file line number Diff line number Diff line change
@@ -3,7 +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(query: String): [User!]!
userSearchResults(query: String, limit: Int! = 100): [UserSearchResult!]!
userPwdStrength( pwd: String! ): JSONObject
}

@@ -23,6 +23,18 @@ type User {
role: String
}

type UserSearchResult {
id: String!
username: String
name: String
bio: String
company: String
avatar: String
verified: Boolean
profiles: JSONObject
role: String
}

extend type Mutation {
"""
Edits a user's profile.
89 changes: 46 additions & 43 deletions modules/core/services/users.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
'use strict'
const bcrypt = require('bcrypt')
const crs = require('crypto-random-string')
const appRoot = require('app-root-path')
const knex = require(`${appRoot}/db/knex`)
const bcrypt = require( 'bcrypt' )
const crs = require( 'crypto-random-string' )
const appRoot = require( 'app-root-path' )
const knex = require( `${appRoot}/db/knex` )

const Users = () => knex('users')
const ServerRoles = () => knex('server_acl')
const Users = () => knex( 'users' )
const ServerRoles = () => knex( 'server_acl' )

module.exports = {

@@ -15,90 +15,93 @@ module.exports = {
*/

async createUser(user) {
let [{ count }] = await ServerRoles().where({ role: 'server:admin' }).count()
async createUser( user ) {
let [ {count} ] = await ServerRoles().where( {role: 'server:admin'} ).count()

user.id = crs({ length: 10 })
user.id = crs( {length: 10} )

if (user.password) {
user.passwordDigest = await bcrypt.hash(user.password, 10)
if ( user.password ) {
user.passwordDigest = await bcrypt.hash( user.password, 10 )
}
delete user.password

let usr = await Users().select('id').where({ email: user.email }).first()
if (usr) throw new Error('Email taken. Try logging in?')
let usr = await Users().select( 'id' ).where( {email: user.email} ).first()
if ( usr ) throw new Error( 'Email taken. Try logging in?' )

let res = await Users().returning('id').insert(user)
let res = await Users().returning( 'id' ).insert( user )

if (parseInt(count) === 0) {
await ServerRoles().insert({ userId: res[0], role: 'server:admin' })
if ( parseInt( count ) === 0 ) {
await ServerRoles().insert( {userId: res[0], role: 'server:admin'} )
} else {
await ServerRoles().insert({ userId: res[0], role: 'server:user' })
await ServerRoles().insert( {userId: res[0], role: 'server:user'} )
}

return res[0]
},

async findOrCreateUser({ user, rawProfile }) {
let existingUser = await Users().select('id').where({ email: user.email }).first()
async findOrCreateUser( {user, rawProfile} ) {
let existingUser = await Users().select( 'id' ).where( {email: user.email} ).first()

if (existingUser)
if ( existingUser )
return existingUser

user.password = crs({ length: 20 })
user.password = crs( {length: 20} )
user.verified = true // because we trust the external identity provider, no?
return { id: await module.exports.createUser(user) }
return {id: await module.exports.createUser( user )}
},

async getUserById({ userId }) {
let user = await Users().where({ id: userId }).select('*').first()
async getUserById( {userId} ) {
let user = await Users().where( {id: userId} ).select( '*' ).first()
delete user.passwordDigest
return user
},

// TODO: deprecate
async getUser(id) {
let user = await Users().where({ id: id }).select('*').first()
async getUser( id ) {
let user = await Users().where( {id: id} ).select( '*' ).first()
delete user.passwordDigest
return user
},

async getUserByEmail({ email }) {
let user = await Users().where({ email: email }).select('*').first()
async getUserByEmail( {email} ) {
let user = await Users().where( {email: email} ).select( '*' ).first()
delete user.passwordDigest
return user
},

async getUserRole(id) {
let { role } = await ServerRoles().where({ userId: id }).select('role').first()
async getUserRole( id ) {
let {role} = await ServerRoles().where( {userId: id} ).select( 'role' ).first()
return role
},

async updateUser(id, user) {
async updateUser( id, user ) {
delete user.id
delete user.passwordDigest
delete user.password
delete user.email
await Users().where({ id: id }).update(user)
await Users().where( {id: id} ).update( user )
},

async findUsers(query) {

query = "%" + query + "%";
async searchUsers( query, limit ) {
if ( limit > 100 || limit === undefined )
limit = 100

let likeQuery = "%" + query + "%"
let users = await Users()
.where('email', 'like', query)
.orWhere('username', 'like', query)
.orWhere('name', 'like', query)
.where( {email: query} ) //match full email or partial username / name
.orWhere( 'username', 'like', likeQuery )
.orWhere( 'name', 'like', likeQuery )
.limit( limit )

return users
},

async validatePasssword({ email, password }) {
let { passwordDigest } = await Users().where({ email: email }).select('passwordDigest').first()
return bcrypt.compare(password, passwordDigest)
async validatePasssword( {email, password} ) {
let {passwordDigest} = await Users().where( {email: email} ).select( 'passwordDigest' ).first()
return bcrypt.compare( password, passwordDigest )
},

async deleteUser(id) {
throw new Error('not implemented')
async deleteUser( id ) {
throw new Error( 'not implemented' )
}
}
12 changes: 10 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
@@ -26,8 +26,16 @@ To debug, simply run `npm run dev:server`. To test, run `npm run test:server`. T
### How to commit to this repo
When pushing commits to this repo, please follow the following guidelines:

1) Install [commitizen](https://www.npmjs.com/package/commitizen#commitizen-for-contributors) globally
3) When ready to commit, type in the commandline `git cz` & follow the prompts.
1. Install [commitizen](https://www.npmjs.com/package/commitizen#commitizen-for-contributors) globally
2. When ready to commit, type in the commandline `git cz` & follow the prompts.
3. Install eslint globally `npm i -g eslint`
1. if using VS code install the `eslint` extension
2. we also recommend setting it to run on save by adding the following VS Code setting
```
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
```
## Modules

0 comments on commit c6e08b2

Please sign in to comment.