Amir Hedieh
Amir Hedieh

Reputation: 1178

RESTful API different responses based on user roles

i'm using Laravel as my PHP framework. its a convention to put index show store ... functions in controllers.

i have 2 types of users(Admin & normal user). lets assume there is an Order(in restaurant) model and i want to implement index function for its controller. a user can have more than one Order.

what i need is that this function:

  • if admin is calling this API: returns all Orders
  • if normal user is calling this API: returns just Orders owned by this user

i searched but i couldn't find anything(tbh i didn't know what to search).

once i did this as below which i didn't like because it looks two different functions gathered in one:

if ($user->role == admin) {
       // fetch all orders
   } else if ($user->role == normal_user) {
       // just find user orders
     }

so my question is what's best approach to achieve what i want?

Upvotes: 7

Views: 4065

Answers (5)

Arne Burmeister
Arne Burmeister

Reputation: 20594

Such a REST API endpoint is typically a search allowing multiple filters, sorting and pagination. If so it is completly fine to apply different defaults for filters and also restrict filters to roles.

I would auto apply a filter user=currentUser for missing admin role and return a forbidden if a user without the admin role tries to apply a user filter for a different user.

With this approach you give admins also the functionality to search for offers of a specific user and you only need one search api to be used by the controller.

Upvotes: 2

mdexp
mdexp

Reputation: 3567

I had a similar question myself a while ago and ended up with this strange solution to avoid that if/else block.

Assumptions

I assumed the existence of an helper method in the User model called isNot($role) to verify the if the user's role matches or not the given one. This is just an example to give the idea of the check, but you should implement the condition as you like.

Second assumption I made is that each order has a user_id field which will reference the owner of that order though his id (FK of 1:N among user and order).

Implementation

public function index(Request $request)
{
    $orders = Order::query()
            ->when($request->user()->isNot('admin'), function ($query) use ($request) {
                return $request->user()->orders();
                // Or return $query->where('user_id', $request->user()->id);
            })
            ->paginate();

    return OrderResource::collection($orders);
}

The when method is the key here. Basically you call it like: when($value, $callback) and if $value is false the callback won't be executed, otherwise it will.

So for example, if the user is not an admin, you will end up executing this query: Order::paginate(); that would fetch all the order with pagination (note that you could swap paginate with get.

Otherwise, the callback is gonna be executed and you will execute the paginate method on $request->user()->orders(); (orders called like a method is still a query builder object, so you can call paginate on it). The query would be: $request->user()->orders()->paginate();

If you instead opted for the second solution in the callback you would basically add a where condition (filtering on the user_id of the orders) to the main scope to get only the user's orders. The query would be: Order::query()->where('user_id', $request->user()->id)->paginate();

Finally, to better control what's sent back as response I use Laravel's API Resource (and I really suggest you to do so as well if you need to customize the responses).

NOTE: The code might have syntax and/or logical errors as it was just an on the fly edit from production code, and it hasn't been tested, but it should give an overall idea for a correct implementation.

Upvotes: 1

Maxim Abdalov
Maxim Abdalov

Reputation: 559

You can create scope in Order class... For example you have field user_id in Order, for detect user

class Order 
{
 ...
 public function scopeByRole($query)
 {
   if (!Auth::user()->isAdmin())
     $query = $query->where('user_id', Auth::user()->id);
   return $query;
 }

}


in you controller just get all Orders with scope:

$orders = Order::byRole()->get();

it return you orders by you role

Also you need have in class User function for detect role, example

class User
{
  public function isAdmin()
  {
    // you logic which return true or false
  }
}

Upvotes: 0

Djellal Mohamed Aniss
Djellal Mohamed Aniss

Reputation: 1733

it would be better to include the if/else in your order modal like this:

class Order extends Model {
....
 static function fetchFor (User $user) : Collection
 {
   return $user->isAdmin() ? self::all() : self::where("user_id",$user->id); 
 } 
}

then you can call this method on your controller


public function index()
{
  return view('your-view')->with('orders',Order::fetchFor(Auth::user())->get())
}

Upvotes: 0

Jasper Helmich
Jasper Helmich

Reputation: 751

Why don't use an if statement?

You could make a scope on the model but then you'll still have an if.

What about this?

if ($user->role == admin) {    
    Order::all();
} else if ($user->role == normal_user) {
   $user->orders()->get();
}

Or make it an inline if

$user->role == admin ? Order::all() : $user->orders()->get();

IMO the best practice here is to make a different Admin/OrderController.php

Then with middleware check wat, the role of the user is, and then redirect them to the admin controllers.

Since you'll probably also want an update and delete, or other functions only accesible by an Admin

Upvotes: 1

Related Questions