diff --git a/src/middleware/permissionsMiddleware.js b/src/middleware/permissionsMiddleware.js index 08feb1e5e..62999c235 100644 --- a/src/middleware/permissionsMiddleware.js +++ b/src/middleware/permissionsMiddleware.js @@ -56,11 +56,12 @@ const permissions = shield({ CreateBadge: isAdmin, UpdateBadge: isAdmin, DeleteBadge: isAdmin, + // addFruitToBasket: isAuthenticated follow: isAuthenticated, unfollow: isAuthenticated, shout: isAuthenticated, unshout: isAuthenticated, - + changePassword: isAuthenticated, enable: isModerator, disable: isModerator // CreateUser: allow, diff --git a/src/resolvers/user_management.js b/src/resolvers/user_management.js index ec4ae7ce2..36865646f 100644 --- a/src/resolvers/user_management.js +++ b/src/resolvers/user_management.js @@ -30,22 +30,71 @@ export default { // throw new Error('Already logged in.') // } const session = driver.session() - return session.run( + const result = await session.run( 'MATCH (user:User {email: $userEmail}) ' + - 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role} as user LIMIT 1', { + 'RETURN user {.id, .slug, .name, .avatar, .email, .password, .role} as user LIMIT 1', + { userEmail: email - }) - .then(async (result) => { - session.close() - const [currentUser] = await result.records.map(function (record) { - return record.get('user') - }) + } + ) - if (currentUser && await bcrypt.compareSync(password, currentUser.password)) { - delete currentUser.password - return encode(currentUser) - } else throw new AuthenticationError('Incorrect email address or password.') - }) + session.close() + const [currentUser] = await result.records.map(function (record) { + return record.get('user') + }) + + if ( + currentUser && + (await bcrypt.compareSync(password, currentUser.password)) + ) { + delete currentUser.password + return encode(currentUser) + } else { + throw new AuthenticationError('Incorrect email address or password.') + } + }, + changePassword: async ( + _, + { oldPassword, newPassword }, + { driver, user } + ) => { + const session = driver.session() + let result = await session.run( + `MATCH (user:User {email: $userEmail}) + RETURN user {.id, .email, .password}`, + { + userEmail: user.email + } + ) + + const [currentUser] = result.records.map(function (record) { + return record.get('user') + }) + + if (!(await bcrypt.compareSync(oldPassword, currentUser.password))) { + throw new AuthenticationError('Old password is not correct') + } + + if (await bcrypt.compareSync(newPassword, currentUser.password)) { + throw new AuthenticationError( + 'Old password and new password should be different' + ) + } else { + const newHashedPassword = await bcrypt.hashSync(newPassword, 10) + session.run( + `MATCH (user:User {email: $userEmail}) + SET user.password = $newHashedPassword + RETURN user + `, + { + userEmail: user.email, + newHashedPassword + } + ) + session.close() + + return encode(currentUser) + } } } } diff --git a/src/resolvers/user_management.spec.js b/src/resolvers/user_management.spec.js index a3bf6fdd0..c4b09df37 100644 --- a/src/resolvers/user_management.spec.js +++ b/src/resolvers/user_management.spec.js @@ -21,11 +21,14 @@ const factory = Factory() // iss: 'http://localhost:4000', // sub: 'u3' // } -const jennyRostocksHeaders = { authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc' } +const jennyRostocksHeaders = { + authorization: + 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsImxvY2F0aW9uTmFtZSI6bnVsbCwibmFtZSI6Ikplbm55IFJvc3RvY2siLCJhYm91dCI6bnVsbCwiYXZhdGFyIjoiaHR0cHM6Ly9zMy5hbWF6b25hd3MuY29tL3VpZmFjZXMvZmFjZXMvdHdpdHRlci9zYXNoYV9zaGVzdGFrb3YvMTI4LmpwZyIsImlkIjoidTMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5vcmciLCJzbHVnIjoiamVubnktcm9zdG9jayIsImlhdCI6MTU1MDg0NjY4MCwiZXhwIjoxNjM3MjQ2NjgwLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjQwMDAiLCJzdWIiOiJ1MyJ9.eZ_mVKas4Wzoc_JrQTEWXyRn7eY64cdIg4vqQ-F_7Jc' +} beforeEach(async () => { await factory.create('User', { - avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg', + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', id: 'acb2d923-f3af-479e-9f00-61b12e864666', name: 'Matilde Hermiston', slug: 'matilde-hermiston', @@ -43,7 +46,9 @@ describe('isLoggedIn', () => { const query = '{ isLoggedIn }' describe('unauthenticated', () => { it('returns false', async () => { - await expect(request(host, query)).resolves.toEqual({ isLoggedIn: false }) + await expect(request(host, query)).resolves.toEqual({ + isLoggedIn: false + }) }) }) @@ -52,7 +57,9 @@ describe('isLoggedIn', () => { const client = new GraphQLClient(host, { headers }) it('returns false', async () => { - await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false }) + await expect(client.request(query)).resolves.toEqual({ + isLoggedIn: false + }) }) }) @@ -60,14 +67,18 @@ describe('isLoggedIn', () => { const client = new GraphQLClient(host, { headers: jennyRostocksHeaders }) it('returns false', async () => { - await expect(client.request(query)).resolves.toEqual({ isLoggedIn: false }) + await expect(client.request(query)).resolves.toEqual({ + isLoggedIn: false + }) }) describe('and a corresponding user in the database', () => { it('returns true', async () => { // see the decoded token above await factory.create('User', { id: 'u3' }) - await expect(client.request(query)).resolves.toEqual({ isLoggedIn: true }) + await expect(client.request(query)).resolves.toEqual({ + isLoggedIn: true + }) }) }) }) @@ -116,7 +127,7 @@ describe('currentUser', () => { it('returns the whole user object', async () => { const expected = { currentUser: { - avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/seyedhossein1/128.jpg', + avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/jimmuirhead/128.jpg', email: 'test@example.org', id: 'acb2d923-f3af-479e-9f00-61b12e864666', name: 'Matilde Hermiston', @@ -131,7 +142,7 @@ describe('currentUser', () => { }) describe('login', () => { - const mutation = (params) => { + const mutation = params => { const { email, password } = params return ` mutation { @@ -142,10 +153,13 @@ describe('login', () => { describe('ask for a `token`', () => { describe('with valid email/password combination', () => { it('responds with a JWT token', async () => { - const data = await request(host, mutation({ - email: 'test@example.org', - password: '1234' - })) + const data = await request( + host, + mutation({ + email: 'test@example.org', + password: '1234' + }) + ) const token = data.login jwt.verify(token, process.env.JWT_SECRET, (err, data) => { expect(data.email).toEqual('test@example.org') @@ -157,10 +171,13 @@ describe('login', () => { describe('with a valid email but incorrect password', () => { it('responds with "Incorrect email address or password."', async () => { await expect( - request(host, mutation({ - email: 'test@example.org', - password: 'wrong' - })) + request( + host, + mutation({ + email: 'test@example.org', + password: 'wrong' + }) + ) ).rejects.toThrow('Incorrect email address or password.') }) }) @@ -168,12 +185,89 @@ describe('login', () => { describe('with a non-existing email', () => { it('responds with "Incorrect email address or password."', async () => { await expect( - request(host, mutation({ - email: 'non-existent@example.org', - password: 'wrong' - })) + request( + host, + mutation({ + email: 'non-existent@example.org', + password: 'wrong' + }) + ) ).rejects.toThrow('Incorrect email address or password.') }) }) }) }) + +describe('change password', () => { + let headers + let client + + beforeEach(async () => { + headers = await login({ email: 'test@example.org', password: '1234' }) + client = new GraphQLClient(host, { headers }) + }) + + const mutation = params => { + const { oldPassword, newPassword } = params + return ` + mutation { + changePassword(oldPassword:"${oldPassword}", newPassword:"${newPassword}") + }` + } + + describe('should be authenticated before changing password', () => { + it('throws not "Not Authorised!', async () => { + await expect( + request( + host, + mutation({ + oldPassword: '1234', + newPassword: '1234' + }) + ) + ).rejects.toThrow('Not Authorised!') + }) + }) + + describe('old and new password should not match', () => { + it('responds with "Old password and new password should be different"', async () => { + await expect( + client.request( + mutation({ + oldPassword: '1234', + newPassword: '1234' + }) + ) + ).rejects.toThrow('Old password and new password should be different') + }) + }) + + describe('incorrect old password', () => { + it('responds with "Old password isn\'t valid"', async () => { + await expect( + client.request( + mutation({ + oldPassword: 'notOldPassword', + newPassword: '12345' + }) + ) + ).rejects.toThrow('Old password is not correct') + }) + }) + + describe('correct password', () => { + it('changes the password if given correct credentials "', async () => { + let response = await client.request( + mutation({ + oldPassword: '1234', + newPassword: '12345' + }) + ) + await expect( + response + ).toEqual(expect.objectContaining({ + changePassword: expect.any(String) + })) + }) + }) +}) diff --git a/src/schema.graphql b/src/schema.graphql index 952b7291b..152301715 100644 --- a/src/schema.graphql +++ b/src/schema.graphql @@ -21,6 +21,7 @@ type Mutation { "Get a JWT Token for the given Email and password" login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! + changePassword(oldPassword:String!, newPassword: String!): String! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID