From 5dd74ef751e19bfae68ca1facadb45ff8f6d766a Mon Sep 17 00:00:00 2001 From: Tomas Bednarik Date: Wed, 30 Oct 2019 13:20:46 +0100 Subject: [PATCH 01/26] Feature added, tags clickable as hashtags --- webapp/components/Hashtag/Hashtag.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/components/Hashtag/Hashtag.vue b/webapp/components/Hashtag/Hashtag.vue index a6085fabc..577834024 100644 --- a/webapp/components/Hashtag/Hashtag.vue +++ b/webapp/components/Hashtag/Hashtag.vue @@ -1,5 +1,5 @@ From 3f7a7913c0fd170bb084128379f10ef0934a04cc Mon Sep 17 00:00:00 2001 From: Stephen Hogsten Date: Wed, 30 Oct 2019 10:53:39 -0700 Subject: [PATCH 05/26] convert email to lowercase before submitting --- webapp/components/PasswordReset/Request.vue | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index bcececcca..6f0176f12 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -68,9 +68,12 @@ export default { } }, computed: { - submitMessage() { + lowercaseEmail() { const { email } = this.formData - return this.$t('components.password-reset.request.form.submitted', { email }) + return email.toLowerCase() + }, + submitMessage() { + return this.$t('components.password-reset.request.form.submitted', { email: this.lowercaseEmail }) }, }, methods: { @@ -86,7 +89,7 @@ export default { requestPasswordReset(email: $email) } ` - const { email } = this.formData + const email = this.lowercaseEmail try { await this.$apollo.mutate({ mutation, variables: { email } }) From 1d1517650066cc91ea227085c0827dc74ce04973 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 4 Nov 2019 22:07:05 +0100 Subject: [PATCH 06/26] Write a test for #2057 --- webapp/components/PasswordReset/Request.spec.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index 594d6628d..4a6dbde9f 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -84,5 +84,18 @@ describe('Request', () => { }) }) }) + + describe('capital letters in a gmail address', () => { + beforeEach(async () => { + wrapper = Wrapper() + wrapper.find('input#email').setValue('mAiL@gmail.com') + await wrapper.find('form').trigger('submit') + }) + + it('normalizes email to lower case letters', () => { + const expected = expect.objectContaining({ variables: { email: 'mail@gmail.com' } }) + expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expected) + }) + }) }) }) From bcf06dff25eec69786a98f5a30aacfc1835c2e21 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 4 Nov 2019 23:09:49 +0100 Subject: [PATCH 07/26] Implement backend lookup with `normalizeEmail` --- backend/package.json | 1 + backend/src/schema/resolvers/passwordReset.js | 29 +---------------- .../schema/resolvers/passwordReset.spec.js | 5 +-- .../passwordReset/createPasswordReset.js | 31 +++++++++++++++++++ .../passwordReset/createPasswordReset.spec.js | 31 +++++++++++++++++++ backend/yarn.lock | 5 +++ 6 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 backend/src/schema/resolvers/passwordReset/createPasswordReset.js create mode 100644 backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js diff --git a/backend/package.json b/backend/package.json index 622a8313d..78dd26bc0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -101,6 +101,7 @@ "slug": "~1.1.0", "trunc-html": "~1.1.2", "uuid": "~3.3.3", + "validator": "^12.0.0", "wait-on": "~3.3.0", "xregexp": "^4.2.4" }, diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index 3c5f4636c..e03378ec1 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -1,34 +1,7 @@ import uuid from 'uuid/v4' import bcrypt from 'bcryptjs' +import createPasswordReset from './passwordReset/createPasswordReset' -export async function createPasswordReset(options) { - const { driver, nonce, email, issuedAt = new Date() } = options - const session = driver.session() - let response = {} - try { - const cypher = ` - MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) - CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL}) - MERGE (u)-[:REQUESTED]->(pr) - RETURN e, pr, u - ` - const transactionRes = await session.run(cypher, { - issuedAt: issuedAt.toISOString(), - nonce, - email, - }) - const records = transactionRes.records.map(record => { - const { email } = record.get('e').properties - const { nonce } = record.get('pr').properties - const { name } = record.get('u').properties - return { email, nonce, name } - }) - response = records[0] || {} - } finally { - session.close() - } - return response -} export default { Mutation: { diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index fabee1c7e..03de77493 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,7 +1,7 @@ import Factory from '../../seed/factories' import { gql } from '../../jest/helpers' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' -import { createPasswordReset } from './passwordReset' +import { createPasswordReset } from './passwordReset/createPasswordReset' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' @@ -109,10 +109,7 @@ describe('passwordReset', () => { describe('resetPassword', () => { const setup = async (options = {}) => { const { email = 'user@example.org', issuedAt = new Date(), nonce = 'abcdef' } = options - - const session = driver.session() await createPasswordReset({ driver, email, issuedAt, nonce }) - session.close() } const mutation = gql` diff --git a/backend/src/schema/resolvers/passwordReset/createPasswordReset.js b/backend/src/schema/resolvers/passwordReset/createPasswordReset.js new file mode 100644 index 000000000..8d575abfc --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset/createPasswordReset.js @@ -0,0 +1,31 @@ +import { normalizeEmail } from 'validator' + +export default async function createPasswordReset(options) { + const { driver, nonce, email, issuedAt = new Date() } = options + const normalizedEmail = normalizeEmail(email) + const session = driver.session() + let response = {} + try { + const cypher = ` + MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email}) + CREATE(pr:PasswordReset {nonce: $nonce, issuedAt: datetime($issuedAt), usedAt: NULL}) + MERGE (u)-[:REQUESTED]->(pr) + RETURN e, pr, u + ` + const transactionRes = await session.run(cypher, { + issuedAt: issuedAt.toISOString(), + nonce, + email: normalizedEmail, + }) + const records = transactionRes.records.map(record => { + const { email } = record.get('e').properties + const { nonce } = record.get('pr').properties + const { name } = record.get('u').properties + return { email, nonce, name } + }) + response = records[0] || {} + } finally { + session.close() + } + return response +} diff --git a/backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js b/backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js new file mode 100644 index 000000000..a5c4d75a5 --- /dev/null +++ b/backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js @@ -0,0 +1,31 @@ +import createPasswordReset from './createPasswordReset' + +describe('createPasswordReset', () => { + const issuedAt = new Date() + const nonce = 'abcdef' + + describe('email lookup', () => { + let driver + let mockSession + beforeEach(() => { + mockSession = { + close() {}, + run: jest.fn().mockReturnValue({ + records: { + map: jest.fn(() => []) + } + }) + } + driver = { session: () => mockSession } + }) + + it('lowercases email address', async () => { + const email = 'stRaNGeCaSiNG@ExAmplE.ORG' + await createPasswordReset({ driver, email, issuedAt, nonce }) + expect(mockSession.run.mock.calls) + .toEqual([[expect.any(String), expect.objectContaining({ + email: 'strangecasing@example.org' + })]]) + }) + }) +}) diff --git a/backend/yarn.lock b/backend/yarn.lock index e6c662229..59254dfc7 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -8418,6 +8418,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-12.0.0.tgz#fb33221f5320abe2422cda2f517dc3838064e813" + integrity sha512-r5zA1cQBEOgYlesRmSEwc9LkbfNLTtji+vWyaHzRZUxCTHdsX3bd+sdHfs5tGZ2W6ILGGsxWxCNwT/h3IY/3ng== + vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 61a89149892c2c225824b789325363e681dd8a18 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 4 Nov 2019 23:41:46 +0100 Subject: [PATCH 08/26] Always normalize email in backend --- backend/src/schema/resolvers/emails.js | 9 ++++++--- .../createPasswordReset.js | 0 .../createPasswordReset.spec.js | 18 +++++++++++------- .../resolvers/helpers/existingEmailAddress.js | 7 +++---- backend/src/schema/resolvers/passwordReset.js | 3 +-- .../src/schema/resolvers/passwordReset.spec.js | 2 +- backend/src/schema/resolvers/registration.js | 13 +++++++------ 7 files changed, 29 insertions(+), 23 deletions(-) rename backend/src/schema/resolvers/{passwordReset => helpers}/createPasswordReset.js (100%) rename backend/src/schema/resolvers/{passwordReset => helpers}/createPasswordReset.spec.js (68%) diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js index ce93a28e9..06c0dbd1a 100644 --- a/backend/src/schema/resolvers/emails.js +++ b/backend/src/schema/resolvers/emails.js @@ -3,11 +3,14 @@ import Resolver from './helpers/Resolver' import existingEmailAddress from './helpers/existingEmailAddress' import { UserInputError } from 'apollo-server' import Validator from 'neode/build/Services/Validator.js' +import { normalizeEmail } from 'validator' export default { Mutation: { AddEmailAddress: async (_parent, args, context, _resolveInfo) => { let response + args.email = normalizeEmail(args.email) + try { const { neode } = context await new Validator(neode, neode.model('UnverifiedEmailAddress'), args) @@ -16,13 +19,13 @@ export default { } // check email does not belong to anybody - await existingEmailAddress(_parent, args, context) + await existingEmailAddress({ args, context }) const nonce = generateNonce() const { user: { id: userId }, } = context - const { email } = args + const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async txc => { const result = await txc.run( @@ -32,7 +35,7 @@ export default { SET email.createdAt = toString(datetime()) RETURN email, user `, - { userId, email, nonce }, + { userId, email: args.email, nonce }, ) return result.records.map(record => ({ name: record.get('user').properties.name, diff --git a/backend/src/schema/resolvers/passwordReset/createPasswordReset.js b/backend/src/schema/resolvers/helpers/createPasswordReset.js similarity index 100% rename from backend/src/schema/resolvers/passwordReset/createPasswordReset.js rename to backend/src/schema/resolvers/helpers/createPasswordReset.js diff --git a/backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js b/backend/src/schema/resolvers/helpers/createPasswordReset.spec.js similarity index 68% rename from backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js rename to backend/src/schema/resolvers/helpers/createPasswordReset.spec.js index a5c4d75a5..a566e225a 100644 --- a/backend/src/schema/resolvers/passwordReset/createPasswordReset.spec.js +++ b/backend/src/schema/resolvers/helpers/createPasswordReset.spec.js @@ -12,9 +12,9 @@ describe('createPasswordReset', () => { close() {}, run: jest.fn().mockReturnValue({ records: { - map: jest.fn(() => []) - } - }) + map: jest.fn(() => []), + }, + }), } driver = { session: () => mockSession } }) @@ -22,10 +22,14 @@ describe('createPasswordReset', () => { it('lowercases email address', async () => { const email = 'stRaNGeCaSiNG@ExAmplE.ORG' await createPasswordReset({ driver, email, issuedAt, nonce }) - expect(mockSession.run.mock.calls) - .toEqual([[expect.any(String), expect.objectContaining({ - email: 'strangecasing@example.org' - })]]) + expect(mockSession.run.mock.calls).toEqual([ + [ + expect.any(String), + expect.objectContaining({ + email: 'strangecasing@example.org', + }), + ], + ]) }) }) }) diff --git a/backend/src/schema/resolvers/helpers/existingEmailAddress.js b/backend/src/schema/resolvers/helpers/existingEmailAddress.js index 007d2de6b..ee1a6af82 100644 --- a/backend/src/schema/resolvers/helpers/existingEmailAddress.js +++ b/backend/src/schema/resolvers/helpers/existingEmailAddress.js @@ -1,7 +1,6 @@ import { UserInputError } from 'apollo-server' -export default async function alreadyExistingMail(_parent, args, context) { - let { email } = args - email = email.toLowerCase() + +export default async function alreadyExistingMail({ args, context }) { const cypher = ` MATCH (email:EmailAddress {email: $email}) OPTIONAL MATCH (email)-[:BELONGS_TO]-(user) @@ -10,7 +9,7 @@ export default async function alreadyExistingMail(_parent, args, context) { let transactionRes const session = context.driver.session() try { - transactionRes = await session.run(cypher, { email }) + transactionRes = await session.run(cypher, { email: args.email }) } finally { session.close() } diff --git a/backend/src/schema/resolvers/passwordReset.js b/backend/src/schema/resolvers/passwordReset.js index e03378ec1..7c0d9e747 100644 --- a/backend/src/schema/resolvers/passwordReset.js +++ b/backend/src/schema/resolvers/passwordReset.js @@ -1,7 +1,6 @@ import uuid from 'uuid/v4' import bcrypt from 'bcryptjs' -import createPasswordReset from './passwordReset/createPasswordReset' - +import createPasswordReset from './helpers/createPasswordReset' export default { Mutation: { diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 03de77493..e9f986acd 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,7 +1,7 @@ import Factory from '../../seed/factories' import { gql } from '../../jest/helpers' import { neode as getNeode, getDriver } from '../../bootstrap/neo4j' -import { createPasswordReset } from './passwordReset/createPasswordReset' +import { createPasswordReset } from './helpers/createPasswordReset' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index bd62b32c3..bdcb6f04f 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -4,6 +4,7 @@ import fileUpload from './fileUpload' import encryptPassword from '../../helpers/encryptPassword' import generateNonce from './helpers/generateNonce' import existingEmailAddress from './helpers/existingEmailAddress' +import { normalizeEmail } from 'validator' const instance = neode() @@ -29,9 +30,9 @@ export default { return response }, Signup: async (_parent, args, context) => { - const nonce = generateNonce() - args.nonce = nonce - let emailAddress = await existingEmailAddress(_parent, args, context) + args.nonce = generateNonce() + args.email = normalizeEmail(args.email) + let emailAddress = await existingEmailAddress({ args, context }) if (emailAddress) return emailAddress try { emailAddress = await instance.create('EmailAddress', args) @@ -42,8 +43,8 @@ export default { }, SignupByInvitation: async (_parent, args, context) => { const { token } = args - const nonce = generateNonce() - args.nonce = nonce + args.nonce = generateNonce() + args.email = normalizeEmail(args.email) let emailAddress = await existingEmailAddress(_parent, args, context) if (emailAddress) return emailAddress try { @@ -78,7 +79,7 @@ export default { args.termsAndConditionsAgreedAt = new Date().toISOString() let { nonce, email } = args - email = email.toLowerCase() + email = normalizeEmail(email) const result = await instance.cypher( ` MATCH(email:EmailAddress {nonce: {nonce}, email: {email}}) From 0cf59743ab57532a9d2e770d5fd93c38d98898a7 Mon Sep 17 00:00:00 2001 From: roschaefer Date: Mon, 4 Nov 2019 23:45:01 +0100 Subject: [PATCH 09/26] refactor: replace 'isemail' with 'validator' ..and use `normalizeEmail` everywhere in the webapp. --- webapp/components/PasswordReset/Request.vue | 12 +- webapp/components/Registration/Signup.vue | 9 +- webapp/package.json | 2 +- webapp/pages/admin/users.vue | 6 +- .../pages/settings/my-email-address/index.vue | 14 +- webapp/yarn.lock | 235 ++---------------- 6 files changed, 47 insertions(+), 231 deletions(-) diff --git a/webapp/components/PasswordReset/Request.vue b/webapp/components/PasswordReset/Request.vue index 6f0176f12..1cf575574 100644 --- a/webapp/components/PasswordReset/Request.vue +++ b/webapp/components/PasswordReset/Request.vue @@ -46,6 +46,7 @@