00:02
All right, first we wanna jump back into our organization controller, the one for our settings section, and get a list of pending invites
00:12
that we have inside of our organization. Currently that would be none because we haven't sent any yet, but we'll also do that here in this lesson. So let's do node ace make action, put this inside of our organizations folder
00:22
called get organization pending invites, and we can clear that out. Jump back into our text editor and let's jump into that get pending invites action.
00:32
We want to take in our organization of type organization and grab that out of our params. Here we want to return organization.related,
00:41
reach through our invites relationship and query from there. And we specifically wanna check where the accepted at
00:48
and the canceled at are both null. Because remember back to the last lesson whenever we discussed that if they have a value, then that means that the invitation has been accepted
00:58
or has been canceled and then makes the invitation as a whole invalid. And then we can go ahead and order these by the created at date in a descending fashion.
01:07
So it goes most recently invited to oldest invited. Okay, we're good there. Jump back into our organization settings controller because we now need to add our invites in
01:17
as an async method, get our comma down there, const pending invites equals await get organization pending invites handle
01:27
and pass the organization in. Then we'll want to return organization invite ETO.fromArray and pass our pending invites in. We can give that a save.
01:37
And next let's go ahead and set up the card and then we'll work on our settings organization page. So let's scroll back down to our inertia directory, components, right click that, new file.
01:46
And we'll call this our organization user invites card.vue. Script setup lang of TS. And then we'll also add that template in there.
01:56
Within our script, we want some props. So we'll define props. And inside of here, we're gonna want those invites,
02:02
which will be an organization invite DTO array. And then we also want our applications roles, which will be a role DTO array there.
02:12
After our props, we're gonna want our invite form. So for this, we can use form from inertia view three. We'll take in an email, which will be an empty string by default.
02:21
And then the desired role, the invited user should be assigned should they accept. And for this, we can use our roles enum.
02:29
So we'll import that dot and default it to member. So since we're using our enum there, what that's gonna do is just give it a value of one by default, which one put in our database,
02:39
we'll map to our member role record. Then we also want a function to get the role name for display. So we'll take in a role ID of type number
02:47
and then return props roles, find the role where the role ID is equal to the role ID being passed in.
02:56
And then we just wanna grab the name off of that. As for our template now, as implied by our component name, we want this to be a card. So we'll get our card going here with a card header
03:06
and a card title of pending organization invitations and a card description of the following users
03:17
have been invited to your organization. And then underneath our card header, we will have our card content. The first thing that we'll have inside of here
03:25
is our table listing out our pending invitations. So we'll have a table header with a table row and a table head. And the first column is going to be the user's email
03:35
that we've invited. And then the second table column is gonna be the role that we want that user to have should they accept. And then we have our last table head,
03:43
which will eventually be our cancellation action. Underneath our table header, we will then have our table body and inside of the table body will be a table row. This table row is where we're gonna wanna loop over
03:53
each of our invitations. So we'll add a V4 invite in invites with a key of the invite ID. The first cell inside of our table row,
04:03
so table cell is gonna be the invite email. Second cell is going to be a call to our get role name and pass the invite role ID into that.
04:13
And then the third for right now, we're just gonna go ahead and leave blank. We'll fill that in later on. Underneath our looped table row, we also wanna put an additional table row in
04:23
that we display if we don't have any invites. So we'll do an inverse check against our invites length there. So if we don't have any invites,
04:32
we'll do a table cell that spans all three of our columns and we'll put a div with a class text center text slate 600,
04:40
no pending invites. Underneath our table, but still within our card content, we're gonna put kind of a card within a card here where we can actually send an invitation.
04:50
So to differentiate this a little bit, we'll do a div class, adding a four, make it rounded and give it a BG slates of 100 with a margin top of eight. Then we'll give it an H4 heading
05:00
with the class font bold invites new member. Underneath that, we'll do a little bit of a descriptive text with text slate 400,
05:09
text small margin bottom three that says invite a new member to your organization. And then underneath that is where our actual form will go.
05:19
On the form, let's go ahead and tack on a class, flex, flex, wrap items, end and a gap of four. And then we can also tack on our @submit,
05:29
prevent invite form.post and send that out to our settings, organization, invite. And we also want to reset the form
05:38
and preserve scroll on success. So let's go ahead and we can just add in a non-reactive object called invite form options
05:46
that on success follows the invite form reset method and also preserve grow, we can set to true. Then we just need to take this invite form options
05:56
and give it a copy and paste it down as the second argument on our post there. Inside of our form then, we'll have our form input and this one will be for our email.
06:05
So we'll give it a label of email, a type of email, a class of flex one so that consumes as much space as possible,
06:13
a view model of invite form email, our error of invite form errors email,
06:21
set it to disabled when our invite form is processing. And then we also want to make that required. All right, then after that, we have our role selection.
06:31
So we can use our form input as a select input here as well. It won't be self-closing as we'll need to put our select options inside,
06:38
but we can give it a label of role, a type of select, a view model of invite form role ID,
06:46
error of invite form errors role ID. And then we can set this as well to disabled when our invite form is processing.
06:55
As for the select options, so inside of our form input here, we'll do a select item loop over each role in our roles,
07:04
set the key to the roles ID and set the value to role.ID there as well. Inside of the select item for the display text, we can put the roles name there.
07:13
Then lastly, we're gonna have a button of type submit and set it to disabled when our invite form is processing. Inside of there, we can do our loader icon
07:23
from Lucidview next if our invite form is processing. Now we can also give it a class margin right to height four, width four and animate spin.
07:33
With the text of send invite. Last thing to do is to jump now back down into our settings organization page so that we can add this card in, but we also need to add our new prop in as well.
07:43
So it's gonna be called invites as our organization invite DTO array. And then we can add our card in
07:51
as organization user invites card, providing it our invites as a prop. So invites there and then roles as roles there as well.
08:01
And a member of ours, Gribble, gave me a note that in view 3.4, they actually added a shorthand syntax to where if the prop name and the variable name that you're passing in match.
08:10
So as all of ours do right here, you could do a simple shorthand where you just leave off the equals and it will derive that as user from user. So if you prefer that shorthand,
08:19
you also have that option available to you as well if you're using view 3.4. I'm gonna go ahead and leave everything consistent with the rest of our project here as is. But thank you again for that note there, Gribble.
08:29
And with that all set, we should be able to jump back into our browser now. I see an alert here already, at least one template or script. I think that's just, we need to refresh there. Nope, I must not have saved something.
08:39
All right, let's jump back into here. Looks like I saved this. Take a look at our recent files. Looks like I saved everything in here. Let's try, nope, sure enough, I didn't save this. Okay, let's save that then. That would definitely do it.
08:49
Jump back into our browser. Give that a refresh one more time. There we go. Okay, so we scroll back down now and now we see our pending organization invites section. We currently have no pending invitations,
08:59
but we now have a form down here as well where we can send out a new invite. Our form input for our role is currently missing our role options.
09:08
So let's go take a look at that while we're here. I see roles there, scroll back up. We've got it in the props. Let's go take a look at our page. Ah, role. Yep, that needs to be roles there.
09:18
Let's give that a save now. Jump back into our browser. Give that one more refresh. Sorry about that. Scroll back down. There we go. Now it defaults to member, but we also have admin there as an option.
09:27
Cool, so let's go ahead and give this a test. So we'll do test1@invite.com and we can leave that as member. Go ahead and send that invite. And there we go.
09:35
So now we have sent an invitation to test1@invite.com who currently does not exist in our organization. So they'll need to go through our registration flow. So since that invitation sent,
09:45
we should be able to jump back into MailTrap, which we're sending our SMTP test emails to. Jump into our inbox here. And there we go. There is our, you have been invited to join my second organization.
09:55
And it's sent to test1@invite.com. If we click on that, we see our email here. And we can see the full link right down here with our signature right there. So if we tamper with that in any way,
10:05
we should get an error. And then it's also an absolute URL with the origin of our server prefixed on our route. Now at present, we're already logged into our organization.
10:15
So if we were to click on this, we'll open it up in a brand new tab, but then it will give us the warning. Your email does not match the invitation because we're already logged in. And the user that we're logged in with
10:24
was not the user that we sent the invitation to. So that flow works okay. But what we do need to do is jump back into our application now
10:32
and log out in order to test all of this. So let's jump back now over into MailTrap. Give that one more click. And now we're sent to our registration page
10:41
because this user does not exist in our database yet. Now we have the invitation that we're working with that already knows what email it's meant for. So if the user tried to type in something that's not meant for the invitation there,
10:51
they're gonna end up getting an error, that same error that we just saw where the email does not match the invitation. So what we can do in that case is jump back through to our registration flow.
11:00
So if we jump back into our register controller here, in our show method, we can go ahead and grab our session.
11:06
Const inviteId equals session.getInvite_id. And then we can do an if inviteId,
11:15
const invite equals await organization, invite.find and pass the inviteId in. Now, if we can't find the invite by its ID,
11:24
what we wanna do is then clear that session out. So session.getInviteId, if not invite, wrap that in brackets there as well.
11:33
Otherwise, what we wanna do is share that with Inertia so that we have it readily available to work with inside of our page. So now we can jump back down into our register.view page,
11:43
do const props equals define props. And now we may or may not have this. We'll do invite optional of type organization invite.
11:53
And we also need to cast that to our DTO there. So let's jump back into our register controller there and share the invite as a new organization invite DTO. Invite just like so.
12:03
And now we can jump back into our register page. And there's a couple of different ways that you can do this, but ultimately what we're after is that they register with the same email that they've been invited with. So what we wanna do is on our form,
12:13
set this to props invite email or an empty string. And since email's optional, we need to add a question mark in there. And then down on our email input,
12:23
we can add in a disabled leg if we have an invitation. So if we're working with an invite, we'll just kind of pre-populate the email and disable that input altogether.
12:32
You could also just hide this field altogether as well. For our case, it's gonna be good to see what the email is here for debugging purposes. Okay, so let's jump back into our browser.
12:41
Now we should still have that invite ID on our session. And sure enough, whenever we refresh, there it is right there as a disabled field. So for this user, we just need to add in the full name. I'm just gonna put test1 there
12:51
and then something for the password. If we register, there we go. They have now been added as a registered user inside of our application. And they've also accepted the invitation into my second organization.
13:01
So if we jump back now into this user's organization settings, we see them as a full-fledged member of it with the role member as we've assigned. And they're now out of our pending invitations.
13:11
Now, despite this user being a member inside of this organization, currently they can still invite somebody even as an admin. That's gonna come in our authorization step in the next module.
13:21
So we will take care of that, don't worry. For now, what we wanna do is continue testing our flow. So let's get back to this page as our admin user. So let's go ahead and log back out.
13:30
And rather than logging back in as our test user one, let's first register a new user so that we can test the preexisting user flow here. So we'll do testinvite2.
13:39
I think we just call the last person test1, but that's all right, the name doesn't matter too much. And then we'll do test2@invite.com, something there for the password, register this user.
13:48
It can create them something for the organization. So now they're already a member of an organization. We can go ahead and log them out.
13:55
And now let's log back in as our test user one@test.com, something there for the password. Jump back over to our organization settings
14:03
and let's invite that test2@invite.com. And this time we can also test them as an admin as well to make sure that's working. Send this invite.
14:12
And now they have been invited as an administrator. And since they already exist inside of our database, we should be redirected when we log out and click that link inside of MailTrap
14:22
to the login page rather than the register page. So let's click join organization. And sure enough, here we are at the login page.
14:29
Do test2@invite.com, something there for the password. Hit enter. Okay, I spelled it wrong. Something there for the password, enter. There we go.
14:39
Our invitation has been successfully accepted and we're now inside of our second organization, which we've been invited to. Jump back over into our organization settings and we can see exactly that.
14:48
We are now a full-fledged member and our invite has been moved out of the pending state. The last flow that we need to check is for a user who's already authenticated.
14:57
So let's log out one more time, register one more time. Test invite three, test3@invite.com, something there for the password.
15:07
We create our organization. And now we need to log back out and log back in as our test user one@test.com, something for the password.
15:16
Jump back over into our organization settings, jump back down to our invitation section and invite test3@invite.com. Doesn't matter the role here, we've verified that they both work okay.
15:26
Send the invite, log out one more time, log in as test3@invite.com, something there for our password.
15:35
Okay, we're logged back in as this user. Now we can jump back over to MailTrap, click on our new email. We're already logged in as this user. So now whenever we join this organization,
15:44
we should be redirected right back to our courses index page. So let's click join organization. And sure enough, there we go. Invitation successfully accepted
15:53
and we're redirected back to our courses index page. And there is my second organization, which we can now freely switch to and jump back over to the organization settings
16:02
to see that we are a member of. Awesome, so all of the valid steps for our invitation system are working perfectly fine. Let's go test now the invalid steps.
16:11
So let's log back out here. We've already verified that the email match works whenever we first tested our system. So let's jump back over to MailTrap now.
16:19
The signed URLs themselves should still all be valid. So if we right click on this, copy the link, and let's jump back over, we can close out all these extra tabs that have opened up,
16:29
get a new tab going. And let's paste that in and go to it. Okay, we've got redirected to the login page, but that's because we don't actually check to see whether or not it's accepted or what have you until after the user's logged in.
16:38
So let's do test3@invite.com, something there for the password, log them in. And there we go, we see this invitation's no longer valid because it's been accepted or canceled.
16:48
Now, currently with the way that we have this set up, this is going to block our user from being able to log in or supposedly. So we are actually logged in. If we refresh there, there we are. But to our user, it's going to appear
16:58
like their login did not succeed. So what we wanna do instead is probably handle that a little bit more gracefully. So let's jump back into our accept organization invites,
17:08
scroll back up to here, and we can probably use our message system here to our advantage. So rather than throwing an unauthorized exception,
17:17
we can instead return, we can set the invite to null, we can set the message to something like what we had in our exception.
17:24
This invitation is no longer valid. And we can also add in a state of error onto there
17:32
and then jump back down here and do a state of success. Okay. And then we wanna jump into our web register action and update our flashes here.
17:40
So we'll switch the success to result.state. And then we still have our result message there to handle that appropriately.
17:48
Let's jump back now over into our web login to do the same. So switch this to result.state, and then we have our result message there already.
17:56
And lastly, into our organization's controller where we want to switch this one here as well to result.state. Okay. Give that a save. Jump back into our browser.
18:06
We shouldn't need to log out or anything. We should just be able to put the URL inside of here, hit enter. And we did not get our flash message down there whenever we did that.
18:15
So let's jump back into our organization controllers, accept invite method, and let's just console.log our result here just to make sure we're getting back what's expected.
18:24
So let's jump back into our browser one more time and try sending off that signed URL one more time as well. Jump back into our terminal. Where we have our server running. And sure enough, we get back our state of error
18:34
and our message of this invitation is no longer. Ah, we need this to be our errors bag. My bad. That's where I expected to plop that at. So let's jump back into that handle method.
18:43
Switch this from our error to our errors bag, which is where we're handling more of our exceptions. Give that a save. Jump back into our browser. And let's try this one last time.
18:52
So let's jump back into here. Give that a paste. Hit enter. There we go. An error occurred. This invitation is no longer valid. Awesome. So we're getting that warning okay.
19:01
The last thing that we want to check is to make sure that we also verify the signature appropriately. So this long bit right here being our signature,
19:09
this is being checked within our organization controllers, except invite. It's one of the first things that we're doing right here to make sure that that has not been tampered with in any way
19:19
and has also not expired, even though we're not setting an expiry on it. So inside of here, if we just go ahead and tamper with that URL slightly,
19:27
I just added a little bit of an extra string there to it. Hit enter. There we are. We see an error occurred. An invalid invitation URL was provided. So awesome.
19:37
It seems like our entire flow here is working perfectly fine now. Next, what we need to do is inside of our organization settings, add the ability to cancel a pending invitation.
19:47
So we'll do that next. First, let's jump back into our correct account. So testuser1@test.com. This is the administrator that we've been working with. There we go.
19:57
Organization settings.