I have three models. I want to avoid that users can change the todo's from todolists belonging to other users.
class User extends Authenticatable
{
public function todolists()
{
return $this->hasMany('App\Todolist');
}
public function todos()
{
return $this->hasManyThrough('App\Todo', 'App\Todolist');
}
}
class Todolist extends Model
{
public function user()
{
return $this->belongsTo('App\User');
}
public function todos()
{
return $this->hasMany('App\Todo');
}
}
class Todo extends Model
{
protected $casts = [
'completed' => 'boolean',
];
public function todolist()
{
return $this->belongsTo('App\Todolist');
}
}
To avoid users can view other users' todolists and todo items, I have implemented the following:
public function getTodosForTodolist(Todolist $todolist)
{
if (Auth::user()->id == $todolist->user_id) {
$todos = Todo::where('todolist_id', $todolist->id )->get();
return view('todo/index', ['todos' => $todos);
}
else {
abort(403, 'Unauthorized action.');
}
}
Next step is to prevent that users can edit other users' todo items. Currently in the TodoController I have simply the following:
public function edit(Todo $todo)
{
if (Auth::user()->todos->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
}
This gives the following error:
Property [id] does not exist on this collection instance.
The error is because the current user has multiple todos. So I changed my code as follows.
public function edit(Todo $todo)
{
if (Auth::user()->todos->first()->id == $todo->todolist->id) {
return view('todo/edit', ['todo' => $todo]);
}
abort('403', 'Unauthorized action.');
}
This works but it just feels very wrong to do this as such.
What would be a better way to accomplish that users' can view/edit/delete items belonging to other users?
I suggest that you use policies for your Todo and TodoList models and a scope to restrict todos to one user to prevent duplicated code within your app:
class ToDoListPolicy
{
public function view(User $user, TodoList $post)
{
return $user->id === $todolist->user_id;
}
}
class ToDoPolicy
{
public function edit(User $user, Todo $toDo)
{
$toDo->loadMissing('todolist');
return $user->id === $toDo->todolist->user_id;
}
}
Register them in your AuthServiceProvider.php
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
TodoList::class => ToDoListPolicy::class,
Todo::class => ToDoPolicy::class
];
}
and then use them in your actions:
public function getTodosForTodolist(Todolist $toDoList)
{
$this->authorize('view', $toDoList);
$toDoList->loadMissing('todos');
return view('todo.index', ['todos' => $toDoList->todos);
}
class ToDoController extends Controller
{
public function edit(Todo $toDo)
{
$this->authorize('edit', $toDo);
return view('todo.edit', compact('toDo'));
}
}
And a scope to restrict the query to a specific user:
class Todo extends Model {
// ...
public function scopeByUser(Builder $query, ?User $user = null)
{
if (! $user) {
$user = Auth::user();
}
$query->whereHas('todolist', function (Builder $toDoListQuery) use ($user) {
$toDoListQuery->where('user_id', $user->id);
});
}
}
Answer to your questions in the comments.
Q1: I had to put Auth::user()->can('view', $todolist); in an if-else clause for it to work. Guess this is the way it works?
Q2: what is the difference between $this->authorize('edit', $todo) and Auth::user()->can('edit', $todo)?
Sorry, that was a mistake on my side. Auth::user()->can() returns a boolean whereas $this->authorize() (which is a method of the AuthorizesRequests trait usually included in the BaseController) throws an exception if the authorization failed.
If you want to let each user work only with his/her own Todos then adding a Global Scope is what you are looking for. This implementation will let your application feel that Todos ( of users other than the logged one ) does not exist.
Global Scopes can be used for many models which means it will reduce boiler plate code.
https://laravel.com/docs/7.x/eloquent#global-scopes
Related
I have a scenario that needs two polymorphic relationship in one model. The main model is Calls which includes a Caller and Callee. Caller and Callee can be a User, Buyer or Seller. So I designed my table like following.
class Call extends Model
{
public function caller()
{
return $this->morphTo();
}
public function callee()
{
return $this->morphTo();
}
}
I created my User model like this
class User extends Authenticatable
{
public function buyers()
{
return $this->hasMany(Buyer::class);
}
public function outgoingCalls()
{
return $this->morphMany(self::class, 'caller');
}
public function incomingCalls()
{
return $this->morphMany(self::class, 'callee');
}
}
and created my Buyer and Seller models like,
class Buyer extends Model
{
public function user()
{
return $this->belongsTo(User::class, 'user_id');
}
public function outgoingCalls()
{
return $this->morphMany(self::class, 'caller');
}
public function incomingCalls()
{
return $this->morphMany(self::class, 'callee');
}
}
So, I want save a call when a User calls a Buyer like this
Auth::user()->buyers()->find($args['buyer_id'])->outgoingCalls()->save()
and save a call when a Buyer or Seller calls to User
Buyer::find(2)->user()->outgoingCalls()->save()
my problem is I couldn't make it work like I wanted so I had to do it like this,
$userCall = new UserCall([
'caller_id' => Auth::user()->id,
'caller_type' => User::class,
'status' => 1,
'started_at' => '2020-06-24 01:00:00',
'ended_at' => '2020-06-24 02:00:00',
]);
if (Auth::user()->buyers()->find($args['buyer_id'])->incomingCalls()->save($userCall)) {
return [
'status' => 'SUCCESS',
'message' => 'User Call successfully saved',
];
}
could you please help me figure out this problem ? Thank you in advance !
The User model has an isAdmin() function to check if the user is an administrator. What to do next?
The best way is to use default laravel LoginController located under App\Http\Controllers\Auth\LoginController.
In that controller you can override authenticated method that is injected from AuthenticatesUsers trait, by simply adding that method in LoginController:
* #param Request $request
* #param $user
*/
protected function authenticated(Request $request, $user)
{
if ($user->isAdmin()) {
return redirect(route('admin-dashboard'));
//redirect to desired place since user is admin.
}
}
Best practique is whit roles, and you add role on your Routes::middleware,
Route::group(['middleware' => ['auth', 'roles:admin']], function () {
//Your routes
});
Kernel.php
'roles' => Middleware\CheckRole::class,
Create middleware
namespace App\Http\Middleware;
use Closure;
class CheckRole
{
public function handle($request, Closure $next, ...$role)
{
if ($request->user()->hasAnyRole($role)) {
return $next($request);
}
return redirect(route('hour'));
}
}
create function on User model
public function authorizeRole($role)
{
if ($this->hasAnyRole($role)) {
return true;
}
return abort(401, 'Unauthorized.');
}
public function hasAnyRole($roles)
{
if (is_array($roles)) {
foreach ($roles as $role) {
if ($this->hasRole($role)) {
return true;
}
}
} else {
if ($this->hasRole($roles)) {
return true;
}
}
return false;
}
public function hasRole($role)
{
if ($this->role()->where('name', $role)->first()) {
return true;
}
return false;
}
public function role()
{
return $this->belongsTo('App\Role')->withDefault();
}
And Role model
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
public function user()
{
return $this->hasMany('App\User');
}
}
Is more code, but best way for this action
I was trying to make tests for my auth routes. For password reset route I am trying to make in which I am faking the notification module of laravel and asserting as per the docs.
This is my test file
public function testUserReceivesAnEmailWithAPasswordResetLink()
{
$this->withoutExceptionHandling();
Notification::fake();
$user = factory(User::class)->make();
$response = $this->post($this->passwordEmailPostRoute(), [
'email' => $user->email,
]);
$this->assertNotNull($token = DB::table('password_resets')->where('email', $user->email));
Notification::assertSentTo($user, PasswordReset::class);
}
While I am running this, I am getting notification was not sent error.
My User model is like this:
use Notifiable, HasApiTokens, SoftDeletes, Uuidable, Switchable, ResourceMapper;
public function role()
{
return $this->belongsTo('App\Models\Role');
}
public function company()
{
return $this->belongsTo('App\Models\Company');
}
public function AauthAccessToken()
{
return $this->hasMany('App\Models\OauthAccessToken');
}
public function isRole($role)
{
return $this->role->uuid == $role;
}
public function sendPasswordResetNotification($token)
{
$this->notify(new PasswordReset($token));
}
public function resource()
{
return $this->morphTo();
}
I can't figure whats the exact problem.
I would like to get the model relations, in array;
My model look like:
class User extends Model
{
public function profile() {
return $this->haOne(Profile::class);
}
public function settings() {
return $this->morphOne(Settings::class, 'settingsable');
}
public function addresses() {
return $this->hasMany(Addresses::class);
}
}
And my code:
$user = User::with(['profile', 'settings', 'addresses'])->find(1);
$user->getRelations(); // return ['profile', 'settings', 'addresses'];
If I have more then 10 relation, I don't want to list all.
I would like to get like this:
$relations = ['profile', 'settings', 'addresses'];
Is this posible?
You could try adding a scope to the model, and so, you have to only write them once.
class User extends Model
{
public function profile() {
return $this->haOne(Profile::class);
}
public function settings() {
return $this->morphOne(Settings::class, 'settingsable');
}
public function addresses() {
return $this->hasMany(Addresses::class);
}
public function scopeWithRelations(Builder $query){
return $query->with([...]);
}
}
$users = User::withRelations()->get();
This way you only have to write them once there, and everywhere in the code you'll use the scope.
Not exactly 100% what you're asking, but this could be a solution.
I have a question about filtering a controller and its actions for multiple user roles. Lets say i have a controller named MyController :
public class MyController extends \BaseController
{
public static function index()
{
}
public static function show()
{
}
public static function create()
{
}
public static function store()
{
}
public static function other()
{
}
}
And i have 2 filters for each roles, named admin and staff :
Route::filter('admin', function()
{
// Lines of code to get role
if($role != 'admin') return View::make('errors.401');
});
Route::filter('staff', function()
{
// Lines of code to get role
if($role != 'staff') return View::make('errors.401');
});
Then, i'm trying to use beforeFilter on the constructor of MyController :
public function __construct()
{
$this->beforeFilter('admin', ['only' => ['index', 'show', 'create', 'store']]);
$this->beforeFilter('staff', ['only' => ['index', 'show']]);
}
When I added the first beforeFilter, it works as I expected (when I logged in to my application as staff, I cannot access the index, show, create, and store methods). But when I added the second filter and logged in as staff again, I cannot access the index and show actions, which is I expected to be accessible by staff.
My questions are, is it possible to define filters for multiple roles in the constructor of a controller? (In this case, I want to make action index and show accessible by admin and staff, but create and store only accessible by admin) And if it is possible, how could I achieve that?
Thanks.
First you should make a controller that should handle access control ... as below
Acl Controller
class ACLController extends \BaseController {
/**
* admin access control list
* #return array
*/
private function adminACL() {
return array(
'users' => array(
'users',
'users.show',
//similar access list for admin users
),
);
}
/**
* staff access control list
* #return array
*/
private function staffACL() {
return array(
'staff' => array(
'staff',
'staff.index',
//similar access list for staff user
),
);
}
//Method that check access of related user
/**
* check access level
* #param string $value
* #return boolean
*/
public function hasAccessLevel($value) {
$user = //get user role here
if ($user->roles == 'staff') {
return TRUE;
} elseif ($user->roles == 'admin') {
$newAcl = array();
foreach ($this->adminACL() as $aclBreak) {
foreach ($aclBreak as $acl) {
$newAcl[] = $acl;
}
}
if (!in_array($value, $newAcl)) {
return FALSE;
} else {
return TRUE;
}
} else {
$newAcl = array();
foreach ($this->staffACL() as $aclBreak) {
foreach ($aclBreak as $acl) {
$newAcl[] = $acl;
}
}
if (!in_array($value, $newAcl)) {
return FALSE;
} else {
return TRUE;
}
}
}
}
Filter the access route...
Route::filter('hasAccess',function($route,$request,$value){
try{
$Routeacl = new App\Controllers\ACLController();
if(!$acl->hasAccessLevel($value))
{
return Redirect::to('admin/dashboard')->withErrors(array(Lang::get('en.user_noaccess')));
}
}catch(\Exception $e){
echo $e->getMessage();
}
});
And then in your route just check if it has access
Route::get('/', array('as' => 'index', 'before' => 'hasAccess:index', 'uses' => 'MyController#Index'));
Happy coding :)
I assume you have Admin can access all feature, and staff can access everything except "show"
This is the controller
class MyController extends \BaseController
{
public function __construct(){
$this->beforeFilter('admin', ['only' => ['show']]);
}
public function index()
{
echo "index";
}
public function show()
{
echo "show";
}
}
See in your last post, you are using public class, I believe in PHP you will just need class, in function better don't use static.
Here is the filters.php
Route::filter('admin', function()
{
// Lines of code to get role
if($role != 'admin') return "This is only for admin";
});
In the routes.php
Route::get("/my", "MyController#index");
Route::get("/show", "MyController#show");
Then try to login as admin, you will can access "index" and "show"
Then try to login as staff, you will can access "index" but cannot access "show"
Is an admin always a staff member? If so - you could just do this:
Route::filter('staff', function()
{
// Lines of code to get role
if(($role != 'staff') && ($role != 'admin'))return View::make('errors.401');
});