diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index f4f8c654b..75caa3c0e 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -149,6 +149,7 @@ export default shield( pinPost: isAdmin, unpinPost: isAdmin, UpdateDonations: isAdmin, + CreateInviteCode: isAuthenticated, }, User: { email: or(isMyOwn, isAdmin), diff --git a/backend/src/models/InviteCode.js b/backend/src/models/InviteCode.js new file mode 100644 index 000000000..da9d3bb48 --- /dev/null +++ b/backend/src/models/InviteCode.js @@ -0,0 +1,19 @@ +export default { + code: { type: 'string', primary: true }, + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + uses: { type: 'int', default: () => 0 }, + maxUses: { type: 'int', default: () => 1 }, + active: { type: 'boolean', default: () => true }, + createdBy: { + type: 'relationship', + relationship: 'CREATED', + target: 'User', + direction: 'in', + }, + usedBy: { + type: 'relationship', + relationship: 'USED', + target: 'User', + direction: 'in', + }, +} diff --git a/backend/src/models/User.js b/backend/src/models/User.js index ae7e1ae8c..e9e21e856 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -100,6 +100,18 @@ export default { target: 'User', direction: 'in', }, + createdInvite: { + type: 'relationship', + relationship: 'CREATED', + target: 'InviteCode', + direction: 'out', + }, + usedInvite: { + type: 'relationship', + relationship: 'USED', + target: 'InviteCode', + direction: 'out', + }, termsAndConditionsAgreedVersion: { type: 'string', allow: [null], diff --git a/backend/src/models/index.js b/backend/src/models/index.js index c53ef89ab..8d6a021ab 100644 --- a/backend/src/models/index.js +++ b/backend/src/models/index.js @@ -15,4 +15,5 @@ export default { Donations: require('./Donations.js').default, Report: require('./Report.js').default, Migration: require('./Migration.js').default, + InviteCode: require('./InviteCode.js').default, } diff --git a/backend/src/schema/resolvers/helpers/generateInviteCode.js b/backend/src/schema/resolvers/helpers/generateInviteCode.js new file mode 100644 index 000000000..1d42c2d5d --- /dev/null +++ b/backend/src/schema/resolvers/helpers/generateInviteCode.js @@ -0,0 +1,5 @@ +export default function generateInviteCode() { + return Array.from({length: 6}, (n = Math.floor(Math.random() * 36)) => { + return String.fromCharCode(n > 9 ? n + 55 : n + 48) + }).join('') +} diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js new file mode 100644 index 000000000..450339add --- /dev/null +++ b/backend/src/schema/resolvers/inviteCodes.js @@ -0,0 +1,66 @@ +import { neo4jgraphql } from 'neo4j-graphql-js' +import generateInvieCode from './helpers/generateInviteCode' +import Resolver from './helpers/Resolver' + +const uniqueInviteCode = async (session, code) => { + return session.readTransaction(async (txc) => { + const result = await txc.run( + `MATCH (ic:InviteCode { id: $code }) RETURN count(ic) AS count`, + { code }, + ) + return parseInt(String(result.records[0].get('count'))) === 0 + }) +} + +export default { + Mutation: { + CreateInviteCode: async (_parent, args, context, _resolveInfo) => { + const { + user: { id: userId }, + } = context + const session = context.driver.session() + let code = generateInvieCode() + let response + while(!await uniqueInviteCode(session, code)) { + code = generateInvieCode() + } + const writeTxResultPromise = session.writeTransaction(async (txc) => { + const result = await txc.run( + `MATCH (user:User {id: $userId}) + MERGE (user)-[:CREATED]->(ic:InviteCode { + code: $code, + createdAt: toString(datetime()), + uses: $uses, + maxUses: $maxUses, + active: true + }) RETURN ic AS inviteCode`, + { + userId, + code, + maxUses: args.maxUses, + uses: 0, + }, + ) + return result.records.map((record) => record.get('inviteCode').properties) + }) + try { + const txResult = await writeTxResultPromise + console.log(txResult) + response = txResult[0] + } finally { + session.close() + } + return response + } + }, + InviteCode: { + ...Resolver('InviteCode', { + hasOne: { + createdBy: '<-[:CREATED]-(related:User)', + }, + hasMany: { + usedBy: '<-[:USED]-(related:User)', + }, + }), + }, +} diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index e276968e5..c718187c1 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -293,6 +293,7 @@ export default { avatar: '-[:AVATAR_IMAGE]->(related:Image)', invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', + usedInviteCode: '-[:USED]->(related:InviteCode)', }, hasMany: { followedBy: '<-[:FOLLOWS]-(related:User)', @@ -304,6 +305,7 @@ export default { shouted: '-[:SHOUTED]->(related:Post)', categories: '-[:CATEGORIZED]->(related:Category)', badges: '<-[:REWARDED]-(related:Badge)', + inviteCodes: '-[:CREATED]->(related:InviteCode)', }, }), }, diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql new file mode 100644 index 000000000..b20235620 --- /dev/null +++ b/backend/src/schema/types/type/InviteCode.gql @@ -0,0 +1,14 @@ +type InviteCode { + code: ID! + createdAt: String + uses: Int! + maxUses: Int! + createdBy: User @relation(name: "CREATED", direction: "IN") + usedBy: [User] @relation(name: "USED", direction: "IN") + active: Boolean! +} + + +type Mutation { + CreateInviteCode(maxUses: Int = 1): InviteCode +} diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index e6e7191c5..bbee746b9 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -56,6 +56,9 @@ type User { followedBy: [User]! @relation(name: "FOLLOWS", direction: "IN") followedByCount: Int! @cypher(statement: "MATCH (this)<-[:FOLLOWS]-(r:User) RETURN COUNT(DISTINCT r)") + inviteCodes: [InviteCode] @relation(name: "CREATED", direction: "OUT") + usedInviteCode: InviteCode @relation(name: "USED", direction: "OUT") + # Is the currently logged in user following that user? followedByCurrentUser: Boolean! @cypher( statement: """ @@ -83,7 +86,7 @@ type User { RETURN COUNT(user) >= 1 """ ) - + # contributions: [WrittenPost]! # contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]! # @cypher(