diff --git a/.editorconfig b/.editorconfig index ab561f3c56..435e72be69 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,4 @@ charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true +spaces_around_brackets = both diff --git a/modules/core/graph/resolvers/user.js b/modules/core/graph/resolvers/user.js index 00903c9d9b..65fc46bbb0 100644 --- a/modules/core/graph/resolvers/user.js +++ b/modules/core/graph/resolvers/user.js @@ -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 } } diff --git a/modules/core/graph/schemas/user.graphql b/modules/core/graph/schemas/user.graphql index 2767a7b3f3..a4ae70bd5d 100644 --- a/modules/core/graph/schemas/user.graphql +++ b/modules/core/graph/schemas/user.graphql @@ -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. diff --git a/modules/core/services/users.js b/modules/core/services/users.js index e422a9bbf8..0fb2b85a7e 100644 --- a/modules/core/services/users.js +++ b/modules/core/services/users.js @@ -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' ) } } \ No newline at end of file diff --git a/readme.md b/readme.md index ea7587e989..f9c4f6bf50 100644 --- a/readme.md +++ b/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