mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1044 from Human-Connection/refactor_email_address
Refactoring: Split User and Email
This commit is contained in:
commit
7de73f861e
@ -8,5 +8,6 @@ module.exports = {
|
|||||||
relationship: 'BELONGS_TO',
|
relationship: 'BELONGS_TO',
|
||||||
target: 'User',
|
target: 'User',
|
||||||
direction: 'out',
|
direction: 'out',
|
||||||
|
eager: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,6 @@ module.exports = {
|
|||||||
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
id: { type: 'string', primary: true, default: uuid }, // TODO: should be type: 'uuid' but simplified for our tests
|
||||||
actorId: { type: 'string', allow: [null] },
|
actorId: { type: 'string', allow: [null] },
|
||||||
name: { type: 'string', min: 3 },
|
name: { type: 'string', min: 3 },
|
||||||
email: { type: 'string', lowercase: true, email: true },
|
|
||||||
slug: 'string',
|
slug: 'string',
|
||||||
encryptedPassword: 'string',
|
encryptedPassword: 'string',
|
||||||
avatar: { type: 'string', allow: [null] },
|
avatar: { type: 'string', allow: [null] },
|
||||||
|
|||||||
@ -5,7 +5,7 @@ export async function createPasswordReset(options) {
|
|||||||
const { driver, code, email, issuedAt = new Date() } = options
|
const { driver, code, email, issuedAt = new Date() } = options
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const cypher = `
|
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})
|
CREATE(pr:PasswordReset {code: $code, issuedAt: datetime($issuedAt), usedAt: NULL})
|
||||||
MERGE (u)-[:REQUESTED]->(pr)
|
MERGE (u)-[:REQUESTED]->(pr)
|
||||||
RETURN u
|
RETURN u
|
||||||
@ -35,7 +35,7 @@ export default {
|
|||||||
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10)
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (pr:PasswordReset {code: $code})
|
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
|
WHERE duration.between(pr.issuedAt, datetime()).days <= 0 AND pr.usedAt IS NULL
|
||||||
SET pr.usedAt = datetime()
|
SET pr.usedAt = datetime()
|
||||||
SET u.encryptedPassword = $encryptedNewPassword
|
SET u.encryptedPassword = $encryptedNewPassword
|
||||||
|
|||||||
@ -12,8 +12,8 @@ const instance = neode()
|
|||||||
*/
|
*/
|
||||||
const checkEmailDoesNotExist = async ({ email }) => {
|
const checkEmailDoesNotExist = async ({ email }) => {
|
||||||
email = email.toLowerCase()
|
email = email.toLowerCase()
|
||||||
const users = await instance.all('User', { email })
|
const emails = await instance.all('EmailAddress', { email })
|
||||||
if (users.length > 0) throw new UserInputError('User account with this email already exists.')
|
if (emails.length > 0) throw new UserInputError('User account with this email already exists.')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@ -166,11 +166,12 @@ describe('SignupByInvitation', () => {
|
|||||||
await expect(action()).rejects.toThrow('"email" must be a valid email')
|
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 {
|
try {
|
||||||
await action()
|
await action()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddresses = await instance.all('EmailAddress')
|
||||||
|
emailAddresses = await emailAddresses.toJson
|
||||||
expect(emailAddresses).toHaveLength(0)
|
expect(emailAddresses).toHaveLength(0)
|
||||||
done()
|
done()
|
||||||
}
|
}
|
||||||
@ -191,16 +192,16 @@ describe('SignupByInvitation', () => {
|
|||||||
describe('creates a EmailAddress node', () => {
|
describe('creates a EmailAddress node', () => {
|
||||||
it('with a `createdAt` attribute', async () => {
|
it('with a `createdAt` attribute', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.createdAt).toBeTruthy()
|
expect(emailAddress.createdAt).toBeTruthy()
|
||||||
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
|
expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('with a cryptographic `nonce`', async () => {
|
it('with a cryptographic `nonce`', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -220,6 +221,7 @@ describe('SignupByInvitation', () => {
|
|||||||
it('rejects because codes can be used only once', async done => {
|
it('rejects because codes can be used only once', async done => {
|
||||||
await action()
|
await action()
|
||||||
try {
|
try {
|
||||||
|
variables.email = 'yetanotheremail@example.org'
|
||||||
await action()
|
await action()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toMatch(/Invitation code already used/)
|
expect(e.message).toMatch(/Invitation code already used/)
|
||||||
@ -282,8 +284,8 @@ describe('Signup', () => {
|
|||||||
|
|
||||||
it('creates a Signup with a cryptographic `nonce`', async () => {
|
it('creates a Signup with a cryptographic `nonce`', async () => {
|
||||||
await action()
|
await action()
|
||||||
const emailAddresses = await instance.all('EmailAddress')
|
let emailAddress = await instance.first('EmailAddress', { email: 'someuser@example.org' })
|
||||||
const emailAddress = await emailAddresses.first().toJson()
|
emailAddress = await emailAddress.toJson()
|
||||||
expect(emailAddress.nonce).toEqual(expect.any(String))
|
expect(emailAddress.nonce).toEqual(expect.any(String))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import encode from '../../jwt/encode'
|
|||||||
import bcrypt from 'bcryptjs'
|
import bcrypt from 'bcryptjs'
|
||||||
import { AuthenticationError } from 'apollo-server'
|
import { AuthenticationError } from 'apollo-server'
|
||||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
|
import { neode } from '../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
@ -21,8 +24,8 @@ export default {
|
|||||||
// }
|
// }
|
||||||
const session = driver.session()
|
const session = driver.session()
|
||||||
const result = await session.run(
|
const result = await session.run(
|
||||||
'MATCH (user:User {email: $userEmail}) ' +
|
'MATCH (user:User)-[:PRIMARY_EMAIL]->(e:EmailAddress {email: $userEmail})' +
|
||||||
'RETURN user {.id, .slug, .name, .avatar, .email, .encryptedPassword, .role, .disabled} as user LIMIT 1',
|
'RETURN user {.id, .slug, .name, .avatar, .encryptedPassword, .role, .disabled, email:e.email} as user LIMIT 1',
|
||||||
{
|
{
|
||||||
userEmail: email,
|
userEmail: email,
|
||||||
},
|
},
|
||||||
@ -46,41 +49,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
changePassword: async (_, { oldPassword, newPassword }, { driver, user }) => {
|
||||||
const session = driver.session()
|
let currentUser = await instance.find('User', user.id)
|
||||||
let result = await session.run(
|
|
||||||
`MATCH (user:User {email: $userEmail})
|
|
||||||
RETURN user {.id, .email, .encryptedPassword}`,
|
|
||||||
{
|
|
||||||
userEmail: user.email,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const [currentUser] = result.records.map(function(record) {
|
const encryptedPassword = currentUser.get('encryptedPassword')
|
||||||
return record.get('user')
|
if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) {
|
||||||
})
|
|
||||||
|
|
||||||
if (!(await bcrypt.compareSync(oldPassword, currentUser.encryptedPassword))) {
|
|
||||||
throw new AuthenticationError('Old password is not correct')
|
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')
|
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())
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -65,6 +65,13 @@ export const hasOne = obj => {
|
|||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
User: async (object, args, context, resolveInfo) => {
|
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)
|
return neo4jgraphql(object, args, context, resolveInfo, false)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -104,6 +111,14 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
User: {
|
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([
|
...undefinedToNull([
|
||||||
'actorId',
|
'actorId',
|
||||||
'avatar',
|
'avatar',
|
||||||
|
|||||||
@ -2,7 +2,7 @@ type User {
|
|||||||
id: ID!
|
id: ID!
|
||||||
actorId: String
|
actorId: String
|
||||||
name: String
|
name: String
|
||||||
email: String!
|
email: String! @cypher(statement: "MATCH (this)-[:PRIMARY_EMAIL]->(e:EmailAddress) RETURN e.email")
|
||||||
slug: String!
|
slug: String!
|
||||||
avatar: String
|
avatar: String
|
||||||
coverImg: String
|
coverImg: String
|
||||||
|
|||||||
@ -21,7 +21,11 @@ export default function create() {
|
|||||||
...args,
|
...args,
|
||||||
}
|
}
|
||||||
args = await encryptPassword(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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,6 +111,13 @@ u.createdAt = user.createdAt.`$date`,
|
|||||||
u.updatedAt = user.updatedAt.`$date`,
|
u.updatedAt = user.updatedAt.`$date`,
|
||||||
u.deleted = user.deletedAt IS NOT NULL,
|
u.deleted = user.deletedAt IS NOT NULL,
|
||||||
u.disabled = false
|
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
|
WITH u, user, user.badgeIds AS badgeIds
|
||||||
UNWIND badgeIds AS badgeId
|
UNWIND badgeIds AS badgeId
|
||||||
MATCH (b:Badge {id: badgeId})
|
MATCH (b:Badge {id: badgeId})
|
||||||
|
|||||||
@ -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 (c:Category) ASSERT c.slug IS UNIQUE;
|
||||||
CREATE CONSTRAINT ON (u:User) ASSERT u.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 (o:Organization) ASSERT o.slug IS UNIQUE;
|
||||||
|
|
||||||
|
CREATE CONSTRAINT ON (e:EmailAddress) ASSERT e.email IS UNIQUE;
|
||||||
' | cypher-shell
|
' | cypher-shell
|
||||||
|
|
||||||
echo '
|
echo '
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user