new invite system
See: https://discordapp.com/channels/423137607682228234/423139257591398400/666360448446234649
Let's improve invites!
Current system
This is how the system currently works:
- User A wants to register for elixi.re. They make an account on the website.
- The registration webhook posts to
#user-registers
.- chef recognizes the webhook message and creates a job.
- User B, User A's friend, already has an account. They tell us that they want their friend to join, and they give us User A's Discord account ID.
- We run
d?gk allow <user a's id>
, allowing them to join. - User A joins the server.
- Some time passes. Theoretically, User A starts talking in the server, but in practice this doesn't happen. (We expect this so we can "judge" them if they should have an account or not.)
- User A is either approved or denied:
- If denied, the elixi.re account is deleted. The Discord user stays in the server. (We probably don't want this.)
- If approved, the elixi.re account is enabled and User A gets an email.
Problems
There's some problems with this:
- Everyone is allowed to join, even though ideally Discord invites should be gated off to potential users of the service.
- Having users grab their friend's ID and give it to us is annoying.
- Us having to manually whitelist potential users is annoying.
- Anyone can register, even if they don't have some to refer them.
- Using a webhook to communicate data is really dirty and hacky.
Proposed system
Here's how I propose we fix this:
- Firstly, "give" everyone 3 invites or so. (Configurable through
MAX_INVITES
).- In this context, an "invite" is an elixi.re invite, not a Discord invite.
- To prevent abuse, we can "stagger" the provisioning of new invites. For example, once a new user is approved, they'll start off with 0 invites. After 2 weeks, they'll get their 1st invite, then their 2nd invite after one week. Then after one more week, they'll get their 3rd invite.
- In reality, we don't need to implement a stateful "number of invites left"
quantity. We can just query how many invites that user has, then subtract
it from
MAX_INVITES
.
- User C wants to have their friend, User D, join the service.
- User C logs onto the website and uses one of their invites, generating an
invite link.
- The link should look something like
elixi.re/invite/aabbccddee
.
- The link should look something like
- User C gives the invite link to their friend, User D.
- User D opens the link, bringing them to the registration page.
- The frontend gives the backend the invite code.
- The backend validates this code when registering.
- User D registers as normal.
- After they hit submit, the chef creates a unique Discord invite for this user and sends it to them in the email. They can now join the server.
- chef creates the user registration job when the user joins.
- Approvals and denials work as expected.
Because of these changes:
-
/api/v3/auth/register
will now require a valid invite code to use. - The register form will no longer be publicly visible on the website.
-
@everyone
should no longer have the permission to create invites.
In this system, chef now handles everything related to Discord. However, we need to figure out a real IPC system so that chef can generate invites upon registration, without using a webhook. This is actually a requirement because chef needs to give the invite URL back to the backend, which makes it two-way.
In the implementation, this feature should be entirely optional. Forcing dependence on Discord is undesirable.
Routes
GET /api/v3/invites
Responds with HTTP 200
and a JSON array of all invites (used or not) under the
user's possession.
Example response:
{
// The number of invites the user has. Should be within 0 to `MAX_INVITES`.
"invites_remaining": 2,
"invites": [
{
// The snowflake of the invite.
"id": "...",
// The invite's code. This will be `POST`ed to `/api/v3/auth/register`.
"code": "sj6OakMx",
// Just for convenience, the URL representation of the invite.
// The backend should generate this based on the main domain.
"url": "https://elixi.re/invite/sj6OakMx",
// Was the invite used?
"used": false,
// The user that used this invite. If unused, this should be `null`.
// Only the username is necessary; let's not expose too much information.
"used_by": { "name": "..." }
}
]
}
POST /api/v3/invites/generate
Generates an invite. This consumes one of the invites that a user has.
If the user already has MAX_INVITES
invites (used or not), respond with HTTP 403
and an appropriate error message. This can be checked by querying the
database for all of the invites that the user has ever generated.
An example response would be some JSON of an invite object (as outlined in
/api/v3/invites
).
POST /api/v3/auth/register
Modify this route so that invite_code
is required to register. For validation,
query the database to make sure that the invite exists and is unused.
Schema
CREATE TABLE invites (
id BIGINT PRIMARY KEY,
code TEXT UNIQUE NOT NULL,
used_by BIGINT REFERENCES users (user_id) DEFAULT NULL
);
This table will be touched:
- When fetching invites.
- When checking if a user already has
MAX_INVITES
invites.
- When checking if a user already has
- When creating invites.
None of this is final, please discuss and give feedback.