-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4125 from Ocelot-Social-Community/invite-codes
feat: Invite codes
- Loading branch information
Showing
12 changed files
with
397 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 7 additions & 6 deletions
13
backend/src/models/InvitationCode.js → backend/src/models/InviteCode.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,17 @@ | ||
export default { | ||
code: { type: 'string', primary: true }, | ||
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, | ||
token: { type: 'string', primary: true, token: true }, | ||
generatedBy: { | ||
expiresAt: { type: 'string', isoDate: true, default: null }, | ||
generated: { | ||
type: 'relationship', | ||
relationship: 'GENERATED', | ||
target: 'User', | ||
direction: 'in', | ||
}, | ||
activated: { | ||
redeemed: { | ||
type: 'relationship', | ||
relationship: 'ACTIVATED', | ||
target: 'EmailAddress', | ||
direction: 'out', | ||
relationship: 'REDEEMED', | ||
target: 'User', | ||
direction: 'in', | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export default function generateInviteCode() { | ||
// 6 random numbers in [ 0, 35 ] are 36 possible numbers (10 [0-9] + 26 [A-Z]) | ||
return Array.from({ length: 6 }, (n = Math.floor(Math.random() * 36)) => { | ||
// n > 9: it is a letter (ASCII 65 is A) -> 10 + 55 = 65 | ||
// else: it is a number (ASCII 48 is 0) -> 0 + 48 = 48 | ||
return String.fromCharCode(n > 9 ? n + 55 : n + 48) | ||
}).join('') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import generateInviteCode 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 { | ||
Query: { | ||
MyInviteCodes: async (_parent, args, context, _resolveInfo) => { | ||
const { | ||
user: { id: userId }, | ||
} = context | ||
const session = context.driver.session() | ||
const readTxResultPromise = session.readTransaction(async (txc) => { | ||
const result = await txc.run( | ||
`MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode) | ||
RETURN properties(ic) AS inviteCodes`, | ||
{ | ||
userId, | ||
}, | ||
) | ||
return result.records.map((record) => record.get('inviteCodes')) | ||
}) | ||
try { | ||
const txResult = await readTxResultPromise | ||
return txResult | ||
} finally { | ||
session.close() | ||
} | ||
}, | ||
isValidInviteCode: async (_parent, args, context, _resolveInfo) => { | ||
const { code } = args | ||
if (!code) return false | ||
const session = context.driver.session() | ||
const readTxResultPromise = session.readTransaction(async (txc) => { | ||
const result = await txc.run( | ||
`MATCH (ic:InviteCode { code: toUpper($code) }) | ||
RETURN | ||
CASE | ||
WHEN ic.expiresAt IS NULL THEN true | ||
WHEN datetime(ic.expiresAt) >= datetime() THEN true | ||
ELSE false END AS result`, | ||
{ | ||
code, | ||
}, | ||
) | ||
return result.records.map((record) => record.get('result')) | ||
}) | ||
try { | ||
const txResult = await readTxResultPromise | ||
return !!txResult[0] | ||
} finally { | ||
session.close() | ||
} | ||
}, | ||
}, | ||
Mutation: { | ||
GenerateInviteCode: async (_parent, args, context, _resolveInfo) => { | ||
const { | ||
user: { id: userId }, | ||
} = context | ||
const session = context.driver.session() | ||
let code = generateInviteCode() | ||
while (!(await uniqueInviteCode(session, code))) { | ||
code = generateInviteCode() | ||
} | ||
const writeTxResultPromise = session.writeTransaction(async (txc) => { | ||
const result = await txc.run( | ||
`MATCH (user:User {id: $userId}) | ||
MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code }) | ||
ON CREATE SET | ||
ic.createdAt = toString(datetime()), | ||
ic.expiresAt = $expiresAt | ||
RETURN ic AS inviteCode`, | ||
{ | ||
userId, | ||
code, | ||
expiresAt: args.expiresAt, | ||
}, | ||
) | ||
return result.records.map((record) => record.get('inviteCode').properties) | ||
}) | ||
try { | ||
const txResult = await writeTxResultPromise | ||
return txResult[0] | ||
} finally { | ||
session.close() | ||
} | ||
}, | ||
}, | ||
InviteCode: { | ||
...Resolver('InviteCode', { | ||
idAttribute: 'code', | ||
undefinedToNull: ['expiresAt'], | ||
hasOne: { | ||
generatedBy: '<-[:GENERATED]-(related:User)', | ||
}, | ||
hasMany: { | ||
redeemedBy: '<-[:REDEEMED]-(related:User)', | ||
}, | ||
}), | ||
}, | ||
} |
Oops, something went wrong.