Skip to content
Snippets Groups Projects

Robust Role & Permission Architecture for Multi-Tenant Restaurant Management System

  • Clone with SSH
  • Clone with HTTPS
  • Embed
  • Share
    The snippet can be accessed without any authentication.
    Authored by Jamal Arradi

    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.

    Edited
    database-schema-and-data.sql 4.23 KiB
    // 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*
    permission-architecture.md 9.41 KiB
    • Migration Approach: incrementally, starting with a solid foundation (profiles, permissions), preserving existing users, and prioritizing critical apps first.


      :white_check_mark: Step-by-Step Migration Plan

      From numeric roles to a robust permission system


      :puzzle_piece: Phase 1: Core Setup (Profiles, Permissions, Pivot Tables)

      1. Create the New Tables

      • Migrate schema for:

        • profiles
        • permissions
        • permission_profile
        • Add profile_id to users 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

      4. Map users.role (numeric) to New profiles

      • 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.

      :puzzle_piece: Phase 2: Integrate App-Level Permissions

      5. 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);
      }

      :puzzle_piece: Phase 3: Progressive Refactor of Hardcoded Permissions

      8. 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

      :puzzle_piece: Phase 4: Cleanup and Finalization

      10. 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.

      :vertical_traffic_light:Migration Order Summary

      Phase Task Priority
      1 DB & UI for profiles, permissions, link to users :fire: Immediate
      2 Seed & assign app-related permissions :fire: Immediate
      3 Update kitchen, kiosk, POS, queue board apps :fire: Immediate
      4 Gradually refactor other app areas :white_check_mark: Ongoing
      5 Remove legacy role logic :white_check_mark: Final step
      Edited by Jamal Arradi
    • :small_blue_diamond: Seeder for Profiles

      DB::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()]);

      :small_blue_diamond: Seeder for App-Level Permissions

      DB::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()]);
    • :repeat: Migration Plan for RoleChecker Middleware to Permission-Based System

      You’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:


      :white_check_mark: 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('/');
          }
      }

      :pushpin: Register Middleware in app/Http/Kernel.php

      'permission' => \App\Http\Middleware\PermissionChecker::class,

      :white_check_mark: 2. Update the User Model

      Add 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;
      }

      :white_check_mark: 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.


      :white_check_mark: 4. Keep RoleChecker Until Fully Migrated

      You don't need to delete or modify your existing RoleChecker middleware yet. Just start migrating new or critical routes (e.g., app access) to PermissionChecker.

      Once you're confident everything is covered by permissions:

      • Remove roleChecker
      • Rename role to legacy_role or drop it
      Edited by Jamal Arradi
    • 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.


      :white_check_mark: What You Need: App-Level Access Gate

      This 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.


      :small_blue_diamond: Step-by-Step Solution

      :white_check_mark: 1. Use the Permission Middleware for API

      Create 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);
          }
      }

      :white_check_mark: 2. Register It

      In Kernel.php under $routeMiddleware:

      'checkAppAccess' => \App\Http\Middleware\CheckAppAccess::class,

      :white_check_mark: 3. Apply It to External App Routes

      Update 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...
      });

      :lock: Now only users with the corresponding permission (e.g. kitchen_app.access) can access kitchen app routes — even if they have a valid token.


      :brain: Optional: Control Login Behavior Itself

      If you want to block login completely (i.e., prevent token issuance) for unauthorized users:

      • Hook into your API login logic (LoginController or Passport/JWT layer)
      • Check permission before issuing the token
      • But usually it's simpler to issue the token and restrict usage via middleware
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Please register or to comment