How and why I built User Roles & Permissions System with User Impersonation Feature (CRM)
About me
I'm a dad and a passionate self-taught programmer with 12 months of experience. I've learned everything from google, youtube, countless hours poring over documentation and smashing my face on the keyboard.
The problem I wanted to solve
I wanted to provide admin-users of a CRM with a powerful tool to control what their employees can access, view and manipulate-- coupled with a user-impersonation feature for exploring their CRM from the perspective of any role assigned to a given user.
Tech stack
Laravel 6
laravel-spatie-permissions
Javascript
Jquery
Jquery(AJAX)
datatable.net
blockUI.js
Laravel User Impersonation Package
The process of building a User Roles & Permissions System with User Impersonation Feature
I started with a datatable.net table and began with listing my current records. Migrations, factories and seeders were created to provide real data to work with as I developed the feature. I Implemented create/edit modals to be included in my blade and then worked on developing the features and function necessary for your standard CRUD operations. Then it became obvious that AJAX would be necessary to provide the user with a seamless method of creating and editing multiple entries during their visit.
Next, I implemented laravel-spatie-permissions and mapped out the permissions I wanted an admin-user to be capable of granting to created roles with permissions being read-only and roles as the object of control for the user. A permissions table migration was made via the php artisan command line and a permission table seeder to accompany it. Inside of the seeder, I listed all the permissions necessary to grant extraordinary control to the admin-user:
$permissions = [
'role view' => 'role',
'role create' => 'role',
'role edit' => 'role',
'role delete' => 'role',
'customer edit' => 'customer',
'customer view' => 'customer',
'customer create' => 'customer',
'customer delete' => 'customer',
...
foreach ($permissions as $permission => $category) {
Permission::create([
'name' => $permission,
'category' => $category,
]);
}
$roles = [
'super admin',
'manager',
'financial',
'crew leader',
'crew member',
];
foreach ($roles as $role) {
Role::create(['name' => $role]);
}
I created a config file for referencing the id and permissions stored in my database, from within my blade(view) file as seen below:
@foreach(config('lawn_permissions.perms.customer') as $permissionID => $permissionName)
<div class="custom-control custom-checkbox mt-1">
<input type="checkbox" name="create-permissions" data-name="create-permissions" data-value="{{$permissionID}}" class="create-role-perm-checkbox custom-control-input"
id="create-permission_{{$permissionID}}"
{{($role->hasPermissionTo($permissionName) ? 'checked' : '')}}>
<label class="custom-control-label"
for="create-permission_{{$permissionID}}">{{ucwords($permissionName)}}</label>
</div>
@endforeach
Since permission types will rarely (if ever) be added or removed, this was an appropriate method:
//config('lawn_permissions.perms.customer') will give you the array of customer permissions in the file
//config('lawn_permissions.perms') will give you the entire permissions array
'perms' => [
/*
|--------------------------------------------------------------------------
| Role Level Permissions
|--------------------------------------------------------------------------
|
|
*/
'role' => [
1 => 'role view',
2 => 'role create',
3 => 'role edit',
4 => 'role delete',
],
/*
|--------------------------------------------------------------------------
| Customer Level Permissions
|--------------------------------------------------------------------------
|
|
*/
'customer' => [
5 => 'customer edit',
6 => 'customer view',
7 => 'customer create',
8 => 'customer delete',
],
These methods resulted in a dynamically created bootstrap modal that makes use of css breakpoints and BS4 grid styles, with some custom styling to maintain my theme.
With user-roles and permissions functionality complete, I was now ready to make use of the laravel-spatie-permissions Blade Directives to control what content specific permissions are able to see and view. It's best NOT to rely simply on excluding elements from displaying for a user. Make sure to use your new roles & permissions system in conjunction with Laravel's powerful middleware functionality as well.
@can('user view')
<a class="nav-link" id="v-pills-team-tab" data-toggle="pill"
href="#v-pills-team" role="tab" aria-controls="v-pills-team"
aria-selected="true">
{{__('Users & Roles')}}
<span class="badge badge-pill badge-success pull-right">New</span>
</a>
@endcan
Challenges I faced
I found it especially, unintuitive, working with Jquery to handle this amount of responsibility. One of the biggest challenges was not just creating the new elements in the DOM upon creating or editing records, but also making the elements manifest in a method that would persist for data table functions and interfaced with CRUD capabilities asynchronously. This was ultimately accomplished via datatable.net API:
let newRowHTML = '<tr id="' + newRowID + '" class="text-muted">' +
'<td id="' + newTDID + '" class="font-weight-bold sorting_1">' +
'<a href="/impersonate/' + itemID + '" data-toggle="tooltip" data-placement="top" title="Impersonate ' + newName + '" class="icon-style">' +
'<i class="fa fa-user-secret fa-lg" style="color:#1ec677;"></i></a> ' + newName + '</td>' +
'<td><div class="btn-group pull-right">' +
'<a href="#" id="edit-'+i.type+'-button' + itemID + '" data-toggle="modal" data-target="' + target + '" data-name="' + newName + '"' +
' data-id="' + itemID + '" class="btn btn-sm btn-outline-secondary text-right">Edit</a>' +
'<a href="javascript:void(0)" data-id="' + itemID + '" role="button" id="' + deleteID + '" class="btn btn-sm btn-outline-danger rounded-right text-right">' +
'<i class="fa fa-trash fa-lg"></i></a>' +
'</div></td>' +
'</tr>'
table.row.add($(newRowHTML)).draw();
Key learnings
What I learned throughout this process is that Jquery gets the job done, though not in the most glamorous fashion. If I were to start on this task today, knowing what I know now, I would have most certainly used a front-end framework such as Angular, React or Vue.js to accomplish my goal. Be sure to make use of any state-management pattern/libraries available to your chosen framework right away.
Some folks will recommend learning the base language of a framework first, however, I began my journey in Laravel-- having never wrote a single line of PHP before-hand. This is doable and I would argue that briefly covering the base-language for a single week will give you enough of a foundation to move to a framework. Working with a framework from the beginning plunges you straight into a mature example of a solid MVC design pattern, tuned over months/years by hundreds of experienced developers.
Tips and advice
Fiddle with Jquery(AJAX) & Javascript briefly, then move to a front-end framework. Do your best to account for edge-cases when you're performing QA on new features: Often, when you think you're finished, you're not. It's best to solve those edge-cases while the related code is still fresh in your mind before moving on to something else, if your timeline permits.
DONT REINVENT THE WHEEL! Yes, I could have spent months creating these systems such as data tables with search/sorting, user impersonation, and roles/permissions relationships and middleware with blade directives. But someone else already solved my problem. Instead, I was able to do this within a week or two as I learned, and I'm now familiar with the installation and implementation of several powerful packages that will easily transfer to future project needs.
Final thoughts and next steps
I will certainly look at refactoring this tool with a front-end framework in the future and I'll be continuing my development of the project on a daily basis. This is only one of many present features.