mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Use EmailAddressRequest and validate email
This commit is contained in:
parent
707cf741de
commit
e116d52992
12
backend/src/models/EmailAddressRequest.js
Normal file
12
backend/src/models/EmailAddressRequest.js
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
module.exports = {
|
||||||
|
email: { type: 'string', primary: true, lowercase: true, email: true },
|
||||||
|
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() },
|
||||||
|
nonce: { type: 'string', token: true },
|
||||||
|
belongsTo: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'BELONGS_TO',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'out',
|
||||||
|
eager: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ export default {
|
|||||||
User: require('./User.js'),
|
User: require('./User.js'),
|
||||||
InvitationCode: require('./InvitationCode.js'),
|
InvitationCode: require('./InvitationCode.js'),
|
||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
|
EmailAddressRequest: require('./EmailAddressRequest.js'),
|
||||||
SocialMedia: require('./SocialMedia.js'),
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
Post: require('./Post.js'),
|
Post: require('./Post.js'),
|
||||||
Comment: require('./Comment.js'),
|
Comment: require('./Comment.js'),
|
||||||
|
|||||||
@ -2,10 +2,18 @@ import generateNonce from './helpers/generateNonce'
|
|||||||
import Resolver from './helpers/Resolver'
|
import Resolver from './helpers/Resolver'
|
||||||
import existingEmailAddress from './helpers/existingEmailAddress'
|
import existingEmailAddress from './helpers/existingEmailAddress'
|
||||||
import { UserInputError } from 'apollo-server'
|
import { UserInputError } from 'apollo-server'
|
||||||
|
import Validator from 'neode/build/Services/Validator.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
|
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
|
||||||
|
try {
|
||||||
|
const { neode } = context
|
||||||
|
await new Validator(neode, neode.model('EmailAddressRequest'), args)
|
||||||
|
} catch (e) {
|
||||||
|
throw new UserInputError('must be a valid email')
|
||||||
|
}
|
||||||
|
|
||||||
let response = await existingEmailAddress(_parent, args, context)
|
let response = await existingEmailAddress(_parent, args, context)
|
||||||
if (response) return response
|
if (response) return response
|
||||||
|
|
||||||
@ -19,7 +27,7 @@ export default {
|
|||||||
const result = await txc.run(
|
const result = await txc.run(
|
||||||
`
|
`
|
||||||
MATCH (user:User {id: $userId})
|
MATCH (user:User {id: $userId})
|
||||||
MERGE (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce})
|
MERGE (user)<-[:BELONGS_TO]-(email:EmailAddressRequest {email: $email, nonce: $nonce})
|
||||||
SET email.createdAt = toString(datetime())
|
SET email.createdAt = toString(datetime())
|
||||||
RETURN email
|
RETURN email
|
||||||
`,
|
`,
|
||||||
@ -46,9 +54,11 @@ export default {
|
|||||||
const result = await txc.run(
|
const result = await txc.run(
|
||||||
`
|
`
|
||||||
MATCH (user:User {id: $userId})-[previous:PRIMARY_EMAIL]->(:EmailAddress)
|
MATCH (user:User {id: $userId})-[previous:PRIMARY_EMAIL]->(:EmailAddress)
|
||||||
MATCH (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce})
|
MATCH (user)<-[:BELONGS_TO]-(email:EmailAddressRequest {email: $email, nonce: $nonce})
|
||||||
MERGE (user)-[:PRIMARY_EMAIL]->(email)
|
MERGE (user)-[:PRIMARY_EMAIL]->(email)
|
||||||
|
SET email:EmailAddress
|
||||||
SET email.verifiedAt = toString(datetime())
|
SET email.verifiedAt = toString(datetime())
|
||||||
|
REMOVE email:EmailAddressRequest
|
||||||
DELETE previous
|
DELETE previous
|
||||||
RETURN email
|
RETURN email
|
||||||
`,
|
`,
|
||||||
@ -59,6 +69,10 @@ export default {
|
|||||||
try {
|
try {
|
||||||
const txResult = await writeTxResultPromise
|
const txResult = await writeTxResultPromise
|
||||||
response = txResult[0]
|
response = txResult[0]
|
||||||
|
} catch (e) {
|
||||||
|
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
|
||||||
|
throw new UserInputError('A user account with this email already exists.')
|
||||||
|
throw new Error(e)
|
||||||
} finally {
|
} finally {
|
||||||
session.close()
|
session.close()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,16 @@ describe('AddEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('email attribute is not a valid email', () => {
|
describe('email attribute is not a valid email', () => {
|
||||||
it.todo('throws UserInputError')
|
beforeEach(() => {
|
||||||
|
variables = { ...variables, email: 'foobar' }
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws UserInputError', async () => {
|
||||||
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
|
data: { AddEmailAddress: null },
|
||||||
|
errors: [{ message: 'must be a valid email' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('email attribute is a valid email', () => {
|
describe('email attribute is a valid email', () => {
|
||||||
@ -85,24 +94,23 @@ describe('AddEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('connects `EmailAddress` to the authenticated user', async () => {
|
it('connects `EmailAddressRequest` to the authenticated user', async () => {
|
||||||
await mutate({ mutation, variables })
|
await mutate({ mutation, variables })
|
||||||
const result = await neode.cypher(`
|
const result = await neode.cypher(`
|
||||||
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"})
|
MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"})
|
||||||
MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddress {email: "new-email@example.org"})
|
MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddressRequest {email: "new-email@example.org"})
|
||||||
RETURN e
|
RETURN e
|
||||||
`)
|
`)
|
||||||
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
|
const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddressRequest'))
|
||||||
await expect(email.toJson()).resolves.toMatchObject({
|
await expect(email.toJson()).resolves.toMatchObject({
|
||||||
email: 'new-email@example.org',
|
email: 'new-email@example.org',
|
||||||
nonce: expect.any(String),
|
nonce: expect.any(String),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('if a lone `EmailAddress` node already exists with that email', () => {
|
describe('if another `EmailAddressRequest` node already exists with that email', () => {
|
||||||
it('returns this `EmailAddress` node', async () => {
|
it('throws no unique constraint violation error', async () => {
|
||||||
await factory.create('EmailAddress', {
|
await factory.create('EmailAddressRequest', {
|
||||||
verifiedAt: null,
|
|
||||||
createdAt: '2019-09-24T14:00:01.565Z',
|
createdAt: '2019-09-24T14:00:01.565Z',
|
||||||
email: 'new-email@example.org',
|
email: 'new-email@example.org',
|
||||||
})
|
})
|
||||||
@ -111,7 +119,6 @@ describe('AddEmailAddress', () => {
|
|||||||
AddEmailAddress: {
|
AddEmailAddress: {
|
||||||
email: 'new-email@example.org',
|
email: 'new-email@example.org',
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
createdAt: '2019-09-24T14:00:01.565Z', // this is to make sure it's the one above
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
errors: undefined,
|
errors: undefined,
|
||||||
@ -175,10 +182,10 @@ describe('VerifyEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given an unverified `EmailAddress`', () => {
|
describe('given a `EmailAddressRequest`', () => {
|
||||||
let emailAddress
|
let emailAddress
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
emailAddress = await factory.create('EmailAddress', {
|
emailAddress = await factory.create('EmailAddressRequest', {
|
||||||
nonce: 'abcdef',
|
nonce: 'abcdef',
|
||||||
verifiedAt: null,
|
verifiedAt: null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@ -196,7 +203,7 @@ describe('VerifyEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given valid nonce for unverified `EmailAddress` node', () => {
|
describe('given valid nonce for `EmailAddressRequest` node', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
variables = { ...variables, nonce: 'abcdef' }
|
variables = { ...variables, nonce: 'abcdef' }
|
||||||
})
|
})
|
||||||
@ -210,7 +217,7 @@ describe('VerifyEmailAddress', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('and the `EmailAddress` belongs to the authenticated user', () => {
|
describe('and the `EmailAddressRequest` belongs to the authenticated user', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await emailAddress.relateTo(user, 'belongsTo')
|
await emailAddress.relateTo(user, 'belongsTo')
|
||||||
})
|
})
|
||||||
@ -256,6 +263,19 @@ describe('VerifyEmailAddress', () => {
|
|||||||
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
|
email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress'))
|
||||||
await expect(email).toBe(false)
|
await expect(email).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Edge case: In the meantime someone created an `EmailAddress` node with the given email', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await factory.create('EmailAddress', { email: 'to-be-verified@example.org' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws UserInputError because of unique constraints', async () => {
|
||||||
|
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
|
||||||
|
data: { VerifyEmailAddress: null },
|
||||||
|
errors: [{ message: 'A user account with this email already exists.' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
10
backend/src/seed/factories/emailAddressRequests.js
Normal file
10
backend/src/seed/factories/emailAddressRequests.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { defaults } from './emailAddresses.js'
|
||||||
|
|
||||||
|
export default function create() {
|
||||||
|
return {
|
||||||
|
factory: async ({ args, neodeInstance }) => {
|
||||||
|
args = defaults({ args })
|
||||||
|
return neodeInstance.create('EmailAddressRequest', args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,16 +1,21 @@
|
|||||||
import faker from 'faker'
|
import faker from 'faker'
|
||||||
|
|
||||||
|
export function defaults({ args }) {
|
||||||
|
const defaults = {
|
||||||
|
email: faker.internet.email(),
|
||||||
|
verifiedAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
args = {
|
||||||
|
...defaults,
|
||||||
|
...args,
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
export default function create() {
|
export default function create() {
|
||||||
return {
|
return {
|
||||||
factory: async ({ args, neodeInstance }) => {
|
factory: async ({ args, neodeInstance }) => {
|
||||||
const defaults = {
|
args = defaults({ args })
|
||||||
email: faker.internet.email(),
|
|
||||||
verifiedAt: new Date().toISOString(),
|
|
||||||
}
|
|
||||||
args = {
|
|
||||||
...defaults,
|
|
||||||
...args,
|
|
||||||
}
|
|
||||||
return neodeInstance.create('EmailAddress', args)
|
return neodeInstance.create('EmailAddress', args)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import createTag from './tags.js'
|
|||||||
import createSocialMedia from './socialMedia.js'
|
import createSocialMedia from './socialMedia.js'
|
||||||
import createLocation from './locations.js'
|
import createLocation from './locations.js'
|
||||||
import createEmailAddress from './emailAddresses.js'
|
import createEmailAddress from './emailAddresses.js'
|
||||||
|
import createEmailAddressRequests from './emailAddressRequests.js'
|
||||||
|
|
||||||
export const seedServerHost = 'http://127.0.0.1:4001'
|
export const seedServerHost = 'http://127.0.0.1:4001'
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ const factories = {
|
|||||||
SocialMedia: createSocialMedia,
|
SocialMedia: createSocialMedia,
|
||||||
Location: createLocation,
|
Location: createLocation,
|
||||||
EmailAddress: createEmailAddress,
|
EmailAddress: createEmailAddress,
|
||||||
|
EmailAddressRequest: createEmailAddressRequests,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const cleanDatabase = async (options = {}) => {
|
export const cleanDatabase = async (options = {}) => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user