Robust Role & Permission Architecture for Multi-Tenant Restaurant Management System
This document outlines the transition from a basic role-based access control to a scalable, PrestaShop-inspired permission system. It includes database structure, logic implementation, and profile-based permission examples tailored for multi-tenant environments with modular applications.
// Schéma de base de données pour le système de permissions
Table users {
id integer [primary key]
email varchar(255) [unique, not null]
name varchar(255)
password varchar(255)
created_at timestamp
updated_at timestamp
profile_id integer [ref: > profiles.id]
}
Table profiles {
id integer [primary key]
name varchar(255) [not null]
description text
is_default boolean [default: false] -- for superadmin
is_system boolean [default: false] -- for seeded profiles (superadmin, admin, commercial...)
created_at timestamp
updated_at timestamp
}
Table permissions {
id integer [primary key]
key varchar(255) [unique, not null]
description varchar(255) [not null]
module varchar(100) [not null]
created_at timestamp
updated_at timestamp
}
Table permission_profile {
id integer [primary key]
profile_id integer [ref: > profiles.id]
permission_id integer [ref: > permissions.id]
created_at timestamp
indexes {
(profile_id, permission_id) [unique]
}
}
// Données initiales des permissions
// Module: restaurant
// - *restaurant.access_all* - Accès à tous les restaurants
// - *restaurant.access_specific* - Accès à des restaurants spécifiques
// - *restaurant.access_own* - Accès à son restaurant (par défaut)
// - *restaurant.edit* - Modifier les informations du restaurant
// - *restaurant.advanced_settings* - Gérer les paramètres avancés (paiement, impression, réglages...)
// Module: order
// - *order.view* - Voir les commandes
// - *order.edit* - Modifier les commandes
// Module: statistics
// - *statistics.view* - Voir les statistiques
// Module: catalog
// - *catalog.view* - Voir les produits
// - *catalog.manage_availability* - Gérer la disponibilité des produits
// - *catalog.manage* - Gérer le catalogue (créer et modifier des familles et des produits)
// Module: billing
// - *billing.view* - Voir la facturation
// - *billing.manage* - Gérer la facturation
// Module: front
// - *front.site* - Gérer le Front Site
// - *front.cashregister* - Gérer le Front Caisse
// - *front.kiosk* - Gérer le Front Borne
// - *front.application* - Gérer le Front Application
// Module: user
// - *user.view* - Voir les utilisateurs
// - *user.edit* - Modifier les utilisateurs
// - *user.create* - Ajouter des utilisateurs
// Module: customer
// - *customer.view* - Voir les clients
// - *customer.edit* - Modifier les clients (bloquer/supprimer)
// Module: control
// - *control.access* - Accès au contrôle
// Module: blog
// - *blog.manage* - Gérer le blog
// Module: superadmin
// - *superadmin.restaurant.view* - Voir les restaurants
// - *superadmin.restaurant.edit* - Modifier les restaurants
// - *superadmin.restaurant.create* - Ajouter un restaurant
// - *superadmin.multishop.manage* - Gérer les multiboutiques
// - *superadmin.billing.manage* - Gérer la facturation
// - *superadmin.resources.manage* - Gérer les ingrédients / la banque des images / les spécialités / blog
// - *superadmin.statistics.view* - Voir les statistiques
// - *superadmin.restaurant.archived* - Anciens restaurants
// - *superadmin.profile.manage* - Gérer les profils et les permissions
// Module: commercial
// - *commercial.pipeline* - Pipeline
// - *commercial.settings* - Réglages
// - *commercial.statistics* - Statistiques
// - *commercial.manage_team* - Gérer les commerciaux
// ========================================
// Liste des profils avec exemples de permissions
// ========================================
// PROFILS GLOBAUX (gérés par le SuperAdmin)
// ==========================================
// SuperAdmin
// - *restaurant.access_all*
// - Accès à toutes les fonctionnalités
// Opérateur de saisie
// - *restaurant.access_all*
// Commercial
// - *restaurant.access_all*
// - *commercial.pipeline*
// Contrôleur
// - *restaurant.access_all*
// - *control.access*
// PROFILS SPÉCIFIQUES AU RESTAURANT
// ==================================
// Admin
// - *restaurant.access_own*
// - *statistics.view*
// Gestionnaire
// - *restaurant.access_own*
// Employé
// - *restaurant.access_own*
// Livreur
// - *restaurant.access_own*
// - *order.view*
// - *order.edit*
-
Migration Approach: incrementally, starting with a solid foundation (profiles, permissions), preserving existing users, and prioritizing critical apps first.
Step-by-Step Migration PlanFrom numeric roles to a robust permission system
Phase 1: Core Setup (Profiles, Permissions, Pivot Tables)1. Create the New Tables
-
Migrate schema for:
profiles
permissions
permission_profile
- Add
profile_id
tousers
table
2. Seed Default Profiles
[ ['name' => 'SuperAdmin', 'is_system' => true, 'is_default' => true], ['name' => 'Commercial', 'is_system' => true], ['name' => 'Opérateur de saisie', 'is_system' => true], ['name' => 'Admin', 'is_system' => true], ['name' => 'Gestionnaire', 'is_system' => true], ['name' => 'Employé', 'is_system' => true], ['name' => 'Contrôle', 'is_system' => true], ]
3. Create Permission Management UI
-
Admin panel section:
- Add / Edit
profiles
- Add / Edit
permissions
- Assign permissions to profiles
- Add / Edit
4. Map
users.role
(numeric) to Newprofiles
- Create a migration or script:
$mapping = [ 1 => 'Opérateur de saisie', // if name == saisie 2 => 'Admin', 3 => 'Employé', 4 => 'Commercial', 5 => 'Contrôle' ]; foreach (User::all() as $user) { if ($user->role == 1 && $user->name == 'saisie') { $profileName = 'Opérateur de saisie'; } else { $profileName = $mapping[$user->role] ?? 'Employé'; } $user->profile_id = Profile::where('name', $profileName)->first()->id; $user->save(); }
- Leave SuperAdmin, Commercial, and Opérateur manually editable for accuracy.
Phase 2: Integrate App-Level Permissions5. Define & Seed App Permissions
Apps: Cashregister, Kiosk, Kitchen, Queue Board, Mobile App Example permission keys:
[ ['key' => 'kitchen_app.access', 'description' => 'Access Kitchen App'], ['key' => 'cashregister_app.access', 'description' => 'Access Cash Register'], ['key' => 'queue_board.access', 'description' => 'Access Queue Board'], ['key' => 'kiosk_app.access', 'description' => 'Access Kiosk'], ['key' => 'mobile_app.access', 'description' => 'Access Mobile App'], ]
6. Assign to Profiles
- Admin: assign
cashregister_app.access
,kitchen_app.access
, etc. - Employé: only
kitchen_app.access
,queue_board.access
, etc. - SuperAdmin: bypass check (
is_default = true
)
7. Update App Gateways/Middleware
Wherever app access is controlled (mobile, POS, kitchen), change logic:
if (!$user->hasPermission('kitchen_app.access')) { abort(403); }
Phase 3: Progressive Refactor of Hardcoded Permissions8. Audit Hardcoded Logic
Search your codebase for:
$user->role == 1 Gate::allows('admin')
Create a checklist of files/routes/controllers that need updating.
9. Gradually Replace with Permission Checks
Examples:
// BEFORE if ($user->role == 2) { ... } // AFTER if ($user->hasPermission('menus.edit')) { ... }
Do this module by module, starting with:
- Site settings
- Menu/product management
- Orders
- Stats
Phase 4: Cleanup and Finalization10. Deprecate the Old
role
Field- Keep it temporarily for reference.
- Once confident, remove or rename it (e.g.,
legacy_role
) to avoid confusion.
11. Add Profile/Permission Audit Logs (optional)
- Log when permissions are added/removed for traceability.
Migration Order SummaryPhase Task Priority 1 DB & UI for profiles
,permissions
, link to users Immediate2 Seed & assign app-related permissions Immediate3 Update kitchen, kiosk, POS, queue board apps Immediate4 Gradually refactor other app areas Ongoing5 Remove legacy role
logic Final stepEdited by Jamal Arradi -
-
Seeder for ProfilesDB::table('profiles')->insert(['name' => 'SuperAdmin', 'is_default' => true, 'created_at' => now(), 'updated_at' => now()]); DB::table('profiles')->insert(['name' => 'Commercial', 'is_default' => false, 'created_at' => now(), 'updated_at' => now()]); DB::table('profiles')->insert(['name' => 'Opérateur de saisie', 'is_default' => false, 'created_at' => now(), 'updated_at' => now()]); DB::table('profiles')->insert(['name' => 'Admin', 'is_default' => false, 'created_at' => now(), 'updated_at' => now()]); DB::table('profiles')->insert(['name' => 'Employé', 'is_default' => false, 'created_at' => now(), 'updated_at' => now()]); DB::table('profiles')->insert(['name' => 'Contrôle', 'is_default' => false, 'created_at' => now(), 'updated_at' => now()]);
Seeder for App-Level PermissionsDB::table('permissions')->insert(['key' => 'kitchen_app.access', 'description' => 'Access Kitchen App', 'module' => 'Kitchen App', 'created_at' => now(), 'updated_at' => now()]); DB::table('permissions')->insert(['key' => 'cashregister_app.access', 'description' => 'Access Cash Register', 'module' => 'Cash Register', 'created_at' => now(), 'updated_at' => now()]); DB::table('permissions')->insert(['key' => 'queue_board.access', 'description' => 'Access Queue Board', 'module' => 'Queue Board', 'created_at' => now(), 'updated_at' => now()]); DB::table('permissions')->insert(['key' => 'kiosk_app.access', 'description' => 'Access Kiosk', 'module' => 'Kiosk', 'created_at' => now(), 'updated_at' => now()]); DB::table('permissions')->insert(['key' => 'mobile_app.access', 'description' => 'Access Mobile App', 'module' => 'Mobile App', 'created_at' => now(), 'updated_at' => now()]);
-
Migration Plan forRoleChecker
Middleware to Permission-Based SystemYou’re currently using numeric
role
values (1, 2, 3...
) to control access in your middleware and routes. Since you're moving to profiles and permission keys, here's how to gradually update this without breaking anything:
1. Create a New Middleware:PermissionChecker
This will be the modern replacement for
RoleChecker
.// app/Http/Middleware/PermissionChecker.php namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\Auth; class PermissionChecker { public function handle($request, Closure $next, ...$permissions) { $user = Auth::user(); // Optional shortcut for superadmin profiles if ($user->profile && $user->profile->is_default) { return $next($request); } foreach ($permissions as $permission) { if ($user->hasPermission($permission)) { return $next($request); } } // OR use this if you want to pass one permission n handle function // Pseudo-code pour le middleware if ($user->profile->is_default || $user->hasPermission($permission)) { return $next($request); } return redirect('/'); } }
Register Middleware inapp/Http/Kernel.php
'permission' => \App\Http\Middleware\PermissionChecker::class,
2. Update theUser
ModelAdd a
hasPermission()
helper to check permission via profile:public function profile() { return $this->belongsTo(Profile::class); } public function hasPermission($key) { return $this->profile ? $this->profile->permissions()->where('key', $key)->exists() : false; }
3. Use New Middleware in Routes (Gradually)Instead of:
Route::group(['middleware' => ['auth', 'roleChecker:1,2']], function () {
Use:
Route::group(['middleware' => ['auth', 'permission:orders.edit']], function () {
Or for apps:
Route::group(['middleware' => ['auth', 'permission:cashregister_app.access']], function () {
You can gradually migrate route groups and individual routes this way while keeping
RoleChecker
intact for legacy routes.
4. KeepRoleChecker
Until Fully MigratedYou don't need to delete or modify your existing
RoleChecker
middleware yet. Just start migrating new or critical routes (e.g., app access) toPermissionChecker
.Once you're confident everything is covered by permissions:
- Remove
roleChecker
- Rename
role
tolegacy_role
or drop it
Edited by Jamal Arradi - Remove
-
You're not just protecting routes, but deciding who is allowed to log in to a specific external app (like Kiosk or Kitchen) using shared API authentication.
What You Need: App-Level Access GateThis means after login (via
auth:api
), you must check permission before returning any data. So even if a user has a valid token, they should only access the app if they have the right permission.
Step-by-Step Solution 1. Use the Permission Middleware for APICreate a middleware like
CheckAppAccess
:// app/Http/Middleware/CheckAppAccess.php namespace App\Http\Middleware; use Closure; use Illuminate\Support\Facades\Auth; class CheckAppAccess { public function handle($request, Closure $next, $appKey) { $user = Auth::guard('api')->user(); if (!$user) { return response()->json(['message' => 'Unauthorized'], 401); } // Bypass check for SuperAdmin (default profile) if ($user->profile && $user->profile->is_default) { return $next($request); } // Check if user has the required permission if (!$user->hasPermission($appKey)) { return response()->json(['message' => 'Unauthorized for this application'], 403); } return $next($request); } }
2. Register ItIn
Kernel.php
under$routeMiddleware
:'checkAppAccess' => \App\Http\Middleware\CheckAppAccess::class,
3. Apply It to External App RoutesUpdate your
api.php
like this:// Kitchen App Route::group([ 'middleware' => ['auth:api', 'checkAppAccess:kitchen_app.access'], 'prefix' => 'kitchen-app' ], function () { Route::get('orders', 'kitchenAppApi\KitchentController@pendingOrders'); // other routes... }); // Queue Board Route::group([ 'middleware' => ['auth:api', 'checkAppAccess:queue_board.access'], 'prefix' => 'queue-board' ], function () { Route::get('orders', 'QueueBoard\QueueBoardController@getPendingOrders'); // other routes... });
Now only users with the corresponding permission (e.g.kitchen_app.access
) can access kitchen app routes — even if they have a valid token.
Optional: Control Login Behavior ItselfIf you want to block login completely (i.e., prevent token issuance) for unauthorized users:
- Hook into your API login logic (
LoginController
orPassport
/JWT layer) - Check permission before issuing the token
- But usually it's simpler to issue the token and restrict usage via middleware
- Hook into your API login logic (