feat(gql): adds UserSearchResults and improves user search

This commit is contained in:
Matteo Cominetti 2020-07-27 23:13:20 +01:00
Родитель 8df402e4cb
Коммит c6e08b2c5d
5 изменённых файлов: 109 добавлений и 81 удалений

Просмотреть файл

@ -7,3 +7,4 @@ charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
spaces_around_brackets = both

Просмотреть файл

@ -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,37 +14,41 @@ 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 }
}
@ -52,27 +56,27 @@ module.exports = {
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 )
}
},
@ -80,9 +84,9 @@ module.exports = {
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
}
}

Просмотреть файл

@ -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.

Просмотреть файл

@ -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' )
}
}

Просмотреть файл

@ -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