00:02
Now, when it comes to where we should place our organization invite route, it's not so much a setting, so it doesn't belong in our settings section.
00:11
And we don't even want it inside of this route group because of two things. First, they're not yet a member of the organization. And second, we don't know whether or not
00:19
they actually have an account inside of our system. And that's gonna be part of our invite accept flow as well. We need to first check and see whether or not the user already has an account inside of our system
00:28
to determine where we need to send them during their acceptance process. So we know we don't want it inside of this route group, but this organization group that we have
00:36
at the top of our web file as well also is wrapped by auth. And since we don't know whether or not we're going to be dealing with an already authenticated user, somebody who needs to log in or register,
00:45
I feel a more appropriate place for this route is going to be inside of our auth along with those route definitions. So let's place this down at the bottom of our auth file as a router.get.
00:55
Since the user is gonna be coming from outside of our application via an email, we need this to be a get so that they can safely enter our application and we can begin processing their acceptance.
01:04
And since this is still bound to our organization resource, we can prefix this route definition with organizations and then do invites and then take in the invite ID
01:14
and then tack on the action that we're doing, which is accepting that invitation. So since this is still based off of our organization resource, we can put this inside of our organizations controller.
01:24
And we specifically want the base organization controller and not the settings organization controller. So we'll import the base one there and we'll add a new method in here called accept invite.
01:34
We'll get a red squiggly 'cause currently that method does not exist as organizations invites accept for the route name. And let's go create that method
01:43
inside of our organization controller. And I'm gonna go ahead and hit command B to hide that sidebar out of our way here. All right, let's do async, accept, invite, get our HTTP contacts going there
01:53
and out of our HTTP contacts, we're gonna want our request response off for rams and session. Since we'll be sending this accept invite link
02:02
within our send organization invite as a signed URL, the very first thing that we're gonna wanna do inside of the route handler that handles that signed URL route
02:10
is ensure that the signature on the signed URL is still valid. And we can do that using our request. So we'll do an inverse check against our requests
02:19
as valid signature method. And it's gonna ensure that our signature hasn't been tampered with in any way and is still valid. So since we added a exclamation point to the beginning of that,
02:28
we're checking to see whether or not it is not valid. And when it's not valid, we'll wanna use our session to flash into our errors bag,
02:35
the message and invalid invitation URL was provided. And then we can go ahead and return response, redirect to the routes.
02:45
And here we're going to want to either redirect them to the courses index page if they're already logged in or to the login page if they are not logged in. However, for this route handler,
02:55
if we jump back into our auth, we aren't adding on any auth based middleware to this route. Therefore our auth user is not going to be populated automatically for us.
03:05
So the first line inside of our accept invite method, we'll check to see whether or not we have an authenticated user using auth dot, and there's check directly off of auth,
03:14
or you can use the use method to specify which guard you wanna use to check for an authenticated user. Whenever you specify a guard like this, in order to get access to the user,
03:24
so if we do const user equals, we'll also need to use the use method there to get access to the user as just auth user will not be populated in that case.
03:34
And then check here returns back a Boolean true if the user is authenticated and false if not. So now we have access to whether or not our user is authenticated. If the user is not authenticated, this will be undefined.
03:44
If they are authenticated, it will be an instance of our user model. We can use a ternary, either directly inside of the toRoute method or for the entire response redirect chain here.
03:53
So just checking to see whether or not we have a user. If we do, we wanna redirect them to their courses index page. And if we don't,
04:01
we want to redirect them to the route login dot show. Okay, so once we're past this if check, we know that the signature on the URL is valid and has not been tampered with.
04:11
What we next wanna do is check and see whether or not we're dealing with an authenticated user, and if we're not, whether or not they need to log in or register so that we can send them to the appropriate next step.
04:21
So we wanna do an inverse check against our user to see whether or not we don't have a user. If we don't have a user, then we wanna grab our invitation. So awaits,
04:29
and we can reach for our organization invite model to find or fail using our programs ID. Using that invites email,
04:38
we can check and see whether or not we have a user inside of our database matching. So we'll use our user model for that. Make sure you go down to model dot find by, and we definitely don't wanna do a find by or fail here
04:48
because if the user is not found in our database, we simply just wanna send them to the register page so that they can create their account. So we can find by our email using the invite email.
04:57
Okay, so technically our is user here is going to be a user or null, but we're gonna use that as more of a Boolean rather than anything else. So once we have that determination,
05:06
what we wanna do is return the user to either our login or redirect appropriately. So if we have a user, then we'll do another ternary with response,
05:14
redirect to the route login dot show, since they already have an account in our system for the email. Otherwise we want to send them to the register route.
05:24
So to route register show because they need to create an account in our system. Now, in order for these two routes to know which invitation they're working with or whether they're working with an invitation at all,
05:34
we can go ahead and plop the invitation ID on the user's session using session put and plop that in a variable called invite ID as invite ID there.
05:43
And then we'll be able to read from that off of the user session whenever we're handling our login and registration. If it's populated, then we know that we're dealing with an invitation. And if it's not,
05:52
then we know that we're not dealing with an invitation and we can just bypass that altogether. All right, so now we have both verification that our signature is valid and we're now handling the use case
06:01
where our user is not authenticated, ensuring that they get authenticated or create an account in our system. The next thing that we wanna do is handle the use case
06:10
where a user is already authenticated. And when that's the use case, we can go ahead and process their invite. So for that, we're gonna wanna create a new action. So we'll jump into our terminal here,
06:20
node ace make action. We'll put this inside of our organizations directory since we're dealing with that resource. If you want to, you could also create a separate organization invite resource there, but we'll call this action,
06:30
accept organization invite. Hit enter to create that and we can clear out our terminal and jump back into our text editor. Let's go ahead and jump now into that
06:39
accept organization invite action. From our params, we're gonna want to take in the invite ID as a number. You could also accept in the invite as a whole, if you wish.
06:48
And we're gonna go ahead and just stick with the number input there for the invite ID. And then we also want the user record for the user matching the invitation.
06:56
So this is the user accepting the invite there. Okay, let's grab our invite ID and the user out of our params. And the very first thing that we're gonna wanna do is grab reference to that invite.
07:06
So we'll do awaits organization invites, import that model and we'll just do a find or fail. As if we cannot find the invite, then we really don't have anything to process.
07:16
Pass the invite ID into there. And the next thing that we wanna do is perform a couple of checks. We wanna verify that the email bound to the invite matches the user's email accepting the invite.
07:26
If it does not, then we wanna throw some sort of exception as we cannot verify that the user accepting is the one meant for the invite.
07:34
So we'll do invite.email does not equal user.email. And when that's true, we can go ahead and throw a new and we can either use exception like we have in the past,
07:42
or we can create a new exception that extends the base exception. So let's jump back into our terminal real quick and do node ace make exception.
07:52
And we'll call this on authorized. Hit enter there and that will create us an unauthorized exception. We can clear out of that, jump back into our text editor. And now if we open back up our file explorer,
08:02
we can hide our models back away and we should now under exceptions have a new unauthorized exception file. And this extends that base exception. So if we leave the constructor out,
08:11
by default, it will just accept in the same constructor arguments that the exception accepts. So we can simply change the status here to 403
08:20
and then give it another static code of E and then something like unauthorized and then leave the rest alone. And it will be handled like a traditional exception
08:30
from there, just pre-populating that status and code for us. So with that said, we can jump back into our organization invite and hide that sidebar back away and switch this to unauthorized exception.
08:40
And whenever we call the constructor for this, you can see that we can now pass in a message. And since we've already pre-populated our options, we can simply leave those off. So our message for this is gonna be
08:49
your email does not match the invitation and leave that be and let our system do the rest of the work there. Then we also wanna check whether or not the invite has already been accepted or canceled.
08:59
So we can check invite.acceptedAt or invite.canceledAt. And despite these values being date times, we're using them two different ways. We have that date time to know
09:09
when they were canceled or accepted. And then we also have that date time that we can use to check and see if it's null, then we know it's falsy and they have not yet canceled or accepted it.
09:18
And if it then has a value, then it's truthy and they have canceled or accepted. So we can continue onward with this. If either of those come back truthy, we know that they've either accepted or canceled.
09:27
So we'll throw a new unauthorized exception. This invitation is no longer valid. And those are the two checks that we wanna do
09:37
before we actually process the acceptance. So once we have that set, we now wanna work inside of a managed transaction. So let's import DB, get our transaction going, just like so with our callback.
09:47
And the first thing that we'll do is tell our invite to use this transaction. And now we wanna get the organization that the invite is meant for. Again, since we're working outside of our organization middleware,
09:56
we don't have that organization directly to work with. So we'll do const organization equals await invite. And we have that relationship to the organization directly off of there.
10:06
And we should be able to do query dot and then just call first or fail as there should just be the one. Once we have that organization, we wanna bind the authenticated user
10:16
now to that organization as a user. So we'll await organization dot related users. And then we want to call the attach method.
10:25
And here, if you don't have pivot table data to pass in, you can just simply add in an array of your user's ID. In our case, we also need to assign the role
10:34
off of the invitation to the user we're adding to the organization. So we don't wanna do that here. We instead wanna provide in an object
10:41
where the key of the object is that user's ID that we want to attach to the organization. And then the object of that user's ID
10:50
is the additional pivot table data that we wanna bind to that pivot table relationship. In our case, that's gonna be the role ID, which we can read from our invite and it's designated role ID.
11:00
For that pivot table data, note that you do want to enter it as it's defined inside of the database. So we're using snake case here for that. Additionally, we don't need to worry
11:08
about rebinding our transaction to the organization because we've bound it first to the invite. And then we've queried our organization using the relationship off of the invite. So it's cascaded from the invite
11:18
through to the organization relationship there. Once we've added the authenticated user as a member inside of the organization with the appropriate role,
11:25
what we next wanna do is set the invite as accepted. And we'll set this to the date time, import that from Luxon and set it to right now.
11:34
And then go ahead and await invite.save to persist that to the database. By setting this accepted at, we've now invalidated this invitation, meaning that it will now catch right here
11:44
if we were to try and reuse this invitation. And it's thanks to our ability to invalidate our invitations that we're able to use signed URLs to actually send the link out to our user
11:53
because you can't invalidate signed URLs, you can set an expiry, but you have no manual way to invalidate it once used. So since we have that directly on our invitation, we are a-okay to just go ahead
12:03
and use a signed URL to send that, which we'll do here momentarily. First though, what we next need to do is provide ourselves some information back outside of this action.
12:12
First, we wanna provide the invitation as a whole. And additionally, it would also be good to provide a message. We are using strictly exceptions here, but it gives us an additional way to know that everything succeeded
12:22
and we can just flash that message directly. So we do something like invitation successfully accepted or something of a sort. Give that a save. And now we can jump back into our organization's controller
12:32
and finish processing the invitation for already authenticated users. So for that, we want to grab the result and then call our accept organization invite,
12:41
its handle method, and pass the invite ID in from our programs ID, and then also the user in, and we have that directly via our user variable
12:51
up above right here. All right, once we've accepted the invitation and process that and save it into our database, we now want to make sure that we go ahead
12:58
and forget the invite ID key off of the user session so that they don't get caught in this flow the next time that they go to log in. And then we can also go ahead
13:07
and session flash a success message using the result message that we've sent back from our action. Once we have that, since we're already dealing with an authenticated user
13:17
and we know that they will at least have one organization because we've just accepted them into it, we can go ahead and return response redirect to the route horses index.
13:27
Give that a save. And now our authenticated user flow is done, but we still need to follow up and finish the login and register flows
13:35
as we'll need to check for the existence of this invite ID after we've logged in or registered a user. So first let's go to our web login, which handles our user login,
13:45
and it also injects the HTTP context. So we have the session directly within this action to work with. Let's go ahead and create a new async private method
13:53
called checkForOrganizationInvite that accepts that user in, and then inside of here, we're gonna want to use the session to check for the invite ID. So we'll do this,
14:03
CTX session, get, and checkForInviteID. If invite ID is on the user session, then this will come back with that invite ID.
14:12
Otherwise it won't be populated and it'll come back as a falsy value. So we can go ahead and do an if not invite ID, just return the user back and cancel out of this flow.
14:22
Otherwise, if we did get an ID, then we wanna go ahead and process that invitation. So we'll accept organization invite, all that action from within here,
14:30
pass the invite ID in as well as the user. And then the same as we did within our controller method, we wanna make sure that we're forgetting that invite ID off of the user session
14:40
so that they don't get caught in this flow. So invite ID there. Now we can go ahead and flash. So session, flash, success, the result message that we're kicking back from that action.
14:50
And then we want to call this checkForOrganizationInvite after we have logged the user into our application. So we'll go ahead and await this checkForOrganizationInvite and pass that user in.
15:00
Let's give that a save and let's also copy our checkForOrganizationInvite method here. You can also extract this out into a separate action if you wish. And let's move over now into our web register,
15:10
which does need to handle this ever so slightly different. So you can paste that in here and hit command dot to add an import for our accept organization invite.
15:19
The only difference here is that we also want to return the invitation back. So we'll return results invite right there so that we can return it back from our handle method
15:28
for use inside of our controller. So we can do const invite equals await this checkForOrganizationInvite, plop our user in there
15:36
and then plop our invite into the returned data. Give that a save and now we need to make use of our invite inside of our register controller. And really that's just going to be used to determine
15:45
where we need to send the user to after we have registered and logged them in. So from our web register, we can now do const and extract the invite out of the returned object there.
15:55
So if the user that we're registering doesn't have an invitation that they're working with at all, the invite will come back as undefined and we can still redirect the user
16:02
to their organization's create page as they're brand new and they won't have an organization yet. However, if we were dealing with an invitation and we did get an organization invite back,
16:12
then that means that the user that's just registered was accepted into an organization and is already a member of an organization. So we can instead redirect them directly back to our courses index page
16:22
rather than having them create a brand new organization. So we'll do an if invite and then return response, redirect to the routes versus index, just like so.
16:32
And that completes our acceptance flow. What we last need to do is rig up our signed URL off of our send flow. So let's jump back into our send organization invite method
16:42
where we have our to do right here and we can use AdonisJS's router. So make sure that you import the one from AdonisJS there, .builder to build out our URL.
16:51
And this builder allows us to just kind of segment how we build things out. So we can make, make signed, add in params, prefix it with a URL and add in query strings as well. And we do wanna do most of that.
17:01
So we wanna add in our params there to add the invite ID onto the route parameters. And then we want to prefix this using our environment variable.
17:10
So import that from startemv.get. And we have our app URL that we've used previously for our emails. Since we're sending this invite URL via an email, the user is going to be coming
17:20
from outside of our application. So we need an absolute URL there rather than a relative one. So we'll prefix that with our actual applications origin there. And then once we have that set,
17:30
we can go ahead and make our signed URL using our organizations, invites, accept route. Now, if you wanna set an expiry on that, that's where the second argument comes in here
17:40
where you can set an expires in or a purpose there as well. And then the verify signed URL also accepts a purpose to match if you add a purpose whenever you're making the signed URL there.
17:50
And it looks like my router did not import. So I'm gonna go ahead and hit command dot. Oh, nope, it's just a prettier. So let's go ahead and save. There we go. And we get rid of our to do there as well. And next we'll add in our list of invitations,
18:00
the ability to send an invitation, and then we'll test our flow out here.