From d024d7df91dbac80237fa74e62bba1a8469ed463 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jun 2019 14:03:21 +0200 Subject: [PATCH 01/62] fixed importing of urls - remove url prefix --- .../maintenance-worker/migration/neo4j/badges.cql | 2 +- .../maintenance-worker/migration/neo4j/contributions.cql | 6 +++--- .../maintenance-worker/migration/neo4j/users.cql | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql index 62cd4a2cc..2d1548d4f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql @@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]}) ON CREATE SET b.key = badge.key, b.type = badge.type, -b.icon = badge.image.path, +b.icon = substring(badge.image.path, 38), b.status = badge.status, b.createdAt = badge.createdAt.`$date`, b.updatedAt = badge.updatedAt.`$date` diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql index 98d8f24e9..70f09e035 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql @@ -28,7 +28,7 @@ [?] unique: true, // Unique value is not enforced in Nitro? [-] index: true }, -[ ] type: { +[ ] type: { // db.getCollection('contributions').distinct('type') -> 'DELETED', 'cando', 'post' [ ] type: String, [ ] required: true, [-] index: true @@ -50,7 +50,7 @@ [?] required: true // Not required in Nitro }, [ ] hasMore: { type: Boolean }, -[?] teaserImg: { type: String }, // Path is incorrect in Nitro +[X] teaserImg: { type: String }, [ ] language: { [ ] type: String, [ ] required: true, @@ -131,7 +131,7 @@ MERGE (p:Post {id: post._id["$oid"]}) ON CREATE SET p.title = post.title, p.slug = post.slug, -p.image = post.teaserImg, +p.image = substring(post.teaserImg, 38), p.content = post.content, p.contentExcerpt = post.contentExcerpt, p.visibility = toLower(post.visibility), diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql index aec5499fc..96251a9ce 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql @@ -49,8 +49,8 @@ } }, [ ] timezone: { type: String }, -[?] avatar: { type: String }, // Path is incorrect in Nitro -[?] coverImg: { type: String }, // Path is incorrect in Nitro, was not modeled in latest Nitro - do we want this? +[X] avatar: { type: String }, +[X] coverImg: { type: String }, [ ] doiToken: { type: String }, [ ] confirmedAt: { type: Date }, [?] badgeIds: [], // Verify this is working properly @@ -102,8 +102,8 @@ u.name = user.name, u.slug = user.slug, u.email = user.email, u.password = user.password, -u.avatar = user.avatar, -u.coverImg = user.coverImg, +u.avatar = substring(user.avatar, 38), +u.coverImg = substring(user.coverImg, 38), u.wasInvited = user.wasInvited, u.wasSeeded = user.wasSeeded, u.role = toLower(user.role), From 89d630b1eb82914996e140e6c481b292611f800a Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jun 2019 14:03:37 +0200 Subject: [PATCH 02/62] removed fixImageUrlsMiddleware --- .../src/middleware/fixImageUrlsMiddleware.js | 48 ------------------- .../middleware/fixImageUrlsMiddleware.spec.js | 36 -------------- 2 files changed, 84 deletions(-) delete mode 100644 backend/src/middleware/fixImageUrlsMiddleware.js delete mode 100644 backend/src/middleware/fixImageUrlsMiddleware.spec.js diff --git a/backend/src/middleware/fixImageUrlsMiddleware.js b/backend/src/middleware/fixImageUrlsMiddleware.js deleted file mode 100644 index c930915bf..000000000 --- a/backend/src/middleware/fixImageUrlsMiddleware.js +++ /dev/null @@ -1,48 +0,0 @@ -const legacyUrls = [ - 'https://api-alpha.human-connection.org', - 'https://staging-api.human-connection.org', - 'http://localhost:3000', -] - -export const fixUrl = url => { - legacyUrls.forEach(legacyUrl => { - url = url.replace(legacyUrl, '/api') - }) - return url -} - -const checkUrl = thing => { - return ( - thing && - typeof thing === 'string' && - legacyUrls.find(legacyUrl => { - return thing.indexOf(legacyUrl) === 0 - }) - ) -} - -export const fixImageURLs = (result, recursive) => { - if (checkUrl(result)) { - result = fixUrl(result) - } else if (result && Array.isArray(result)) { - result.forEach((res, index) => { - result[index] = fixImageURLs(result[index], true) - }) - } else if (result && typeof result === 'object') { - Object.keys(result).forEach(key => { - result[key] = fixImageURLs(result[key], true) - }) - } - return result -} - -export default { - Mutation: async (resolve, root, args, context, info) => { - const result = await resolve(root, args, context, info) - return fixImageURLs(result) - }, - Query: async (resolve, root, args, context, info) => { - let result = await resolve(root, args, context, info) - return fixImageURLs(result) - }, -} diff --git a/backend/src/middleware/fixImageUrlsMiddleware.spec.js b/backend/src/middleware/fixImageUrlsMiddleware.spec.js deleted file mode 100644 index b2d808dd9..000000000 --- a/backend/src/middleware/fixImageUrlsMiddleware.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { fixImageURLs } from './fixImageUrlsMiddleware' - -describe('fixImageURLs', () => { - describe('image url of legacy alpha', () => { - it('removes domain', () => { - const url = - 'https://api-alpha.human-connection.org/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png' - expect(fixImageURLs(url)).toEqual( - '/api/uploads/4bfaf9172c4ba03d7645108bbbd16f0a696a37d01eacd025fb131e5da61b15d9.png', - ) - }) - }) - - describe('image url of legacy staging', () => { - it('removes domain', () => { - const url = - 'https://staging-api.human-connection.org/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg' - expect(fixImageURLs(url)).toEqual( - '/api/uploads/1b3c39a24f27e2fb62b69074b2f71363b63b263f0c4574047d279967124c026e.jpeg', - ) - }) - }) - - describe('object', () => { - it('returns untouched', () => { - const object = { some: 'thing' } - expect(fixImageURLs(object)).toEqual(object) - }) - }) - - describe('some string', () => { - it('returns untouched', () => {}) - const string = "Yeah I'm a String" - expect(fixImageURLs(string)).toEqual(string) - }) -}) From 794bb08f141eeed293c9a59821dc506d87eba841 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Fri, 14 Jun 2019 14:04:15 +0200 Subject: [PATCH 03/62] removed reference for fixImageUrls middleware --- backend/src/middleware/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index 75314abc0..aae2dcef3 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -3,7 +3,6 @@ import activityPub from './activityPubMiddleware' import password from './passwordMiddleware' import softDelete from './softDeleteMiddleware' import sluggify from './sluggifyMiddleware' -import fixImageUrls from './fixImageUrlsMiddleware' import excerpt from './excerptMiddleware' import dateTime from './dateTimeMiddleware' import xss from './xssMiddleware' @@ -25,7 +24,6 @@ export default schema => { excerpt: excerpt, notifications: notifications, xss: xss, - fixImageUrls: fixImageUrls, softDelete: softDelete, user: user, includedFields: includedFields, From d558a4de370a36acbefba5e8c565a958f1780768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 11:09:09 +0200 Subject: [PATCH 04/62] Add mailserver for development --- docker-compose.override.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.override.yml b/docker-compose.override.yml index a71418229..d7ee86bd6 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,6 +1,10 @@ version: "3.4" services: + mailserver: + image: djfarrelly/maildev + ports: + - 1080:80 webapp: build: context: webapp From 64c5245f5a4f70bf9b63fe21f26a534d39e8db30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 11:25:01 +0200 Subject: [PATCH 05/62] Update date-fns manually to get passed this bug: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``` ERROR Failed to compile with 16 errors This dependency was not found: * date-fns/addSeconds in ./plugins/vue-filters.js To install it, you can run: npm install --save date-fns/addSeconds These relative modules were not found: * ../../../../_lib/isSameUTCWeek/index.js in ./node_modules/date-fns/esm/locale/be/_lib/formatRelative/index.js, ./node_modules/date-fns/esm/locale/lv/_lib/formatRelative/index.js and 4 others * ../_lib/format/formatters/index.js in ./node_modules/date-fns/format/index.js * ../_lib/format/longFormatters/index.js in ./node_modules/date-fns/format/index.js * ../_lib/getTimezoneOffsetInMilliseconds/index.js in ./node_modules/date-fns/format/index.js, ./node_modules/date-fns/formatRelative/index.js * ../_lib/protectedTokens/index.js in ./node_modules/date-fns/format/index.js * ../_lib/toInteger/index.js in ./node_modules/date-fns/format/index.js, ./node_modules/date-fns/subMilliseconds/index.js * ../addMilliseconds/index.js in ./node_modules/date-fns/subMilliseconds/index.js * ../differenceInCalendarDays/index.js in ./node_modules/date-fns/formatRelative/index.js ℹ Waiting for file changes ``` --- webapp/package.json | 2 +- webapp/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/webapp/package.json b/webapp/package.json index 1abaf479b..5e42d0332 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -59,7 +59,7 @@ "apollo-client": "~2.6.2", "cookie-universal-nuxt": "~2.0.16", "cross-env": "~5.2.0", - "date-fns": "2.0.0-alpha.33", + "date-fns": "2.0.0-alpha.34", "express": "~4.17.1", "graphql": "~14.3.1", "jsonwebtoken": "~8.5.1", diff --git a/webapp/yarn.lock b/webapp/yarn.lock index e5c1daf8a..de0d07924 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -3754,10 +3754,10 @@ data-urls@^1.0.0: whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-fns@2.0.0-alpha.33: - version "2.0.0-alpha.33" - resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.33.tgz#c2f73c3cc50ac301c9217eb93603c9bc40e891bf" - integrity sha512-tqUVEk3oxnJuNIvwAMKHAMo4uFRG0zXvjxZQll+BonoPt+m4NMcUgO14NDxbHuy7uYcrVErd2GdSsw02EDZQ7w== +date-fns@2.0.0-alpha.34: + version "2.0.0-alpha.34" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.0.0-alpha.34.tgz#5d3ae7ca0d08915ccfc87a20545250af4e9c3cae" + integrity sha512-yjSYUHASHvzOZl++cEms+Tw7oQOFA+7Z6/lL7L3lRO9j6pMfT48N6oEyvCGo/MVlH08XWmydgf8X9Y1eedf9sQ== date-now@^0.1.4: version "0.1.4" From f9d25828d520fd838e4cefa367de948908641f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 11:47:24 +0200 Subject: [PATCH 06/62] Add reset password page --- webapp/locales/de.json | 4 ++++ webapp/locales/en.json | 4 ++++ webapp/nuxt.config.js | 11 ++++++++++- webapp/pages/login.vue | 5 +++++ webapp/pages/password-reset.vue | 19 +++++++++++++++++++ 5 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 webapp/pages/password-reset.vue diff --git a/webapp/locales/de.json b/webapp/locales/de.json index efe05a472..d783f9e37 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -8,11 +8,15 @@ "logout": "Ausloggen", "email": "Deine E-Mail", "password": "Dein Passwort", + "forgotPassword": "Passwort vergessen?", "moreInfo": "Was ist Human Connection?", "moreInfoURL": "https://human-connection.org", "moreInfoHint": "zur Präsentationsseite", "hello": "Hallo" }, + "password-reset": { + "title": "Passwort zurücksetzen" + }, "editor": { "placeholder": "Schreib etwas Inspirierendes..." }, diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 4fdcadedb..3a5405eec 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -8,11 +8,15 @@ "logout": "Logout", "email": "Your Email", "password": "Your Password", + "forgotPassword": "Forgot Password?", "moreInfo": "What is Human Connection?", "moreInfoURL": "https://human-connection.org/en/", "moreInfoHint": "to the presentation page", "hello": "Hello" }, + "password-reset": { + "title": "Reset your password" + }, "editor": { "placeholder": "Leave your inspirational thoughts..." }, diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 49f2f5d0a..8af3dbb16 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -25,7 +25,16 @@ module.exports = { env: { // pages which do NOT require a login - publicPages: ['login', 'logout', 'register', 'signup', 'reset', 'reset-token', 'pages-slug'], + publicPages: [ + 'login', + 'logout', + 'password-reset', + 'register', + 'signup', + 'reset', + 'reset-token', + 'pages-slug', + ], // pages to keep alive keepAlivePages: ['index'], // active locales diff --git a/webapp/pages/login.vue b/webapp/pages/login.vue index a96fbdbf1..94c974b29 100644 --- a/webapp/pages/login.vue +++ b/webapp/pages/login.vue @@ -45,6 +45,11 @@ name="password" type="password" /> + + + {{ $t('login.forgotPassword') }} + + + + + + + + {{ $t('password-reset.title') }} + + + + + + + + From e44ed7d281d5077786a56590dae11306681a9810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 15:42:17 +0200 Subject: [PATCH 07/62] Start writing a resolver for requestPasswordReset --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/passwordReset.js | 10 ++++ .../schema/resolvers/passwordReset.spec.js | 46 +++++++++++++++++++ .../src/schema/resolvers/user_management.js | 2 +- backend/src/schema/types/schema.gql | 2 + 5 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 backend/src/schema/resolvers/passwordReset.js create mode 100644 backend/src/schema/resolvers/passwordReset.spec.js diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 10b777748..ad2787579 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -147,6 +147,7 @@ const permissions = shield( CreateComment: isAuthenticated, DeleteComment: isAuthor, DeleteUser: isDeletingOwnAccount, + requestPasswordReset: allow, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js new file mode 100644 index 000000000..83a1080d0 --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset.js @@ -0,0 +1,10 @@ +export default { + Mutation: { + requestPasswordReset: async (_, { email }, { driver }) => { + throw Error('Not Implemented') + }, + resetPassword: async (_, { email, token, newPassword }, { driver }) => { + throw Error('Not Implemented') + } + } +} diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js new file mode 100644 index 000000000..d07ca4b09 --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -0,0 +1,46 @@ +import { GraphQLClient } from 'graphql-request' +import Factory from '../../seed/factories' +import { host, login } from '../../jest/helpers' +import { getDriver } from '../../bootstrap/neo4j' + +const factory = Factory() +let client +const driver = getDriver() + +const getAllPasswordResets = async () => { + const session = driver.session() + let transactionRes = await session.run('MATCH (r:PasswordReset) RETURN r') + const resets = transactionRes.records.map(record => record.get('r')) + session.close() + return resets +} + +describe('passwordReset', () => { + beforeEach(async () => { + client = new GraphQLClient(host) + await factory.create('User', { + email: 'user@example.org', + role: 'user', + password: '1234', + }) + }) + + afterEach(async () => { + await factory.cleanDatabase() + }) + + describe('requestPasswordReset', () => { + const variables = { email: 'user@example.org' } + const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }` + + it('resolves', async () => { + await expect(client.request(mutation, variables)).resolves.toEqual(true) + }) + + it('creates node with label `PasswordReset`', async () => { + await client.request(mutation, variables) + const resets = await getAllPasswordResets() + expect(resets).toHaveLength(1) + }) + }) +}) diff --git a/backend/src/schema/resolvers/user_management.js b/backend/src/schema/resolvers/user_management.js index eb07a07b3..e33314f7e 100644 --- a/backend/src/schema/resolvers/user_management.js +++ b/backend/src/schema/resolvers/user_management.js @@ -59,7 +59,7 @@ export default { changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => { const session = driver.session() let result = await session.run( - `MATCH (user:User {email: $userEmail}) + `MATCH (user:User {email: $userEmail}) RETURN user {.id, .email, .password}`, { userEmail: user.email, diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 2a8be9e09..358797631 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -25,6 +25,8 @@ type Mutation { login(email: String!, password: String!): String! signup(email: String!, password: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! + requestPasswordReset(email: String!): Boolean! + resetPassword(email: String!, resetToken: String!, newPassword: String!): String! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID From c7ee0c8121c09a0ee2f25786a325266b968c6e7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 16:22:28 +0200 Subject: [PATCH 08/62] Implement tests for requestPasswordReset --- backend/src/schema/resolvers/passwordReset.js | 13 ++++++++++++- .../schema/resolvers/passwordReset.spec.js | 19 ++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 83a1080d0..f3e1d32d2 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -1,7 +1,18 @@ export default { Mutation: { requestPasswordReset: async (_, { email }, { driver }) => { - throw Error('Not Implemented') + const session = driver.session() + let validUntil = new Date() + validUntil += 3*60*1000 + const cypher = ` + MATCH(u:User) WHERE u.email = $email + CREATE(pr:PasswordReset {id: apoc.create.uuid(), validUntil: $validUntil, redeemedAt: NULL}) + MERGE (u)-[:REQUESTED]->(pr) + RETURN u,pr + ` + await session.run(cypher, { email, validUntil }) + session.close() + return true }, resetPassword: async (_, { email, token, newPassword }, { driver }) => { throw Error('Not Implemented') diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index d07ca4b09..3b0d39864 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -34,7 +34,7 @@ describe('passwordReset', () => { const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }` it('resolves', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual(true) + await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) }) it('creates node with label `PasswordReset`', async () => { @@ -42,5 +42,22 @@ describe('passwordReset', () => { const resets = await getAllPasswordResets() expect(resets).toHaveLength(1) }) + + it('creates an id used as a reset token', async () => { + await client.request(mutation, variables) + const [reset] = await getAllPasswordResets() + const { id: token } = reset.properties + expect(token).toMatch(/^........-....-....-....-............$/) + }) + + it('created PasswordReset is valid for less than 4 minutes', async () => { + await client.request(mutation, variables) + const [reset] = await getAllPasswordResets() + let { validUntil } = reset.properties + validUntil = Date.parse(validUntil) + const now = (new Date()).getTime() + expect(validUntil).toBeGreaterThan(now - 60*1000) + expect(validUntil).toBeLessThan(now + 4*60*1000) + }) }) }) From 145a8d8bf65efa55fd53e5a70d711ed5241aa11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sat, 15 Jun 2019 23:01:22 +0200 Subject: [PATCH 09/62] Check invalid email Sending a mail with further instructions even if the email is invalid seems to be a good practice: A potential attacker will not now if a user has an account under that email address. If a user does not remember the email address, but has control over the other mail account, she will get feedback that this mail account is incorrect. --- .../schema/resolvers/passwordReset.spec.js | 61 ++++++++++++------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 3b0d39864..4bd29c9c6 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -30,34 +30,51 @@ describe('passwordReset', () => { }) describe('requestPasswordReset', () => { - const variables = { email: 'user@example.org' } const mutation = `mutation($email: String!) { requestPasswordReset(email: $email) }` - it('resolves', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) + describe('with invalid email', () => { + const variables = { email: 'non-existent@example.org' } + + it('resolves anyways', async () => { + await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) + }) + + it('creates no node', async () => { + await client.request(mutation, variables) + const resets = await getAllPasswordResets() + expect(resets).toHaveLength(0) + }) }) - it('creates node with label `PasswordReset`', async () => { - await client.request(mutation, variables) - const resets = await getAllPasswordResets() - expect(resets).toHaveLength(1) - }) + describe('with a valid email', () => { + const variables = { email: 'user@example.org' } - it('creates an id used as a reset token', async () => { - await client.request(mutation, variables) - const [reset] = await getAllPasswordResets() - const { id: token } = reset.properties - expect(token).toMatch(/^........-....-....-....-............$/) - }) + it('resolves', async () => { + await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) + }) - it('created PasswordReset is valid for less than 4 minutes', async () => { - await client.request(mutation, variables) - const [reset] = await getAllPasswordResets() - let { validUntil } = reset.properties - validUntil = Date.parse(validUntil) - const now = (new Date()).getTime() - expect(validUntil).toBeGreaterThan(now - 60*1000) - expect(validUntil).toBeLessThan(now + 4*60*1000) + it('creates node with label `PasswordReset`', async () => { + await client.request(mutation, variables) + const resets = await getAllPasswordResets() + expect(resets).toHaveLength(1) + }) + + it('creates an id used as a reset token', async () => { + await client.request(mutation, variables) + const [reset] = await getAllPasswordResets() + const { id: token } = reset.properties + expect(token).toMatch(/^........-....-....-....-............$/) + }) + + it('created PasswordReset is valid for less than 4 minutes', async () => { + await client.request(mutation, variables) + const [reset] = await getAllPasswordResets() + let { validUntil } = reset.properties + validUntil = Date.parse(validUntil) + const now = (new Date()).getTime() + expect(validUntil).toBeGreaterThan(now - 60*1000) + expect(validUntil).toBeLessThan(now + 4*60*1000) + }) }) }) }) From c9ea956f858c631e0735cc93ab28f571ed55bedb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Sun, 16 Jun 2019 00:08:36 +0200 Subject: [PATCH 10/62] Test+Implement resetPassword --- .../src/middleware/permissionsMiddleware.js | 1 + backend/src/schema/resolvers/passwordReset.js | 53 +++++-- .../schema/resolvers/passwordReset.spec.js | 143 ++++++++++++++++-- backend/src/schema/types/schema.gql | 2 +- 4 files changed, 174 insertions(+), 25 deletions(-) diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index ad2787579..dbcde849c 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -148,6 +148,7 @@ const permissions = shield( DeleteComment: isAuthor, DeleteUser: isDeletingOwnAccount, requestPasswordReset: allow, + resetPassword: allow, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index f3e1d32d2..d6d2a6e6c 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -1,21 +1,46 @@ -export default { - Mutation: { - requestPasswordReset: async (_, { email }, { driver }) => { - const session = driver.session() - let validUntil = new Date() - validUntil += 3*60*1000 - const cypher = ` - MATCH(u:User) WHERE u.email = $email - CREATE(pr:PasswordReset {id: apoc.create.uuid(), validUntil: $validUntil, redeemedAt: NULL}) +import uuid from 'uuid/v4' +import bcrypt from 'bcryptjs' + +export async function createPasswordReset({ driver, token, email, validUntil }) { + const session = driver.session() + const cypher = ` + MATCH (u:User) WHERE u.email = $email + CREATE(pr:PasswordReset {token: $token, validUntil: $validUntil, redeemedAt: NULL}) MERGE (u)-[:REQUESTED]->(pr) RETURN u,pr ` - await session.run(cypher, { email, validUntil }) - session.close() + const transactionRes = await session.run(cypher, { token, email, validUntil }) + const resets = transactionRes.records.map(record => record.get('pr')) + session.close() + return resets +} + +export default { + Mutation: { + requestPasswordReset: async (_, { email }, { driver }) => { + let validUntil = new Date() + validUntil += 3 * 60 * 1000 + const token = uuid() + await createPasswordReset({ driver, token, email, validUntil }) return true }, resetPassword: async (_, { email, token, newPassword }, { driver }) => { - throw Error('Not Implemented') - } - } + const session = driver.session() + const now = new Date().getTime() + const newHashedPassword = await bcrypt.hashSync(newPassword, 10) + const cypher = ` + MATCH (r:PasswordReset {token: $token}) + MATCH (u:User {email: $email})-[:REQUESTED]->(r) + WHERE r.validUntil > $now AND r.redeemedAt IS NULL + SET r.redeemedAt = $now + SET u.password = $newHashedPassword + RETURN r + ` + let transactionRes = await session.run(cypher, { now, email, token, newHashedPassword }) + const [reset] = transactionRes.records.map(record => record.get('r')) + const result = !!(reset && reset.properties.redeemedAt) + session.close() + return result + }, + }, } diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 4bd29c9c6..89b724fca 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,7 +1,8 @@ import { GraphQLClient } from 'graphql-request' import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' +import { host } from '../../jest/helpers' import { getDriver } from '../../bootstrap/neo4j' +import { createPasswordReset } from './passwordReset' const factory = Factory() let client @@ -36,7 +37,9 @@ describe('passwordReset', () => { const variables = { email: 'non-existent@example.org' } it('resolves anyways', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) + await expect(client.request(mutation, variables)).resolves.toEqual({ + requestPasswordReset: true, + }) }) it('creates no node', async () => { @@ -50,7 +53,9 @@ describe('passwordReset', () => { const variables = { email: 'user@example.org' } it('resolves', async () => { - await expect(client.request(mutation, variables)).resolves.toEqual({"requestPasswordReset": true}) + await expect(client.request(mutation, variables)).resolves.toEqual({ + requestPasswordReset: true, + }) }) it('creates node with label `PasswordReset`', async () => { @@ -59,21 +64,139 @@ describe('passwordReset', () => { expect(resets).toHaveLength(1) }) - it('creates an id used as a reset token', async () => { + it('creates a reset token', async () => { await client.request(mutation, variables) - const [reset] = await getAllPasswordResets() - const { id: token } = reset.properties + const resets = await getAllPasswordResets() + const [reset] = resets + const { token } = reset.properties expect(token).toMatch(/^........-....-....-....-............$/) }) it('created PasswordReset is valid for less than 4 minutes', async () => { await client.request(mutation, variables) - const [reset] = await getAllPasswordResets() + const resets = await getAllPasswordResets() + const [reset] = resets let { validUntil } = reset.properties validUntil = Date.parse(validUntil) - const now = (new Date()).getTime() - expect(validUntil).toBeGreaterThan(now - 60*1000) - expect(validUntil).toBeLessThan(now + 4*60*1000) + const now = new Date().getTime() + expect(validUntil).toBeGreaterThan(now - 60 * 1000) + expect(validUntil).toBeLessThan(now + 4 * 60 * 1000) + }) + }) + }) + + describe('resetPassword', () => { + const setup = async (options = {}) => { + const { + email = 'user@example.org', + validUntil = new Date().getTime() + 3 * 60 * 1000, + token = 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456', + } = options + + const session = driver.session() + await createPasswordReset({ driver, email, validUntil, token }) + session.close() + } + + const mutation = `mutation($token: String!, $email: String!, $newPassword: String!) { resetPassword(token: $token, email: $email, newPassword: $newPassword) }` + let email = 'user@example.org' + let token = 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456' + let newPassword = 'supersecret' + let variables + + describe('invalid email', () => { + it('resolves to false', async () => { + await setup() + variables = { newPassword, email: 'non-existent@example.org', token } + await expect(client.request(mutation, variables)).resolves.toEqual({ resetPassword: false }) + }) + }) + + describe('valid email', () => { + describe('but invalid token', () => { + it('resolves to false', async () => { + await setup() + variables = { newPassword, email, token: 'slkdjfldsjflsdjfsjdfl' } + await expect(client.request(mutation, variables)).resolves.toEqual({ + resetPassword: false, + }) + }) + }) + + describe('but invalid token', () => { + it('resolves to false', async () => { + variables = { newPassword, email: 'user@example.org', token: 'lksjdflksjdflksjdlkfjsf' } + await expect(client.request(mutation, variables)).resolves.toEqual({ + resetPassword: false, + }) + }) + }) + + describe('and valid token', () => { + beforeEach(() => { + variables = { + newPassword, + email: 'user@example.org', + token: 'abcdefgh-ijkl-mnop-qrst-uvwxyz123456', + } + }) + + describe('and token not expired', () => { + beforeEach(async () => { + await setup() + }) + + it('resolves to true', async () => { + await expect(client.request(mutation, variables)).resolves.toEqual({ + resetPassword: true, + }) + }) + + it('updates PasswordReset `redeemedAt` property', async () => { + await client.request(mutation, variables) + const requests = await getAllPasswordResets() + const [request] = requests + const { redeemedAt } = request.properties + expect(redeemedAt).not.toBeNull() + }) + + it('updates password of the user', async () => { + await client.request(mutation, variables) + const checkLoginMutation = ` + mutation($email: String!, $password: String!) { + login(email: $email, password: $password) + } + ` + const expected = expect.objectContaining({ login: expect.any(String) }) + await expect( + client.request(checkLoginMutation, { + email: 'user@example.org', + password: 'supersecret', + }), + ).resolves.toEqual(expected) + }) + }) + + describe('but expired token', () => { + beforeEach(async () => { + const validUntil = new Date().getTime() - 1000 + await setup({ validUntil }) + }) + + it('resolves to false', async () => { + await expect(client.request(mutation, variables)).resolves.toEqual({ + resetPassword: false, + }) + }) + + it('does not update PasswordReset `redeemedAt` property', async () => { + await client.request(mutation, variables) + const requests = await getAllPasswordResets() + const [request] = requests + const { redeemedAt } = request.properties + expect(redeemedAt).toBeUndefined() + }) + }) }) }) }) diff --git a/backend/src/schema/types/schema.gql b/backend/src/schema/types/schema.gql index 358797631..ae77ef8e8 100644 --- a/backend/src/schema/types/schema.gql +++ b/backend/src/schema/types/schema.gql @@ -26,7 +26,7 @@ type Mutation { signup(email: String!, password: String!): Boolean! changePassword(oldPassword: String!, newPassword: String!): String! requestPasswordReset(email: String!): Boolean! - resetPassword(email: String!, resetToken: String!, newPassword: String!): String! + resetPassword(email: String!, token: String!, newPassword: String!): Boolean! report(id: ID!, description: String): Report disable(id: ID!): ID enable(id: ID!): ID From 65df4c5a20b9d05cd08cfd4a33cebab1ccc29461 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 17 Jun 2019 12:16:15 +0200 Subject: [PATCH 11/62] use replace instead of substring --- .../maintenance-worker/migration/neo4j/badges.cql | 2 +- .../maintenance-worker/migration/neo4j/contributions.cql | 2 +- .../maintenance-worker/migration/neo4j/users.cql | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql index 2d1548d4f..027cea019 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/badges.cql @@ -45,7 +45,7 @@ MERGE(b:Badge {id: badge._id["$oid"]}) ON CREATE SET b.key = badge.key, b.type = badge.type, -b.icon = substring(badge.image.path, 38), +b.icon = replace(badge.image.path, 'https://api-alpha.human-connection.org', ''), b.status = badge.status, b.createdAt = badge.createdAt.`$date`, b.updatedAt = badge.updatedAt.`$date` diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql index 70f09e035..472354763 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/contributions.cql @@ -131,7 +131,7 @@ MERGE (p:Post {id: post._id["$oid"]}) ON CREATE SET p.title = post.title, p.slug = post.slug, -p.image = substring(post.teaserImg, 38), +p.image = replace(post.teaserImg, 'https://api-alpha.human-connection.org', ''), p.content = post.content, p.contentExcerpt = post.contentExcerpt, p.visibility = toLower(post.visibility), diff --git a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql index 96251a9ce..4d7c9aa9f 100644 --- a/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql +++ b/deployment/legacy-migration/maintenance-worker/migration/neo4j/users.cql @@ -102,8 +102,8 @@ u.name = user.name, u.slug = user.slug, u.email = user.email, u.password = user.password, -u.avatar = substring(user.avatar, 38), -u.coverImg = substring(user.coverImg, 38), +u.avatar = replace(user.avatar, 'https://api-alpha.human-connection.org', ''), +u.coverImg = replace(user.coverImg, 'https://api-alpha.human-connection.org', ''), u.wasInvited = user.wasInvited, u.wasSeeded = user.wasSeeded, u.role = toLower(user.role), From 5a806ca99e3fba3e178954d462c0f5b32610f078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 12:24:14 +0200 Subject: [PATCH 12/62] Remove duplicate test case --- backend/src/schema/resolvers/passwordReset.spec.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 89b724fca..1fbd96b7a 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -123,15 +123,6 @@ describe('passwordReset', () => { }) }) - describe('but invalid token', () => { - it('resolves to false', async () => { - variables = { newPassword, email: 'user@example.org', token: 'lksjdflksjdflksjdlkfjsf' } - await expect(client.request(mutation, variables)).resolves.toEqual({ - resetPassword: false, - }) - }) - }) - describe('and valid token', () => { beforeEach(() => { variables = { From 7228d68149e10517be360f1f6db3a08285ffd633 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 12:30:39 +0200 Subject: [PATCH 13/62] Write a nice form to reset your password --- webapp/locales/de.json | 6 +++++- webapp/locales/en.json | 6 +++++- webapp/pages/password-reset.vue | 23 ++++++++++++++++++++--- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index d783f9e37..14ab9d906 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -15,7 +15,11 @@ "hello": "Hallo" }, "password-reset": { - "title": "Passwort zurücksetzen" + "title": "Passwort zurücksetzen", + "form": { + "description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.", + "submit": "Email anfordern" + } }, "editor": { "placeholder": "Schreib etwas Inspirierendes..." diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 3a5405eec..3c2b7c8d8 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -15,7 +15,11 @@ "hello": "Hello" }, "password-reset": { - "title": "Reset your password" + "title": "Reset your password", + "form": { + "description": "A password reset email will be sent to the given email address.", + "submit": "Request email" + } }, "editor": { "placeholder": "Leave your inspirational thoughts..." diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index 0205ea4f4..bd7da49be 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -3,9 +3,26 @@ - - {{ $t('password-reset.title') }} - + + +
+ + + + {{ $t('password-reset.form.description') }} + + + + {{ $t('password-reset.form.submit') }} + + +
+
From c4eb2178f241a7119028888c028768176c71dbf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 12:39:36 +0200 Subject: [PATCH 14/62] Scaffold page component test for password reset --- webapp/pages/password-reset.spec.js | 41 +++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 webapp/pages/password-reset.spec.js diff --git a/webapp/pages/password-reset.spec.js b/webapp/pages/password-reset.spec.js new file mode 100644 index 000000000..3c1f5b286 --- /dev/null +++ b/webapp/pages/password-reset.spec.js @@ -0,0 +1,41 @@ +import { shallowMount, createLocalVue } from '@vue/test-utils' +import PasswordResetPage from './password-reset.vue' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('ProfileSlug', () => { + let wrapper + let Wrapper + let mocks + + beforeEach(() => { + mocks = { + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + $apollo: { + loading: false, + mutate: jest.fn().mockResolvedValue(), + }, + } + }) + + describe('shallowMount', () => { + Wrapper = () => { + return shallowMount(PasswordResetPage, { + mocks, + localVue, + }) + } + + it('renders a password reset form', () => { + wrapper = Wrapper() + expect(wrapper.find('.password-reset-card').exists()).toBe(true) + }) + }) +}) From 4dde53f67d4dca075240c90466fd765c67e24d19 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 17 Jun 2019 13:30:18 +0200 Subject: [PATCH 15/62] added coverage report text in oder to fix coverage --- webapp/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/package.json b/webapp/package.json index 1abaf479b..4f203ef5c 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -29,6 +29,7 @@ "!**/?(*.)+(spec|test).js?(x)" ], "coverageReporters": [ + "text", "lcov" ], "transform": { @@ -110,4 +111,4 @@ "vue-jest": "~3.0.4", "vue-svg-loader": "~0.12.0" } -} +} \ No newline at end of file From a501e1145daf8ad8fe4200b19750963a2018a879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 14:02:25 +0200 Subject: [PATCH 16/62] Component test to call a mutation passes --- webapp/locales/de.json | 8 +++- webapp/locales/en.json | 8 +++- webapp/pages/password-reset.spec.js | 31 +++++++++++++-- webapp/pages/password-reset.vue | 59 +++++++++++++++++++++++++++-- 4 files changed, 96 insertions(+), 10 deletions(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 14ab9d906..7497648e1 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -18,7 +18,8 @@ "title": "Passwort zurücksetzen", "form": { "description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.", - "submit": "Email anfordern" + "submit": "Email anfordern", + "submitted": "Eine E-Mail zum Zurücksetzen wurde angefordert" } }, "editor": { @@ -201,7 +202,10 @@ "name": "Name", "loadMore": "mehr laden", "loading": "wird geladen", - "reportContent": "Melden" + "reportContent": "Melden", + "validations": { + "email": "muss eine gültige E-Mail Adresse sein" + } }, "actions": { "loading": "lade", diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 3c2b7c8d8..fd7968428 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -18,7 +18,8 @@ "title": "Reset your password", "form": { "description": "A password reset email will be sent to the given email address.", - "submit": "Request email" + "submit": "Request email", + "submitted": "Reset email was requested" } }, "editor": { @@ -201,7 +202,10 @@ "name": "Name", "loadMore": "load more", "loading": "loading", - "reportContent": "Report" + "reportContent": "Report", + "validations": { + "email": "must be a valid email address" + } }, "actions": { "loading": "loading", diff --git a/webapp/pages/password-reset.spec.js b/webapp/pages/password-reset.spec.js index 3c1f5b286..bba001614 100644 --- a/webapp/pages/password-reset.spec.js +++ b/webapp/pages/password-reset.spec.js @@ -1,4 +1,4 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils' +import { mount, createLocalVue } from '@vue/test-utils' import PasswordResetPage from './password-reset.vue' import Styleguide from '@human-connection/styleguide' @@ -25,9 +25,9 @@ describe('ProfileSlug', () => { } }) - describe('shallowMount', () => { + describe('mount', () => { Wrapper = () => { - return shallowMount(PasswordResetPage, { + return mount(PasswordResetPage, { mocks, localVue, }) @@ -37,5 +37,30 @@ describe('ProfileSlug', () => { wrapper = Wrapper() expect(wrapper.find('.password-reset-card').exists()).toBe(true) }) + + describe('submit', () => { + beforeEach(async () => { + wrapper = Wrapper() + wrapper.find('input#email').setValue('mail@example.org') + await wrapper.find('form').trigger('submit') + }) + + it('calls requestPasswordReset graphql mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it.todo('delivers email to backend') + it.todo('disables form to avoid re-submission') + it.todo('displays a message that a password email was requested') + }) + + describe('given password reset token as URL param', () => { + it.todo('displays a form to update your password') + describe('submitting new password', () => { + it.todo('calls resetPassword graphql mutation') + it.todo('delivers new password to backend') + it.todo('displays success message') + }) + }) }) }) diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index bd7da49be..0ad9b7997 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -5,10 +5,17 @@ -
+ @@ -17,10 +24,12 @@ {{ $t('password-reset.form.description') }} - + {{ $t('password-reset.form.submit') }} - +
@@ -30,7 +39,51 @@ From de9ed55738f31442f81f19cbe9aa90c20c2947d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 14:29:24 +0200 Subject: [PATCH 17/62] Display nice success message on password reset --- webapp/locales/de.json | 2 +- webapp/locales/en.json | 2 +- webapp/pages/password-reset.spec.js | 18 ++++++++--- webapp/pages/password-reset.vue | 49 +++++++++++++++++++++-------- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 7497648e1..88afa07e0 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -19,7 +19,7 @@ "form": { "description": "Eine Mail zum Zurücksetzen des Passworts wird an die angegebene E-Mail Adresse geschickt.", "submit": "Email anfordern", - "submitted": "Eine E-Mail zum Zurücksetzen wurde angefordert" + "submitted": "E-Mail verschickt an {email}" } }, "editor": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index fd7968428..53ad9fb40 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -19,7 +19,7 @@ "form": { "description": "A password reset email will be sent to the given email address.", "submit": "Request email", - "submitted": "Reset email was requested" + "submitted": "Email sent to {email}" } }, "editor": { diff --git a/webapp/pages/password-reset.spec.js b/webapp/pages/password-reset.spec.js index bba001614..e78ace4ba 100644 --- a/webapp/pages/password-reset.spec.js +++ b/webapp/pages/password-reset.spec.js @@ -20,7 +20,7 @@ describe('ProfileSlug', () => { $t: jest.fn(), $apollo: { loading: false, - mutate: jest.fn().mockResolvedValue(), + mutate: jest.fn().mockResolvedValue({ data: { reqestPasswordReset: true } }), }, } }) @@ -49,9 +49,19 @@ describe('ProfileSlug', () => { expect(mocks.$apollo.mutate).toHaveBeenCalled() }) - it.todo('delivers email to backend') - it.todo('disables form to avoid re-submission') - it.todo('displays a message that a password email was requested') + it('delivers email to backend', () => { + const expected = expect.objectContaining({ variables: { email: 'mail@example.org' } }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + it('hides form to avoid re-submission', () => { + expect(wrapper.find('form').exists()).not.toBeTruthy() + }) + + it('displays a message that a password email was requested', () => { + const expected = ['password-reset.form.submitted', { email: 'mail@example.org' }] + expect(mocks.$t).toHaveBeenCalledWith(...expected) + }) }) describe('given password reset token as URL param', () => { diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index 0ad9b7997..9e7d2d195 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -6,11 +6,13 @@ + @submit="handleSubmit" + > + :loading="$apollo.loading" + primary + fullwidth + name="submit" + type="submit" + icon="envelope" + > {{ $t('password-reset.form.submit') }} +
+ + + + + + +
@@ -40,33 +56,43 @@ From 0e3eb4327678f64ed47dd1d5c11e1cad3c4c829a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 14:53:28 +0200 Subject: [PATCH 18/62] Split PasswordResetPage into subcomponents --- .../PasswordReset/PasswordReset.spec.js} | 4 +- .../PasswordReset/PasswordReset.vue | 103 ++++++++++++++++++ webapp/pages/password-reset.vue | 98 +---------------- 3 files changed, 109 insertions(+), 96 deletions(-) rename webapp/{pages/password-reset.spec.js => components/PasswordReset/PasswordReset.spec.js} (95%) create mode 100644 webapp/components/PasswordReset/PasswordReset.vue diff --git a/webapp/pages/password-reset.spec.js b/webapp/components/PasswordReset/PasswordReset.spec.js similarity index 95% rename from webapp/pages/password-reset.spec.js rename to webapp/components/PasswordReset/PasswordReset.spec.js index e78ace4ba..e55b9273a 100644 --- a/webapp/pages/password-reset.spec.js +++ b/webapp/components/PasswordReset/PasswordReset.spec.js @@ -1,5 +1,5 @@ import { mount, createLocalVue } from '@vue/test-utils' -import PasswordResetPage from './password-reset.vue' +import PasswordReset from './PasswordReset' import Styleguide from '@human-connection/styleguide' const localVue = createLocalVue() @@ -27,7 +27,7 @@ describe('ProfileSlug', () => { describe('mount', () => { Wrapper = () => { - return mount(PasswordResetPage, { + return mount(PasswordReset, { mocks, localVue, }) diff --git a/webapp/components/PasswordReset/PasswordReset.vue b/webapp/components/PasswordReset/PasswordReset.vue new file mode 100644 index 000000000..7a74bbe31 --- /dev/null +++ b/webapp/components/PasswordReset/PasswordReset.vue @@ -0,0 +1,103 @@ + + + diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index 9e7d2d195..98642cfe8 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -3,51 +3,7 @@ - - - - - - - {{ $t('password-reset.form.description') }} - - - - {{ $t('password-reset.form.submit') }} - - -
- - - - - - -
-
-
+
@@ -55,58 +11,12 @@ From aa6855434de199fa68e09125d0ea5642db759249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Mon, 17 Jun 2019 15:10:57 +0200 Subject: [PATCH 19/62] Emit `submitted` from PasswordReset component --- .../PasswordReset/PasswordReset.spec.js | 19 +++---- .../PasswordReset/PasswordReset.vue | 6 ++- .../PasswordReset/VerifyCode.spec.js | 50 +++++++++++++++++++ .../components/PasswordReset/VerifyCode.vue | 9 ++++ webapp/locales/de.json | 5 ++ webapp/locales/en.json | 5 ++ 6 files changed, 84 insertions(+), 10 deletions(-) create mode 100644 webapp/components/PasswordReset/VerifyCode.spec.js create mode 100644 webapp/components/PasswordReset/VerifyCode.vue diff --git a/webapp/components/PasswordReset/PasswordReset.spec.js b/webapp/components/PasswordReset/PasswordReset.spec.js index e55b9273a..6284fed36 100644 --- a/webapp/components/PasswordReset/PasswordReset.spec.js +++ b/webapp/components/PasswordReset/PasswordReset.spec.js @@ -6,7 +6,7 @@ const localVue = createLocalVue() localVue.use(Styleguide) -describe('ProfileSlug', () => { +describe('PasswordReset', () => { let wrapper let Wrapper let mocks @@ -26,6 +26,8 @@ describe('ProfileSlug', () => { }) describe('mount', () => { + beforeEach(jest.useFakeTimers) + Wrapper = () => { return mount(PasswordReset, { mocks, @@ -35,7 +37,7 @@ describe('ProfileSlug', () => { it('renders a password reset form', () => { wrapper = Wrapper() - expect(wrapper.find('.password-reset-card').exists()).toBe(true) + expect(wrapper.find('.password-reset').exists()).toBe(true) }) describe('submit', () => { @@ -62,14 +64,13 @@ describe('ProfileSlug', () => { const expected = ['password-reset.form.submitted', { email: 'mail@example.org' }] expect(mocks.$t).toHaveBeenCalledWith(...expected) }) - }) - describe('given password reset token as URL param', () => { - it.todo('displays a form to update your password') - describe('submitting new password', () => { - it.todo('calls resetPassword graphql mutation') - it.todo('delivers new password to backend') - it.todo('displays success message') + describe('after animation', () => { + beforeEach(jest.runAllTimers) + + it('emits `submitted`', () => { + expect(wrapper.emitted('submitted')).toBeTruthy() + }) }) }) }) diff --git a/webapp/components/PasswordReset/PasswordReset.vue b/webapp/components/PasswordReset/PasswordReset.vue index 7a74bbe31..175207949 100644 --- a/webapp/components/PasswordReset/PasswordReset.vue +++ b/webapp/components/PasswordReset/PasswordReset.vue @@ -1,5 +1,5 @@ + + diff --git a/webapp/locales/de.json b/webapp/locales/de.json index dece0fcff..3610ff68b 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -24,8 +24,12 @@ }, "verify-code": { "form": { + "input": "Code eingeben", "description": "Öffne Deine E-Mail Postfach und gib den Code ein, den wir geschickt haben.", - "submit": "Sicherheitscode überprüfen" + "submit": "Sicherheitscode überprüfen", + "validations": { + "code": "muss genau 6 Buchstaben lang sein" + } } }, "editor": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index d18603278..225c26391 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -26,7 +26,10 @@ "form": { "input": "Enter your code", "description": "Open your inbox and enter the code that we've sent to you.", - "submit": "Check security code" + "submit": "Check security code", + "validations": { + "code": "must be 6 characters long" + } } }, "editor": { From 8fc74b9e14c2fea130865c14a246f7cd4032eff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 12:09:06 +0200 Subject: [PATCH 23/62] Put validation error translations together --- webapp/components/PasswordReset/PasswordReset.vue | 2 +- webapp/components/PasswordReset/VerifyCode.vue | 2 +- webapp/locales/de.json | 8 +++----- webapp/locales/en.json | 8 +++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/webapp/components/PasswordReset/PasswordReset.vue b/webapp/components/PasswordReset/PasswordReset.vue index e7d3a5f65..e34efc070 100644 --- a/webapp/components/PasswordReset/PasswordReset.vue +++ b/webapp/components/PasswordReset/PasswordReset.vue @@ -63,7 +63,7 @@ export default { email: { type: 'email', required: true, - message: this.$t('password-reset.form.validations.email'), + message: this.$t('common.validations.email'), }, }, disabled: true, diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 7df5395f0..13415a5fa 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -41,7 +41,7 @@ export default { min: 6, max: 6, required: true, - message: this.$t('verify-code.form.validations.code'), + message: this.$t('common.validations.verification-code'), }, }, } diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 3610ff68b..2c5f9a07e 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -26,10 +26,7 @@ "form": { "input": "Code eingeben", "description": "Öffne Deine E-Mail Postfach und gib den Code ein, den wir geschickt haben.", - "submit": "Sicherheitscode überprüfen", - "validations": { - "code": "muss genau 6 Buchstaben lang sein" - } + "submit": "Sicherheitscode überprüfen" } }, "editor": { @@ -214,7 +211,8 @@ "loading": "wird geladen", "reportContent": "Melden", "validations": { - "email": "muss eine gültige E-Mail Adresse sein" + "email": "muss eine gültige E-Mail Adresse sein", + "verification-code": "muss genau 6 Buchstaben lang sein" } }, "actions": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 225c26391..44d1153da 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -26,10 +26,7 @@ "form": { "input": "Enter your code", "description": "Open your inbox and enter the code that we've sent to you.", - "submit": "Check security code", - "validations": { - "code": "must be 6 characters long" - } + "submit": "Check security code" } }, "editor": { @@ -214,7 +211,8 @@ "loading": "loading", "reportContent": "Report", "validations": { - "email": "must be a valid email address" + "email": "must be a valid email address", + "verification-code": "must be 6 characters long" } }, "actions": { From de40499007529de22b8e8c756b8fa04a3f2e521d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 12:41:58 +0200 Subject: [PATCH 24/62] Move to next step after verify code --- webapp/components/PasswordReset/VerifyCode.spec.js | 11 ++++++++++- webapp/components/PasswordReset/VerifyCode.vue | 10 +++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index 92d6f8d07..3904f62f2 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -39,7 +39,16 @@ describe('VerifyCode ', () => { }) describe('after verification code given', () => { - it.todo('displays a form to update your password') + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('input').setValue('123456') + wrapper.find('form').trigger('submit') + }) + + it('displays a form to update your password', () => { + expect(wrapper.find('.change-password').exists()).toBe(true) + }) + describe('submitting new password', () => { it.todo('calls resetPassword graphql mutation') it.todo('delivers new password to backend') diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 13415a5fa..3defca13d 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -1,7 +1,7 @@ From 5a781b0bc6c494509174c696cd233ae462aeb49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 12:58:43 +0200 Subject: [PATCH 25/62] Copy+Paste code form Password/Change, DRY later --- .../components/PasswordReset/VerifyCode.vue | 122 ++++++++++++++---- 1 file changed, 97 insertions(+), 25 deletions(-) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 3defca13d..bd214bbef 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -1,7 +1,14 @@ From a641ab68840da4b6c2f848f2ba1f6664a4eb3b30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 13:01:19 +0200 Subject: [PATCH 26/62] Turn off autocomplete for all password fields --- webapp/components/Password/Change.vue | 3 +++ webapp/components/PasswordReset/VerifyCode.vue | 2 ++ 2 files changed, 5 insertions(+) diff --git a/webapp/components/Password/Change.vue b/webapp/components/Password/Change.vue index 95da2a7be..63c797157 100644 --- a/webapp/components/Password/Change.vue +++ b/webapp/components/Password/Change.vue @@ -11,18 +11,21 @@ id="oldPassword" model="oldPassword" type="password" + autocomplete="off" :label="$t('settings.security.change-password.label-old-password')" /> diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index bd214bbef..6e8f116ed 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -37,12 +37,14 @@ id="newPassword" model="newPassword" type="password" + autocomplete="off" :label="$t('settings.security.change-password.label-new-password')" /> From 288e5002fd4dd21b98993a8fffd76f77e0fbf485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 13:13:13 +0200 Subject: [PATCH 27/62] Flesh out VerifyCode.spec.js --- .../PasswordReset/VerifyCode.spec.js | 26 ++++++++++++++++--- webapp/locales/de.json | 6 ++++- webapp/locales/en.json | 6 ++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index 3904f62f2..63e8ce2b3 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -20,7 +20,7 @@ describe('VerifyCode ', () => { $t: jest.fn(), $apollo: { loading: false, - mutate: jest.fn().mockResolvedValue({ data: { resetPassword: false } }), + mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }), }, } }) @@ -50,9 +50,27 @@ describe('VerifyCode ', () => { }) describe('submitting new password', () => { - it.todo('calls resetPassword graphql mutation') - it.todo('delivers new password to backend') - it.todo('displays success message') + beforeEach(() => { + wrapper.find('input#newPassword').setValue('supersecret') + wrapper.find('input#confirmPassword').setValue('supersecret') + wrapper.find('form').trigger('submit') + }) + + it('calls resetPassword graphql mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('delivers new password to backend', () => { + const expected = expect.objectContaining({ variables: { newPassword: 'supersecret' } }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + describe('password reset successful', () => { + it('displays success message', () => { + const expected = 'verify-code.change-password.sucess' + expect(mocks.$t).toHaveBeenCalledWith(expected) + }) + }) }) }) }) diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 2c5f9a07e..cd04befe0 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -26,7 +26,11 @@ "form": { "input": "Code eingeben", "description": "Öffne Deine E-Mail Postfach und gib den Code ein, den wir geschickt haben.", - "submit": "Sicherheitscode überprüfen" + "submit": "Sicherheitscode überprüfen", + "change-password":{ + "success": "Änderung des Passworts war erfolgreich", + "failure": "Passwort Änderung fehlgeschlagen. Möglicherweise falscher Sicherheitscode." + } } }, "editor": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 44d1153da..b0dcc0f03 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -26,7 +26,11 @@ "form": { "input": "Enter your code", "description": "Open your inbox and enter the code that we've sent to you.", - "submit": "Check security code" + "submit": "Check security code", + "change-password": { + "success": "Changing your password was successful", + "failure": "Changing your password failed. Probably the security code was not correct" + } } }, "editor": { From 559210d204bb2be26bed55aad00395160a7e65bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 13:23:37 +0200 Subject: [PATCH 28/62] Oh, forgot, you have to add the email again Well, this is not good practice. If an attacker has access to the mailbox then she knows also the email account as well. It's better to ask the user for the unique username, e.g. `@username`. https://stackoverflow.com/a/16018373 --- .../PasswordReset/VerifyCode.spec.js | 3 ++- .../components/PasswordReset/VerifyCode.vue | 22 +++++++++++++++---- webapp/locales/de.json | 2 +- webapp/locales/en.json | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index 63e8ce2b3..0d198721b 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -41,7 +41,8 @@ describe('VerifyCode ', () => { describe('after verification code given', () => { beforeEach(() => { wrapper = Wrapper() - wrapper.find('input').setValue('123456') + wrapper.find('input#email').setValue('mail@example.org') + wrapper.find('input#code').setValue('123456') wrapper.find('form').trigger('submit') }) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 6e8f116ed..67a164ba6 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -2,7 +2,7 @@ + @@ -68,9 +76,15 @@ export default { return { verification: { formData: { + email: '', code: '', }, formSchema: { + email: { + type: 'email', + required: true, + message: this.$t('common.validations.email'), + }, code: { type: 'string', min: 6, @@ -103,7 +117,7 @@ export default { ], }, }, - codeSubmitted: false, + verificationSubmitted: false, disabled: true, } }, @@ -115,7 +129,7 @@ export default { this.disabled = false }, handleSubmitVerify() { - this.codeSubmitted = true + this.verificationSubmitted = true }, handleSubmitPassword() {}, matchPassword(rule, value, callback, source, options) { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index cd04befe0..502f72607 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -24,7 +24,7 @@ }, "verify-code": { "form": { - "input": "Code eingeben", + "code": "Code eingeben", "description": "Öffne Deine E-Mail Postfach und gib den Code ein, den wir geschickt haben.", "submit": "Sicherheitscode überprüfen", "change-password":{ diff --git a/webapp/locales/en.json b/webapp/locales/en.json index b0dcc0f03..2aac32ca6 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -24,7 +24,7 @@ }, "verify-code": { "form": { - "input": "Enter your code", + "code": "Enter your code", "description": "Open your inbox and enter the code that we've sent to you.", "submit": "Check security code", "change-password": { From 3948cb8ace7f426840807f99b932a28b6dceb394 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 13:48:30 +0200 Subject: [PATCH 29/62] VerifyCode.spec passes --- .../PasswordReset/VerifyCode.spec.js | 16 ++- .../components/PasswordReset/VerifyCode.vue | 107 ++++++++++++------ webapp/locales/de.json | 2 +- webapp/locales/en.json | 2 +- 4 files changed, 90 insertions(+), 37 deletions(-) diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index 0d198721b..217c58ce9 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -26,6 +26,8 @@ describe('VerifyCode ', () => { }) describe('mount', () => { + beforeEach(jest.useFakeTimers) + Wrapper = () => { return mount(VerifyCode, { mocks, @@ -62,15 +64,25 @@ describe('VerifyCode ', () => { }) it('delivers new password to backend', () => { - const expected = expect.objectContaining({ variables: { newPassword: 'supersecret' } }) + const expected = expect.objectContaining({ + variables: { token: '123456', email: 'mail@example.org', newPassword: 'supersecret' }, + }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) describe('password reset successful', () => { it('displays success message', () => { - const expected = 'verify-code.change-password.sucess' + const expected = 'verify-code.form.change-password.success' expect(mocks.$t).toHaveBeenCalledWith(expected) }) + + describe('after animation', () => { + beforeEach(jest.runAllTimers) + + it('emits `change-password-sucess`', () => { + expect(wrapper.emitted('change-password-result')).toEqual([['success']]) + }) + }) }) }) }) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index 67a164ba6..49f951324 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -32,42 +32,49 @@ {{ $t('verify-code.form.submit') }}
- - - - - - - {{ $t('settings.security.change-password.button') }} - - - + diff --git a/webapp/pages/password-reset/request.vue b/webapp/pages/password-reset/request.vue new file mode 100644 index 000000000..7cf37f537 --- /dev/null +++ b/webapp/pages/password-reset/request.vue @@ -0,0 +1,18 @@ + + + diff --git a/webapp/pages/password-reset/verify-code.vue b/webapp/pages/password-reset/verify-code.vue new file mode 100644 index 000000000..e331c673c --- /dev/null +++ b/webapp/pages/password-reset/verify-code.vue @@ -0,0 +1,20 @@ + + + From 304fd028f0ccb1a49f2aecfb63bfe15e91ad7092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 18 Jun 2019 23:51:54 +0200 Subject: [PATCH 39/62] Leftover token=>code --- webapp/components/PasswordReset/VerifyCode.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index cf12aacac..6f489e55f 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -65,7 +65,7 @@ describe('VerifyCode ', () => { it('delivers new password to backend', () => { const expected = expect.objectContaining({ - variables: { token: '123456', email: 'mail@example.org', newPassword: 'supersecret' }, + variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' }, }) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) }) From 65471efb0d19a4f9130ef6e4aa2ac168758db2bc Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 19 Jun 2019 13:08:51 +0200 Subject: [PATCH 40/62] removed fixImageUrls reference --- backend/src/middleware/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index aae2dcef3..9b85bd340 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -40,7 +40,6 @@ export default schema => { 'excerpt', 'notifications', 'xss', - 'fixImageUrls', 'softDelete', 'user', 'includedFields', From 7613ddfc04c2e3f8fd62fc954f1c6486e5fdaeff Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 19 Jun 2019 13:36:14 +0200 Subject: [PATCH 41/62] lint fixes --- backend/src/activitypub/NitroDataSource.js | 8 ++------ backend/src/middleware/notifications/spec.js | 4 +--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/backend/src/activitypub/NitroDataSource.js b/backend/src/activitypub/NitroDataSource.js index eea37337a..0900bed6c 100644 --- a/backend/src/activitypub/NitroDataSource.js +++ b/backend/src/activitypub/NitroDataSource.js @@ -505,9 +505,7 @@ export default class NitroDataSource { const result2 = await this.client.mutate({ mutation: gql` mutation { - AddCommentAuthor(from: {id: "${ - result.data.CreateComment.id - }"}, to: {id: "${toUserId}"}) { + AddCommentAuthor(from: {id: "${result.data.CreateComment.id}"}, to: {id: "${toUserId}"}) { id } } @@ -519,9 +517,7 @@ export default class NitroDataSource { result = await this.client.mutate({ mutation: gql` mutation { - AddCommentPost(from: { id: "${ - result.data.CreateComment.id - }", to: { id: "${postId}" }}) { + AddCommentPost(from: { id: "${result.data.CreateComment.id}", to: { id: "${postId}" }}) { id } } diff --git a/backend/src/middleware/notifications/spec.js b/backend/src/middleware/notifications/spec.js index 65212e544..985654b0f 100644 --- a/backend/src/middleware/notifications/spec.js +++ b/backend/src/middleware/notifications/spec.js @@ -87,9 +87,7 @@ describe('currentUser { notifications }', () => { describe('who mentions me again', () => { beforeEach(async () => { - const updatedContent = `${ - post.content - } One more mention to @al-capone` + const updatedContent = `${post.content} One more mention to @al-capone` // The response `post.content` contains a link but the XSSmiddleware // should have the `mention` CSS class removed. I discovered this // during development and thought: A feature not a bug! This way we From a7eced5f8e26c866472fee73271f62a8f8d0ca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Jun 2019 13:40:08 +0200 Subject: [PATCH 42/62] Redirect to `/' if user is authenticated --- webapp/pages/password-reset.vue | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/webapp/pages/password-reset.vue b/webapp/pages/password-reset.vue index ca826524e..a781bd3fb 100644 --- a/webapp/pages/password-reset.vue +++ b/webapp/pages/password-reset.vue @@ -13,5 +13,10 @@ From c85c94aa40c3d91a474f76d6908b84d232ce5b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 19 Jun 2019 14:07:35 +0200 Subject: [PATCH 43/62] Splitting components, better route navigation This also allows us to generate a password reset link to quickly reset your password without entering the code and email manually. --- .../PasswordReset/ChangePassword.spec.js | 83 +++++++++ .../PasswordReset/ChangePassword.vue | 140 +++++++++++++++ ...{PasswordReset.spec.js => Request.spec.js} | 6 +- .../{PasswordReset.vue => Request.vue} | 0 .../PasswordReset/VerifyCode.spec.js | 47 +----- .../components/PasswordReset/VerifyCode.vue | 159 +++--------------- .../pages/password-reset/change-password.vue | 28 +++ webapp/pages/password-reset/request.vue | 6 +- webapp/pages/password-reset/verify-code.vue | 8 +- 9 files changed, 282 insertions(+), 195 deletions(-) create mode 100644 webapp/components/PasswordReset/ChangePassword.spec.js create mode 100644 webapp/components/PasswordReset/ChangePassword.vue rename webapp/components/PasswordReset/{PasswordReset.spec.js => Request.spec.js} (94%) rename webapp/components/PasswordReset/{PasswordReset.vue => Request.vue} (100%) create mode 100644 webapp/pages/password-reset/change-password.vue diff --git a/webapp/components/PasswordReset/ChangePassword.spec.js b/webapp/components/PasswordReset/ChangePassword.spec.js new file mode 100644 index 000000000..88caa6c6d --- /dev/null +++ b/webapp/components/PasswordReset/ChangePassword.spec.js @@ -0,0 +1,83 @@ +import { mount, createLocalVue } from '@vue/test-utils' +import ChangePassword from './ChangePassword' +import Styleguide from '@human-connection/styleguide' + +const localVue = createLocalVue() + +localVue.use(Styleguide) + +describe('ChangePassword ', () => { + let wrapper + let Wrapper + let mocks + let propsData + + beforeEach(() => { + propsData = {} + mocks = { + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $t: jest.fn(), + $apollo: { + loading: false, + mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }), + }, + } + }) + + describe('mount', () => { + beforeEach(jest.useFakeTimers) + + Wrapper = () => { + return mount(ChangePassword, { + mocks, + propsData, + localVue, + }) + } + + describe('given email and verification code', () => { + beforeEach(() => { + propsData.email = 'mail@example.org' + propsData.code = '123456' + }) + + describe('submitting new password', () => { + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('input#newPassword').setValue('supersecret') + wrapper.find('input#confirmPassword').setValue('supersecret') + wrapper.find('form').trigger('submit') + }) + + it('calls resetPassword graphql mutation', () => { + expect(mocks.$apollo.mutate).toHaveBeenCalled() + }) + + it('delivers new password to backend', () => { + const expected = expect.objectContaining({ + variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' }, + }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + + describe('password reset successful', () => { + it('displays success message', () => { + const expected = 'verify-code.form.change-password.success' + expect(mocks.$t).toHaveBeenCalledWith(expected) + }) + + describe('after animation', () => { + beforeEach(jest.runAllTimers) + + it('emits `change-password-sucess`', () => { + expect(wrapper.emitted('passwordResetResponse')).toEqual([['success']]) + }) + }) + }) + }) + }) + }) +}) diff --git a/webapp/components/PasswordReset/ChangePassword.vue b/webapp/components/PasswordReset/ChangePassword.vue new file mode 100644 index 000000000..5a12f9938 --- /dev/null +++ b/webapp/components/PasswordReset/ChangePassword.vue @@ -0,0 +1,140 @@ + + + diff --git a/webapp/components/PasswordReset/PasswordReset.spec.js b/webapp/components/PasswordReset/Request.spec.js similarity index 94% rename from webapp/components/PasswordReset/PasswordReset.spec.js rename to webapp/components/PasswordReset/Request.spec.js index 1adf68ee5..e7a1f6866 100644 --- a/webapp/components/PasswordReset/PasswordReset.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -1,12 +1,12 @@ import { mount, createLocalVue } from '@vue/test-utils' -import PasswordReset from './PasswordReset' +import Request from './Request' import Styleguide from '@human-connection/styleguide' const localVue = createLocalVue() localVue.use(Styleguide) -describe('PasswordReset', () => { +describe('Request', () => { let wrapper let Wrapper let mocks @@ -29,7 +29,7 @@ describe('PasswordReset', () => { beforeEach(jest.useFakeTimers) Wrapper = () => { - return mount(PasswordReset, { + return mount(Request, { mocks, localVue, }) diff --git a/webapp/components/PasswordReset/PasswordReset.vue b/webapp/components/PasswordReset/Request.vue similarity index 100% rename from webapp/components/PasswordReset/PasswordReset.vue rename to webapp/components/PasswordReset/Request.vue diff --git a/webapp/components/PasswordReset/VerifyCode.spec.js b/webapp/components/PasswordReset/VerifyCode.spec.js index 6f489e55f..062e7e8f7 100644 --- a/webapp/components/PasswordReset/VerifyCode.spec.js +++ b/webapp/components/PasswordReset/VerifyCode.spec.js @@ -13,15 +13,7 @@ describe('VerifyCode ', () => { beforeEach(() => { mocks = { - $toast: { - success: jest.fn(), - error: jest.fn(), - }, $t: jest.fn(), - $apollo: { - loading: false, - mutate: jest.fn().mockResolvedValue({ data: { resetPassword: true } }), - }, } }) @@ -48,42 +40,9 @@ describe('VerifyCode ', () => { wrapper.find('form').trigger('submit') }) - it('displays a form to update your password', () => { - expect(wrapper.find('.change-password').exists()).toBe(true) - }) - - describe('submitting new password', () => { - beforeEach(() => { - wrapper.find('input#newPassword').setValue('supersecret') - wrapper.find('input#confirmPassword').setValue('supersecret') - wrapper.find('form').trigger('submit') - }) - - it('calls resetPassword graphql mutation', () => { - expect(mocks.$apollo.mutate).toHaveBeenCalled() - }) - - it('delivers new password to backend', () => { - const expected = expect.objectContaining({ - variables: { code: '123456', email: 'mail@example.org', newPassword: 'supersecret' }, - }) - expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) - }) - - describe('password reset successful', () => { - it('displays success message', () => { - const expected = 'verify-code.form.change-password.success' - expect(mocks.$t).toHaveBeenCalledWith(expected) - }) - - describe('after animation', () => { - beforeEach(jest.runAllTimers) - - it('emits `change-password-sucess`', () => { - expect(wrapper.emitted('passwordResetResponse')).toEqual([['success']]) - }) - }) - }) + it('emits `verifyCode`', () => { + const expected = [[{ code: '123456', email: 'mail@example.org' }]] + expect(wrapper.emitted('verification')).toEqual(expected) }) }) }) diff --git a/webapp/components/PasswordReset/VerifyCode.vue b/webapp/components/PasswordReset/VerifyCode.vue index d53d08bf2..410a027af 100644 --- a/webapp/components/PasswordReset/VerifyCode.vue +++ b/webapp/components/PasswordReset/VerifyCode.vue @@ -2,9 +2,8 @@ - diff --git a/webapp/pages/password-reset/request.vue b/webapp/pages/password-reset/request.vue index 7cf37f537..9148b4ed4 100644 --- a/webapp/pages/password-reset/request.vue +++ b/webapp/pages/password-reset/request.vue @@ -1,13 +1,13 @@