feat(gql): adds UserSearchResults and improves user search
This commit is contained in:
Родитель
8df402e4cb
Коммит
c6e08b2c5d
|
@ -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' )
|
||||
}
|
||||
}
|
12
readme.md
12
readme.md
|
@ -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
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче