diff --git a/backend/src/helpers/generateNonce.js b/backend/src/helpers/generateNonce.js new file mode 100644 index 000000000..4dde1df04 --- /dev/null +++ b/backend/src/helpers/generateNonce.js @@ -0,0 +1,4 @@ +import uuid from 'uuid/v4' +export default function generateNonce() { + return uuid().substring(0, 6) +} diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index ce98090ad..59ae06c07 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -170,6 +170,8 @@ const permissions = shield( block: isAuthenticated, unblock: isAuthenticated, markAsRead: isAuthenticated, + AddEmailAddress: isAuthenticated, + VerifyEmailAddress: isAuthenticated, }, User: { email: isMyOwn, diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js new file mode 100644 index 000000000..281c7d8ce --- /dev/null +++ b/backend/src/schema/resolvers/emails.js @@ -0,0 +1,41 @@ +import generateNonce from '../../helpers/generateNonce' +import Resolver from './helpers/Resolver' + +export default { + Mutation: { + AddEmailAddress: async (_parent, args, context, _resolveInfo) => { + let response + 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( + ` + MATCH (user:User {id: $userId}) + MERGE (user)<-[:BELONGS_TO]-(email:EmailAddress {email: $email, nonce: $nonce}) + SET email.createdAt = toString(datetime()) + RETURN email + `, + { userId, email, nonce }, + ) + return result.records.map(record => record.get('email').properties) + }) + try { + const txResult = await writeTxResultPromise + response = txResult[0] + } finally { + session.close() + } + return response + }, + VerifyEmailAddress: async (_parent, args, context, _resolveInfo) => {}, + }, + EmailAddress: { + ...Resolver('EmailAddress', { + undefinedToNull: ['verifiedAt'], + }), + }, +} diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index dfd6c8752..ec9e831e3 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -37,7 +37,7 @@ afterEach(async () => { describe('AddEmailAddress', () => { const mutation = gql` mutation($email: String!) { - AddEmailAddress(email: $email){ + AddEmailAddress(email: $email) { email verifiedAt createdAt @@ -62,14 +62,43 @@ describe('AddEmailAddress', () => { }) describe('authenticated', () => { - it.todo('creates a new unverified `EmailAddress` node') - it.todo('connects EmailAddress to the authenticated user') - - describe('even if an unverified `EmailAddress` already exists with that email', () =>{ - it.todo('creates a new unverified `EmailAddress` node') + beforeEach(async () => { + user = await factory.create('User', { email: 'user@example.org' }) + authenticatedUser = await user.toJson() }) - describe('but if a verified `EmailAddress` already exists with that email', () =>{ + it('creates a new unverified `EmailAddress` node', async () => { + await expect(mutate({ mutation, variables })).resolves.toMatchObject({ + data: { + AddEmailAddress: { + email: 'new-email@example.org', + verifiedAt: null, + createdAt: expect.any(String), + }, + }, + errors: undefined, + }) + }) + + it('connects `EmailAddress` to the authenticated user', async () => { + await mutate({ mutation, variables }) + const result = await neode.cypher(` + MATCH(u:User)-[:PRIMARY_EMAIL]->(p:EmailAddress {email: "user@example.org"}) + MATCH(u:User)<-[:BELONGS_TO]-(e:EmailAddress {email: "new-email@example.org"}) + RETURN e + `) + const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) + await expect(email.toJson()).resolves.toMatchObject({ + email: 'new-email@example.org', + nonce: expect.any(String), + }) + }) + + describe('if a lone `EmailAddress` node already exists with that email', () => { + it.todo('returns this `EmailAddress` node') + }) + + describe('but if another user owns an `EmailAddress` already with that email', () => { it.todo('throws UserInputError because of unique constraints') }) }) @@ -78,7 +107,7 @@ describe('AddEmailAddress', () => { describe('VerifyEmailAddress', () => { const mutation = gql` mutation($email: String!, $nonce: String!) { - VerifyEmailAddress(email: $email, nonce: $nonce){ + VerifyEmailAddress(email: $email, nonce: $nonce) { email createdAt } @@ -117,7 +146,7 @@ describe('VerifyEmailAddress', () => { }) describe('and the `EmailAddress` belongs to the authenticated user', () => { - it.todo('verifies the `EmailAddress`') + it.todo('adds `verifiedAt`') it.todo('connects the new `EmailAddress` as PRIMARY') it.todo('removes previous PRIMARY relationship') })