Seeding Super Admin Data0:00
In this video, we're going to talk about having a super admin experience where you can see every tenant, all the data across every tenant, and also show you how to impersonate users so that if you are trying to provide support for your users, you can log in as them and help them if they're having trouble with your app. Now if you've been following along, you're probably going to want to go out to the repository. There's a link below and get the code because I've jumped ahead quite a bit. And I want to show you one file I added, which is the demoSeeder. So in this Seeder, what I'm going to do is just create three tenants, 20 users for each tenant, and then five logins, which we won't use in this video. We'll use these in the next.
tenant, and then five logins, which we won't use in this video. We'll use these in the next. And then after I do this, I'm going to create this admin User that does not belong to a tenant. And so this is how we're actually going to be able to have this super user. So let's go ahead and go to our terminal and php artisan. We're going to migrate fresh with a --seed flag. And that's going to give us that DemoSeeder. And so now our database should be seeded. So if I take a look in SQL Pro, you can see I've got 61 rows in my users table with the
Understanding Tenant Scoping1:03
And so now our database should be seated. So if I take a look in SQL Pro, you can see I've got 61 rows in my users table with the last row being this admin at admin.com. And the tenantId is null. So if you remember back in the beginning of this course, when we created the tenant, when we added an event listener, so let's take a look at the EventServiceProvider. And on login, we are setting the tenantId in the session. And so if you take a look at this, we're putting the tenantId in here based off of whatever the User's tenantId is. So as you can imagine, if we're to log in with this new admin User, the tenantId in
the user's tenantId is. So as you can imagine, if we're to log in with this new admin User, the tenantId in the session is going to be null. So let's do that. Now, I'll log in here, click through. And let's take a look at our session. And sure enough, we have a tenantId that is null. And the other thing that's really interesting is you can see all of our queries that should be scoped to a tenant are actually working. So if we look at the queries, we can get the count of how many tenants, how many users
be scoped to a tenant are actually working. So if we look at the queries, we can get the count of how many tenants, how many users and how many logins, which is all the data I'm showing here. But there's no scope. If we go back to our code, and look at the tenant scope. If the session has a tenantID, then we're going to add this scope. Now, you might be wondering, why aren't we adding a scope where the tenantID is null. And if you take a look at the method, this actually shows you that it's returning if this is null. So when the value is null, it's actually going to return false.
Super Admin Tenant Selector2:31
this is null. So when the value is null, it's actually going to return false. So our tenant scope, this is going to be false every time. So we're never going to apply this scope to the database calls. So now let's go back to our application. And if we go to the team, this is another approach I like to take is this is the same ShowUsers component that we were using before, and I've modified it a little bit so that as the super user, I can choose a tenant. And now I've got just these 20 results as opposed to seeing everything. But basically, this is all the same functionality.
And now I've got just these 20 results as opposed to seeing everything. But basically, this is all the same functionality. And so we're using the same code, we're not going to duplicate the code. So let's take a look at how this works a little bit. Let's go to the ShowUsers component. And one of the things I added here is a mount function that checks if the session has a tenantId. And if it doesn't have it, or if it has it as null, we're going to say this is not a super user, otherwise, it is a super user. And then I'm just getting the tenants that populate this drop down right here.
Adding Impersonate Link3:28
super User, otherwise, it is a super User. And then I'm just getting the tenants that populate this drop down right here. So we're going to use this super variable here to put some things on to our Blade template, depending on if this is actually a super admin or not. So let's go ahead and take a look at the show users Blade template. And what we really want to do here is add an impersonate link so that if I want to impersonate as Aaron, I can click on a link here and it will log me in as Aaron. So let's go down into our code here, you can already see the super this is that choose a tenant option. So that's where we're already using the super variable here.
a tenant option. So that's where we're already using the $super variable here. And this is the username and the user email, which is right here. So I want to add right here a link. So we'll say <a href>, we won't link anywhere right now. Let's add some classes, I want the text to be small. Let's make it indigo-600. And let's just call it impersonate. And so if I give it a refresh, that looks okay. But I want it on the same line as the name.
And so if I give it a refresh, that looks okay. But I want it on the same line as the name. So let's just do this. We're going to surround it with a div tag. And then I want this to be a span. Let's take a look at how it is now. I think that's pretty good. Maybe give it a little bit of margin on the left. So we'll go ml-1. And that looks good to me.
So we'll go ml one. And that looks good to me. Now when I click that link, I want to log in as that user. So let's go ahead and add on this link here. We'll say wire:click equals impersonate. And let's pass through the ID of the user. So just use my curly braces and we'll say $userId. So obviously this does not exist yet. So let's go back to our showUsers. And we're going to add right here just a public function for impersonate.
Implementing Impersonation Logic5:26
So let's go back to our showUsers. And we're going to add right here just a public function for impersonate. And we're going to take in the $userId. OK, now before I log in as this new user, I want to make sure I know who I am, like the user I'm logged in as right now. So let's put that into the session. I'm going to set an $originalId as the current logged in user ID. Now I'm going to put that into the session. And I'm going to call it impersonate. And this is going to, I want this for two reasons.
And I'm going to call it impersonate. And this is going to, I want this for two reasons. Number one, I need to know what my userId is so I can log back into it when I'm done impersonating. But I also just like to have this impersonate in the session so that I can use it to check if that is set, then I know I'm impersonating. So I'm just going to set this to the original ID that I just grabbed. Now I'm actually ready to log in as the user. So I'll just do auth. We'll log in using an ID.
So I'll just do auth. We'll log in using an ID. And this will be the user ID that was passed in from the front end. And then lastly, let's just return a redirect to go to the team page. OK, so let's go back to the browser. Let's refresh our page and let's see if this works. Am I actually logged in as Aaron? And it looks like I am. So you can see down here I'm Aaron. I can only see 20 results.
So you can see down here I'm Aaron. I can only see 20 results. The choose a tenant is gone. So I am definitely logged in as Aaron. And you can also see in my session, this impersonate variable is here. And it's ID 61, which if you remember here, that's the user ID I have for my admin user. You can also see the tenant ID was set. We just logged in as this user. And the event that fires on login set the tenant ID.
We just logged in as this user. And the event that fires on login set the tenant ID. So now everything is scoped correctly. Now one thing I'm noticing is I don't want, if I'm logged in as the tenant user, to show this impersonate link. So let's go back to PHPStorm. Let's get into our show users. And this I only want to show if super is true. So now let's end if here and give it a refresh. And now you can see I don't have the ability to impersonate anymore.
Leaving Impersonation Safely7:41
So now let's end if here and give it a refresh. And now you can see I don't have the ability to impersonate anymore because I'm logged in just as a regular Tenant User. So now this is great. I can just interact with this as if I were a User in this Tenant. I can do anything that they would be able to do and provide whatever support I would need to provide. Once I'm done, though, obviously right now I have no way to get out of here aside from logging out. So now let's add the ability to log out of the impersonated User.
have no way to get out of here aside from logging out. So now let's add the ability to log out of the impersonated user and go back to where we were before. So to do this, I'm going to go to Tailwind UI. And I want to search for a banner here. And I like this. I need a banner at the top. Let's use the centered one. So I can look at the code here. But I know this is what I want.
So I can look at the code here. But I know this is what I want. Let me just hit this Copy button and go back in here. And what I have is a couple of layout files. And I've got an app.blade.php layout. And this right here, you can see I'm yielding the content when you're logged in. And if we go back to our app, this is basically where the content sits. It's right above this Your Team.
Now all I want this to do is say I'm impersonating the User and give myself a link to go back to the Super Admin page. So first things first, this is this x. And when I'm impersonating, I don't want to ever lose this message. So I'm just going to get rid of that x. Really simple. This is the text here. So I'll just say, you are impersonating. And we'll get the auth user name.
So I'll just say, you are impersonating. And we'll get the auth user name. And now instead of learnMore, I'm going to say leaveImpersonation. So let's take a look at this. You're impersonating Aaron Waters. And I can leaveImpersonation if I click that link. So now to make this work, what I actually want to do is create a controller. So let's do a php artisan make:controller.
is create a controller. So let's do a make:controller. And we'll just say ImpersonationController. And let's find that. And I just want to have a public function. I'm not actually going to name this restfully. I'm just going to say leave, which I think makes a lot of sense. And now basically all I want to do here
I think makes a lot of sense. And now basically all I want to do here is log out of the current User. And I want to log in as the super User in the session. So if you remember, in my session, I am holding the userId of the User that I want to go back to when I click this link. So we'll get this working. But first we need to go back to our routes file and add a route for this.
But first we need to go back to our routes file and add a route for this. So this is definitely something that I want behind authentication. So let's just throw it right in here. We'll say Route::get, we'll call it leaveImpersonation. And I want my ImpersonationController. And that was the leave method. And let's give it a name of leaveImpersonation. Pretty straightforward.
And let's give it a name of leaveImpersonation. Pretty straightforward. Now let's go into here and let's build it. So before we do anything here, we could just add some validation here. So we could say if the session doesn't have impersonate, then we can abort(403). That just makes sure that no one's going to get into this method if they shouldn't be here. So if the session doesn't have this, go away.
going to get into this method if they shouldn't be here. So if the session doesn't have this, go away. So now if we go back actually to our show users and look at how we impersonated, all we did is we stored this in the session. And then we logged in using this ID. So why can't we just do that same thing here? So let me just get rid of that. And let's just go ahead and log in. So auth, log in using ID.
And let's just go ahead and log in. So off, log in using id. And we'll just get the impersonate. And then once we're done here, why don't we redirect to just the home page here. So let's see if this works. Let me just refresh the page. I'm going to click Leave Impersonation. I didn't wire that up yet. So let's go back to my template here.
Clearly, it didn't work, because now I reloaded it. And it looks like I'm still logged in as Tenant ID number 2. I'm still logged in as Aaron Water. So that didn't work. And the reason we didn't actually redirect is because I didn't return anything. So I need to return that redirect. So if we try again, you can see I've just got this loop. I'm trying to leave impersonation.
So if we try again, you can see I've just got this loop. I'm trying to leave impersonation. And it's leaving me here. I'm not actually logging out. I'm stayed logged in. This is really frustrating. If you really understand how this multi-tenant structure works, you could probably guess why this is happening. So when I try to log in using this ID, what this is going to do is it's going to try to go get this user out.
So when I try to log in using this ID, what this is going to do is it's going to try to go get this User out of the database. But the problem is all of our queries are scoped to a tenant. So basically, you can imagine I'm in tenant 2. And that is trying to go get this User number 61. But it's scoping that call to tenant ID number 2. So it's not finding anything. So that's why nothing's working.
But to me, it just doesn't feel right. I'm not actually trying to log out of the whole application. I'm just trying to log in as a different user. So what I really want to do is take out the global scope. And if you aren't aware, if you go to Laravel Global Scopes, let's see what we can see in the documentation here. And let's just look for Global Scopes. And so in writing this, and we already went through applying all of this. But you can see here, removing Global Scopes.
went through applying all of this. But you can see here, removing Global Scopes. So we can do a query, and we can remove one or all Global Scopes. You can see here, we can remove every Global Scope from whatever query we're running. So let's use this. And instead of logging in using ID, let's just try to log in. But what we want to do is get a User. And we can say, let's import this.
But what we want to do is get a User. And we can say, let's import this. So we've got our User without GlobalScope. This is our TenantScope. And now let's find the session impersonate, which should give us that 61 that we had. Let's get it. So I think this is going to work. Let's go ahead and come back here, give it a quick refresh. When I click LeaveImpersonation,
logged in with the right User again. And we do have the tenantId as null, but we also have this impersonation still in the session, which we don't want. So let's go back here. And before we log in, let's just say session->forget('impersonate'). And actually, we can't do that right here, because then it would not work there. So let's just move it down right here.
The only thing I'm missing is it now thinks I'm impersonating myself. But this is actually the normal user. So let's go back here. Let's go to our app file. And this whole banner here, we only want this if that impersonate variable is in the session. So let's say if session has impersonate. And down here, we've got an end if. And if we refresh, there we go.
And down here, we've got an end if. And if we refresh, there we go. So let's do it one more time. Let's impersonate a new User. Everything looks great. And when I'm done, I can leave. And we're all set. So now if we go back to our code, just to make sure everything is secure and cleaned up a little bit, if we go to our ShowUsers class,
just to make sure everything is secure and cleaned up a little bit, if we go to our ShowUsers class, we've got this impersonate method. I probably want to protect this. So we could say something like if auth user tenantId. Let's say if this is not null, then we just return. So this is just an extra check to make sure. We obviously don't want someone in one of our tenant or one of our multi-tenant users being able to get to this method and impersonate someone else.
or one of our multi-tenant users being able to get to this method and impersonate someone else. We definitely don't want that. So aside from that, I think we're good. So now, really quickly, you have the ability to impersonate and provide support to your users, which is super valuable. And if we just go back to the code and look at what else we might want to do in this super admin experience,
