00:05
For our organization invitation system, let's first add the ability to actually invite a user into our organization. We'll do that via our invite user method off of our organization's controller.
00:15
Before we start with that invite method though, let's first jump down to our validators and into our organization validator as we're going to need a validation for this.
00:24
So export const organization invite validator equals vine and we'll want with our organization metadata here.
00:34
So first within our validator, we're going to have our vine object. We're going to want the ability to invite a user regardless of whether or not they're currently a user inside of our application.
00:43
So the best way to do that is by taking in an e-mail field. For this, we'll go ahead and set this up to use the same e-mail rules that we have inside of
00:51
our authentication validation for our register validator. Now, currently we have this set up as a new e-mail rule that already comes with a unique constraint.
00:59
But for our organization invitation system, we're going to need a custom unique constraint for that. So we can take just these base validations for our e-mail,
01:09
give them a cut and then export const e-mail rule equals vine. and then give them a paste. Then in terms of our new e-mail rule,
01:18
rather than doing vine.unique, we can now chain this off of our e-mail rule and give it a clone. Let's give that a save and all of our red squiggly should go away.
01:27
We can get rid of that extra line break there, give it a save once more for formatting, and I'm going to go ahead and add a line break in there so that we have a separation. So now we have two e-mail based options.
01:36
We have just the e-mail rule, which will verify it's a string, a max length, an actual e-mail, and then it will normalize it. Then we have our new e-mail rule that will do all of that in
01:44
addition to making sure it's unique inside of our database. So the new e-mail rule is great whenever we're adding a new user into our database, and the e-mail rule is great whenever we just need to
01:53
verify the provided input is an actual e-mail. So we can jump back into our organizations validator, and make use of that e-mail rule now,
02:01
give that a clone, and then we're going to want to add in our own unique constraint to this, which takes in the database value and field.
02:09
Inside of here, we're going to want to do two different checks. First, we're going to want to make sure there isn't already a pending invite for the provided e-mail,
02:19
making sure that they haven't already been invited. Then we're also going to want to make sure the user isn't already an org member. So for our first case,
02:29
we can call this const invite match equals await db, query this from our organization invites table,
02:38
where the organization ID is the field meta organization ID, and that will come along with the metadata that we
02:47
provide whenever we actually make use of this validator. Then we also want to check where the e-mail is the value that we're trying to insert into the database.
02:56
In addition to that, we also have two more where statements that we want to add to, and for both of these, we want to ensure the value is null. So we want to make sure that the invitation has not been accepted,
03:06
and we're making sure that this is null because if the invite has been accepted, then that means that the invite is no longer valid, and the user is either already a member of the organization,
03:15
which our second check will take care of, or the user was previously a member of the organization has since been removed, meaning that they could then be re-invited here.
03:24
Then we also want to check additionally where the canceled at is null too, for much the same reason. If the invitation has been canceled, then the invites no longer valid,
03:33
and they may then be re-invited into our organization. Then to simplify this, we can select just the ID as all that we care about is whether or
03:40
not it exists inside of the database, and then call first. So if we have any matches here with our invite match,
03:47
then the invitation e-mail is not unique and we'll want to like that accordingly. As for our second check here, we can call this our org match as we're
03:56
validating to make sure the user is not already a member inside of the organization. So we'll query this from our organization users pivot table, join in our users.
04:06
The primary column is going to be our organization users.userid. So the user ID on our pivot table there.
04:14
Then the secondary column is going to be our users table ID, and this will add in a join statement for that. So now we have users.to work with.
04:23
Our first where statement for this is going to make sure that
04:26
the organization users pivot table.organizationid matches our field meta organization ID,
04:35
ensuring that the users that we're checking are for the specific organization that we're trying to invite the user to. Then our second where is against the users e-mail,
04:43
which is why we're joining the users table in right here so that we have access to the organization members e-mail to check
04:51
against the e-mail that's being provided into the validator. Then we want to select the users ID there and call first. Again, if we found any matches here,
05:01
then that means that the e-mail provided is not unique. So in terms of these validations, we don't need to run to if one flags any users.
05:10
So we can return immediately if invite match found anybody, go ahead and just return false. As if we found a record, then it is not unique. So we'll return false to flag it.
05:20
Then down here, since we've already checked our invite match, we can return the inverse of our org match results. So if we did not find any org match, then we want to return true.
05:30
If we did find an org match, then we want to return false. That should do it for our e-mail validation rule. The second field that we want to verify against is
05:38
which role the invited user should receive. So for this, this is going to be a number. And we want to also verify that this exists within our database.
05:47
So we'll accept DB and value there, get our callback. And this one's going to be much simpler. We just want to do a match check, which will await DB from our roles table,
05:56
where the ID matches the value. And then we can just select the ID and call first there, returning with the bang bang operator, whether or not we got a match.
06:06
So if we found the role inside of our database, that means that it does exist. So we want to return true and vice versa. If we did not find a match, then we want to return false there.
06:15
And then that bang bang operator will take our match and return it back as a Boolean. All right, so that should do it for our validation. Let's next jump back into our terminal here
06:25
as we need to create our action. So we'll do node ace make action. We'll put this inside of our organizations folder and call it send organization invite.
06:34
We can hit enter to create that. And jump back into our text editor. Scroll on up to our actions organization, send organization invite. We're going to want to accept in the organization
06:44
of type organization, the invited by user ID. So we want the ID of the user who is sending the invitation there. That'll be of type number.
06:54
Then we also have our data for the invitation, which we can infer the type of our organization invite validator there. Go ahead and grab those out of our handle method.
07:03
So we have our organization, the invited by user ID and our data. The very first thing that we want to do is create the invitation using our organization.
07:13
So we'll reach for our relationship to our invites off of our organization and then create a new record there. We want to tack onto this,
07:21
the invited by user ID to create that relationship. Also tracking who made the actual invitation. And then we can go ahead and spread in the rest of the data into that object.
07:31
Now, once we've created the invitation inside of our database, the next thing that we want to do is actually send an email notification to that user, letting them know that they have been invited. So we'll await mail,
07:41
import that from Adonis mail services main, dot send later and grab the message builder out of there.
07:48
We'll send our message to the data dot email, which is the email of the person that's been invited. We'll send that with a subject.
07:57
You have been invited to join and let's switch this to back ticks so that we can add the organization name to the subject line.
08:07
Then we'll create an HTML view at emails slash organization underscore invites. And we're going to want to pass the organization in,
08:16
but we also need a URL for this user to actually accept the invitation with. And for this, a signed URL is a great use case because we already have our invite inside of our database,
08:25
which we can use to track whether or not the invitation itself has been accepted or canceled. If it's been either one of those, then it's already invalid. So we don't need to worry about invalidating this link here altogether.
08:35
In addition to that, it also adds that protection layer of the signature, making sure that it's specifically being used by the person that we've sent it to and that it hasn't been tampered since.
08:44
Now, in order for us to create our signed URL, we first need to have a route definition rigged up for that. So we'll circle back and take care of that.
08:54
For right now, let's go ahead and do our const, invite URL equals and just set that to an empty string so that we can go ahead and add our invite URL here in this lesson.
09:03
Okay, let's finish out by getting this email added into our application. So let's jump back into our browser here and we'll want to jump back over into Mele.to,
09:13
open up the editor there, and we have this, you've been invited template right here. So we'll add in the user's name if we're inviting a user
09:21
that's already a member of our application. Otherwise we'll fill that in with something else. And then we'll also add in the organization's name, rig up that signed URL to both the joined organization
09:31
button, as well as plop it down at the end of the email in plain text as a link. So we go ahead and copy this HTML, jump back over into our text editor
09:40
and let's scroll on down now into our resources, views, emails directory, and we'll make our organization invite.edge file.
09:48
So organization invite.edge and paste that HTML that we've copied from Mele.to into there. Again, this entire HTML structure
09:58
I'll have linked down below as well as a link reference to Mele.to. So anywhere that we have this placeholder.com, we're gonna wanna go ahead and find and replace
10:07
with double curly braces so that we can use EdgeJS to inject in our invite URL. Go ahead and replace all of those. And that should happen in our anchor
10:16
that's down at the bottom of the email, as well as that button right up here. We have that href right there. In addition to that,
10:23
we also have our name and org placeholders here too. For the org placeholder, we can go ahead and replace that with organization.name.
10:32
As for the name that we are sending this email to, and we haven't currently populated anything for that. So we can default that back to hi there. But on top of that,
10:41
we could also jump back into our send organization invite and check and see whether or not the invited email exists inside of our database.
10:49
So we could do something like const invited user equals await import our user model and find by the email, and then use that data email from our validator
10:59
to try and find that record. We don't want to use find by or fail because we might be inviting somebody who's not yet a member of our application. So this could very well be null
11:09
as noted by the type right there. Go ahead and jump back down into our HTML view data and add that invited user into there. Go ahead and give that a copy and we can jump back into our organization invites
11:19
and do double curly braces, pop that invited user in question mark dot full name, otherwise default back to just saying there. Cool, real quick,
11:28
let's jump back into our send organization invite. It'd probably be great to go ahead and use the database stored values for both of these lookups. So let's switch our emails here
11:37
to the actual invite records email rather than reading from our data, just like so. And in addition to that, in the next lesson, we'll finish out this action
11:44
by adding in our accept invitation route, it's action, and then we'll populate this URL here with assigned URL.
11:52
For now, let's jump back into our organization controller and add in our invite user controller method. So for this, we're gonna want our request, response,
12:00
organization, session, and auth out of our HTTP context. And we'll first validate our data. So await request,
12:09
validate using our organization invite validator. And then we also want to make use of our with organization metadata and pass the organization ID into there
12:19
so that we have access to that organization ID to verify that the user being invited is not already a member of the organization
12:27
and does not already have an active invitation pending. Then we can go ahead and await, send organization invite, call our handle method, pass the organization in,
12:37
the invited by user ID, which will come from our auth use web.user. And then we can assert that and pass the specific ID in
12:46
for the currently authenticated user there. And then we also want to add in our validated data. Once we've sent that invitation, we can go ahead and session flash,
12:54
success invitation has been sent. And then we can return response redirect right on back in case they have anybody else to invite.
13:04
Go ahead and give that a safe formatting. And then we can jump into our web routes and add this route. So router.host/settings/organization/invite.
13:15
And this will use our settings organization controllers, invite user method, as settings organization.invite. All right, cool.
13:24
We will pick it up here in the next lesson by adding in our accept organization invitation flow.