Merge pull request #1044 from Human-Connection/refactor_email_address

Refactoring: Split User and Email
This commit is contained in:
Robert Schäfer 2019-07-16 21:28:42 +02:00 committed by GitHub
commit 7de73f861e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 62 additions and 46 deletions

View File

@ -8,5 +8,6 @@ module.exports = {
relationship: 'BELONGS_TO',
target: 'User',
direction: 'out',
eager: true,
},
}

View File

@ -4,7 +4,6 @@ module.exports = {
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
actorId: { type: 'string', allow: [null] },
name: { type: 'string', min: 3 },
email: { type: 'string', lowercase: true, email: true },
slug: 'string',
encryptedPassword: 'string',
avatar: { type: 'string', allow: [null] },

View File

@ -5,7 +5,7 @@ export async function createPasswordReset(options) {
const { driver, code, email, issuedAt = new Date() } = options
const session = driver.session()
const cypher = `
MATCH (u:User) WHERE u.email = $email
MATCH (u:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email:$email})
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
MERGE (u)-[:REQUESTED]->(pr)
RETURN u
@ -35,7 +35,7 @@ export default {
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
const cypher = `
MATCH (pr:PasswordReset {code: $code})
MATCH (u:User {email: $email})-[:REQUESTED]->(pr)
MATCH (e:EmailAddress {email: $email})<-[:PRIMARY_EMAIL]-(u:User)-[:REQUESTED]->(pr)
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
SET pr.usedAt = datetime()
SET u.encryptedPassword = $encryptedNewPassword

View File

@ -12,8 +12,8 @@ const instance = neode()
*/
const checkEmailDoesNotExist = async ({ email }) => {
email = email.toLowerCase()
const users = await instance.all('User', { email })
if (users.length > 0) throw new UserInputError('User account with this email already exists.')
const emails = await instance.all('EmailAddress', { email })
if (emails.length > 0) throw new UserInputError('User account with this email already exists.')
}
export default {

View File

@ -166,11 +166,12 @@ describe('SignupByInvitation', () => {
await expect(action()).rejects.toThrow('"email" must be a valid email')
})
it('creates no EmailAddress node', async done => {
it('creates no additional EmailAddress node', async done => {
try {
await action()
} catch (e) {
const emailAddresses = await instance.all('EmailAddress')
let emailAddresses = await instance.all('EmailAddress')
emailAddresses = await emailAddresses.toJson
expect(emailAddresses).toHaveLength(0)
done()
}
@ -191,16 +192,16 @@ describe('SignupByInvitation', () => {
describe('creates a EmailAddress node', () => {
it('with a `createdAt` attribute', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
emailAddress = await emailAddress.toJson()
expect(emailAddress.createdAt).toBeTruthy()
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
})
it('with a cryptographic `nonce`', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
emailAddress = await emailAddress.toJson()
expect(emailAddress.nonce).toEqual(expect.any(String))
})
@ -220,6 +221,7 @@ describe('SignupByInvitation', () => {
it('rejects because codes can be used only once', async done => {
await action()
try {
variables.email = 'yetanotheremail@example.org'
await action()
} catch (e) {
expect(e.message).toMatch(/Invitation code already used/)
@ -282,8 +284,8 @@ describe('Signup', () => {
it('creates a Signup with a cryptographic `nonce`', async () => {
await action()
const emailAddresses = await instance.all('EmailAddress')
const emailAddress = await emailAddresses.first().toJson()
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
emailAddress = await emailAddress.toJson()
expect(emailAddress.nonce).toEqual(expect.any(String))
})
})

View File

@ -2,6 +2,9 @@ import encode from '../../jwt/encode'
import bcrypt from 'bcryptjs'
import { AuthenticationError } from 'apollo-server'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { neode } from '../../bootstrap/neo4j'
const instance = neode()
export default {
Query: {
@ -21,8 +24,8 @@ export default {
// }
const session = driver.session()
const result = await session.run(
'MATCH (user:User {email: $userEmail}) ' +
'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1',
'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' +
'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1',
{
userEmail: email,
},
@ -46,41 +49,24 @@ export default {
}
},
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
const session = driver.session()
let result = await session.run(
`MATCH (user:User {email: $userEmail})
RETURN user {.id, .email, .encryptedPassword}`,
{
userEmail: user.email,
},
)
let currentUser = await instance.find('User', user.id)
const [currentUser] = result.records.map(function(record) {
return record.get('user')
})
if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) {
const encryptedPassword = currentUser.get('encryptedPassword')
if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) {
throw new AuthenticationError('Old password is not correct')
}
if (await bcrypt.compareSync(newPassword, currentUser.encryptedPassword)) {
if (await bcrypt.compareSync(newPassword, encryptedPassword)) {
throw new AuthenticationError('Old password and new password should be different')
} else {
const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
session.run(
`MATCH (user:User {email: $userEmail})
SET user.encryptedPassword = $newEncryptedPassword
RETURN user
`,
{
userEmail: user.email,
newEncryptedPassword,
},
)
session.close()
return encode(currentUser)
}
const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10)
await currentUser.update({
encryptedPassword: newEncryptedPassword,
updatedAt: new Date().toISOString(),
})
return encode(await currentUser.toJson())
},
},
}

View File

@ -65,6 +65,13 @@ export const hasOne = obj => {
export default {
Query: {
User: async (object, args, context, resolveInfo) => {
const { email } = args
if (email) {
const e = await instance.first('EmailAddress', { email })
let user = e.get('belongsTo')
user = await user.toJson()
return [user.node]
}
return neo4jgraphql(object, args, context, resolveInfo, false)
},
},
@ -104,6 +111,14 @@ export default {
},
},
User: {
email: async (parent, params, context, resolveInfo) => {
if (typeof parent.email !== 'undefined') return parent.email
const { id } = parent
const statement = `MATCH(u:User {id: {id}})-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e`
const result = await instance.cypher(statement, { id })
let [{ email }] = result.records.map(r => r.get('e').properties)
return email
},
...undefinedToNull([
'actorId',
'avatar',

View File

@ -2,7 +2,7 @@ type User {
id: ID!
actorId: String
name: String
email: String!
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
slug: String!
avatar: String
coverImg: String

View File

@ -21,7 +21,11 @@ export default function create() {
...args,
}
args = await encryptPassword(args)
return neodeInstance.create('User', args)
const user = await neodeInstance.create('User', args)
const email = await neodeInstance.create('EmailAddress', { email: args.email })
await user.relateTo(email, 'primaryEmail')
await email.relateTo(user, 'belongsTo')
return user
},
}
}

View File

@ -111,6 +111,13 @@ u.createdAt = user.createdAt.`$date`,
u.updatedAt = user.updatedAt.`$date`,
u.deleted = user.deletedAt IS NOT NULL,
u.disabled = false
MERGE (e:EmailAddress {
email: user.email,
createdAt: toString(datetime()),
verifiedAt: toString(datetime())
})
MERGE (e)-[:BELONGS_TO]->(u)
MERGE (u)<-[:PRIMARY_EMAIL]-(e)
WITH u, user, user.badgeIds AS badgeIds
UNWIND badgeIds AS badgeId
MATCH (b:Badge {id: badgeId})

View File

@ -34,6 +34,8 @@ CREATE CONSTRAINT ON (p:Post) ASSERT p.slug IS UNIQUE;
CREATE CONSTRAINT ON (c:Category) ASSERT c.slug IS UNIQUE;
CREATE CONSTRAINT ON (u:User) ASSERT u.slug IS UNIQUE;
CREATE CONSTRAINT ON (o:Organization) ASSERT o.slug IS UNIQUE;
CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE;
' | cypher-shell
echo '