diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index c7123201f..e729123c9 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -1,4 +1,4 @@ -import { rule, shield, deny, allow, and, or, not } from 'graphql-shield' +import { rule, shield, deny, allow, or } from 'graphql-shield' import { neode } from '../bootstrap/neo4j' import CONFIG from '../config' @@ -41,27 +41,6 @@ const isMySocialMedia = rule({ return socialMedia.ownedBy.node.id === user.id }) -const invitationLimitReached = rule({ - cache: 'no_cache', -})(async (parent, args, { user, driver }) => { - const session = driver.session() - try { - const result = await session.run( - ` - MATCH (user:User {id:$id})-[:GENERATED]->(i:InvitationCode) - RETURN COUNT(i) >= 3 as limitReached - `, - { id: user.id }, - ) - const [limitReached] = result.records.map(record => { - return record.get('limitReached') - }) - return limitReached - } finally { - session.close() - } -}) - const isAuthor = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -129,7 +108,6 @@ export default shield( SignupByInvitation: allow, Signup: or(publicRegistration, isAdmin), SignupVerification: allow, - CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)), UpdateUser: onlyYourself, CreatePost: isAuthenticated, UpdatePost: isAuthor, diff --git a/backend/src/models/index.js b/backend/src/models/index.js index bd89ddc51..239076adc 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -3,7 +3,6 @@ export default { Badge: require('./Badge.js'), User: require('./User.js'), - InvitationCode: require('./InvitationCode.js'), EmailAddress: require('./EmailAddress.js'), UnverifiedEmailAddress: require('./UnverifiedEmailAddress.js'), SocialMedia: require('./SocialMedia.js'), diff --git a/backend/src/schema/index.js b/backend/src/schema/index.js index 516f47abd..4252bd817 100644 --- a/backend/src/schema/index.js +++ b/backend/src/schema/index.js @@ -10,7 +10,6 @@ export default makeAugmentedSchema({ exclude: [ 'Badge', 'Embed', - 'InvitationCode', 'EmailAddress', 'Notfication', 'Statistics', diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index d425357c3..4184c9b1d 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -10,25 +10,6 @@ const instance = neode() export default { Mutation: { - CreateInvitationCode: async (_parent, args, context, _resolveInfo) => { - args.token = generateNonce() - const { - user: { id: userId }, - } = context - let response - try { - const [user, invitationCode] = await Promise.all([ - instance.find('User', userId), - instance.create('InvitationCode', args), - ]) - await invitationCode.relateTo(user, 'generatedBy') - response = invitationCode.toJson() - response.generatedBy = user.toJson() - } catch (e) { - throw new UserInputError(e) - } - return response - }, Signup: async (_parent, args, context) => { args.nonce = generateNonce() args.email = normalizeEmail(args.email) @@ -41,35 +22,6 @@ export default { throw new UserInputError(e.message) } }, - SignupByInvitation: async (_parent, args, context) => { - const { token } = args - args.nonce = generateNonce() - args.email = normalizeEmail(args.email) - let emailAddress = await existingEmailAddress({ args, context }) - if (emailAddress) return emailAddress - try { - const result = await instance.cypher( - ` - MATCH (invitationCode:InvitationCode {token:{token}}) - WHERE NOT (invitationCode)-[:ACTIVATED]->() - RETURN invitationCode - `, - { token }, - ) - const validInvitationCode = instance.hydrateFirst( - result, - 'invitationCode', - instance.model('InvitationCode'), - ) - if (!validInvitationCode) - throw new UserInputError('Invitation code already used or does not exist.') - emailAddress = await instance.create('EmailAddress', args) - await validInvitationCode.relateTo(emailAddress, 'activated') - return emailAddress.toJson() - } catch (e) { - throw new UserInputError(e) - } - }, SignupVerification: async (_parent, args) => { const { termsAndConditionsAgreedVersion } = args const regEx = new RegExp(/^[0-9]+\.[0-9]+\.[0-9]+$/g) diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index 7d53b63c9..35b16b9bb 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -9,7 +9,6 @@ const neode = getNeode() let mutate let authenticatedUser -let user let variables const driver = getDriver() @@ -34,243 +33,6 @@ afterEach(async () => { await factory.cleanDatabase() }) -describe('CreateInvitationCode', () => { - const mutation = gql` - mutation { - CreateInvitationCode { - token - } - } - ` - - describe('unauthenticated', () => { - beforeEach(() => { - authenticatedUser = null - }) - - it('throws Authorization error', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], - }) - }) - }) - - describe('authenticated', () => { - beforeEach(async () => { - user = await factory.create('User', { - id: 'i123', - name: 'Inviter', - email: 'inviter@example.org', - password: '1234', - termsAndConditionsAgreedVersion: null, - }) - authenticatedUser = await user.toJson() - }) - - it('resolves', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - data: { CreateInvitationCode: { token: expect.any(String) } }, - }) - }) - - it('creates an InvitationCode with a `createdAt` attribute', async () => { - await mutate({ mutation }) - const codes = await neode.all('InvitationCode') - const invitation = await codes.first().toJson() - expect(invitation.createdAt).toBeTruthy() - expect(Date.parse(invitation.createdAt)).toEqual(expect.any(Number)) - }) - - it('relates inviting User to InvitationCode', async () => { - await mutate({ mutation }) - const result = await neode.cypher( - 'MATCH(code:InvitationCode)<-[:GENERATED]-(user:User) RETURN user', - ) - const inviter = neode.hydrateFirst(result, 'user', neode.model('User')) - await expect(inviter.toJson()).resolves.toEqual(expect.objectContaining({ name: 'Inviter' })) - }) - - describe('who has invited a lot of users already', () => { - beforeEach(async () => { - await Promise.all([mutate({ mutation }), mutate({ mutation }), mutate({ mutation })]) - }) - - describe('as ordinary `user`', () => { - it('throws `Not Authorised` because of maximum number of invitations', async () => { - await expect(mutate({ mutation })).resolves.toMatchObject({ - errors: [{ message: 'Not Authorised!' }], - }) - }) - - it('creates no additional invitation codes', async () => { - await mutate({ mutation }) - const invitationCodes = await neode.all('InvitationCode') - await expect(invitationCodes.toJson()).resolves.toHaveLength(3) - }) - }) - - describe('as a strong donator', () => { - beforeEach(() => { - // What is the setup? - }) - - it.todo('can invite more people') - // it('can invite more people', async () => { - // await action() - // const invitationQuery = `{ User { createdAt } }` - // const { User: users } = await client.request(invitationQuery ) - // expect(users).toHaveLength(3 + 1 + 1) - // }) - }) - }) - }) -}) - -describe('SignupByInvitation', () => { - const mutation = gql` - mutation($email: String!, $token: String!) { - SignupByInvitation(email: $email, token: $token) { - email - } - } - ` - - describe('with valid email but invalid InvitationCode', () => { - beforeEach(() => { - variables = { - ...variables, - email: 'any-email@example.org', - token: 'wut?', - } - }) - - it('throws UserInputError', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'UserInputError: Invitation code already used or does not exist.' }], - }) - }) - - describe('with valid InvitationCode', () => { - beforeEach(async () => { - const inviter = await factory.create('User', { - name: 'Inviter', - email: 'inviter@example.org', - password: '1234', - }) - authenticatedUser = await inviter.toJson() - const invitationMutation = gql` - mutation { - CreateInvitationCode { - token - } - } - ` - const { - data: { - CreateInvitationCode: { token }, - }, - } = await mutate({ mutation: invitationMutation }) - authenticatedUser = null - variables = { - ...variables, - token, - } - }) - - describe('given an invalid email', () => { - beforeEach(() => { - variables = { ...variables, email: 'someuser' } - }) - - it('throws `email is not a valid email`', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: expect.stringContaining('"email" must be a valid email') }], - }) - }) - - it('creates no additional EmailAddress node', async () => { - let emailAddresses = await neode.all('EmailAddress') - emailAddresses = await emailAddresses.toJson() - expect(emailAddresses).toHaveLength(1) - await mutate({ mutation, variables }) - emailAddresses = await neode.all('EmailAddress') - emailAddresses = await emailAddresses.toJson() - expect(emailAddresses).toHaveLength(1) - }) - }) - - describe('given a valid email', () => { - beforeEach(() => { - variables = { ...variables, email: 'someUser@example.org' } - }) - - it('resolves', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - data: { SignupByInvitation: { email: 'someuser@example.org' } }, - }) - }) - - describe('creates a EmailAddress node', () => { - it('with a `createdAt` attribute', async () => { - await mutate({ mutation, variables }) - let emailAddress = await neode.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 mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) - }) - - it('connects inviter through invitation code', async () => { - await mutate({ mutation, variables }) - const result = await neode.cypher( - 'MATCH(inviter:User)-[:GENERATED]->(:InvitationCode)-[:ACTIVATED]->(email:EmailAddress {email: {email}}) RETURN inviter', - { email: 'someuser@example.org' }, - ) - const inviter = neode.hydrateFirst(result, 'inviter', neode.model('User')) - await expect(inviter.toJson()).resolves.toEqual( - expect.objectContaining({ name: 'Inviter' }), - ) - }) - - describe('using the same InvitationCode twice', () => { - it('rejects because codes can be used only once', async () => { - await mutate({ mutation, variables }) - variables = { ...variables, email: 'yetanotheremail@example.org' } - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [ - { message: 'UserInputError: Invitation code already used or does not exist.' }, - ], - }) - }) - }) - - describe('if a user account with the given email already exists', () => { - beforeEach(async () => { - await factory.create('User', { email: 'someuser@example.org' }) - }) - - it('throws unique violation error', async () => { - await expect(mutate({ mutation, variables })).resolves.toMatchObject({ - errors: [{ message: 'A user account with this email already exists.' }], - }) - }) - }) - - describe('if the EmailAddress already exists but without user account', () => { - it.todo('shall we re-send the registration email?') - }) - }) - }) - }) - }) -}) - describe('Signup', () => { const mutation = gql` mutation($email: String!) { diff --git a/backend/src/schema/types/type/InvitationCode.gql b/backend/src/schema/types/type/InvitationCode.gql deleted file mode 100644 index 61ce0f689..000000000 --- a/backend/src/schema/types/type/InvitationCode.gql +++ /dev/null @@ -1,10 +0,0 @@ -type InvitationCode { - id: ID! - token: String - generatedBy: User @relation(name: "GENERATED", direction: "IN") - createdAt: String -} - -type Mutation { - CreateInvitationCode: InvitationCode -}