My User.php
class User extends Authenticatable
{
public function sites()
{
return $this->hasMany(Site::class);
}
}
My Site.php
class Site extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
My routes.php
Route::resource('site', 'SiteController');
My SiteController.php
class SiteController extends Controller
{
public function edit(int $id)
{
$site = Auth::user()->sites()->find($id);
return view('site.edit', compact('site'));
}
}
How can I validate that the site belongs to user? I understand that in my case if site doesn't belong to user, $site variable will be null. But I want more declarative way, something like laravel requests, because I need the same check in show, update, and destroy methods. But I cannot use laravel request, because checking is something like this
$siteId = Route::current()->param('site');
$ids = Auth::user()->sites()->pluck('id')->toArray();
$result = in_array($siteId, $ids);
Can anyone suggest how to achieve my goal?
Since I use laravel 5.2, solution with route filters is deprecated. Instead route filter we should use middleware.
app/Http/Middleware/RestrictPermission.php
class RestrictPermission
{
public function handle($request, Closure $next)
{
$siteId = Route::current()->parameter('site');
if (!Auth::user()->sites()->find($siteId)) {
abort(403);
}
return $next($request);
}
}
app/Http/Kernel.php
class Kernel extends HttpKernel
{
protected $routeMiddleware = [
'restrict.permission' => RestrictPermission::class,
];
}
SiteController.php
class SiteController extends Controller
{
public function __construct()
{
$this->middleware('restrict.permission', ['except' => [
'index', 'create', 'store',
]]);
}
public function edit(int $id)
{
$site = Auth::user()->sites()->find($id);
return view('site.edit', compact('site'));
}
}
Related
Route:
Route::controller(PublicController::class)->group(function () {
Route::get('/index', 'index')->name('public.index');
});
View:
index.blade.php
wrong_browser.blade.php
In controller, this way is ok:
class PublicController extends Controller
{
public function index(Request $request)
{
if(is_wrong_browser)
return view(public.wrong_browser);
return view('public.index');
}
}
But how can I return view from another function, like this, without making a new route:
class PublicController extends Controller
{
public function index(Request $request)
{
$this->CheckBrowser();
return view('public.index');
}
public function CheckBrowser()
{
if(is_wrong_browser)
return view(public.wrong_browser);
}
}
You can use the method redirect.
return redirect()->route('index');
You could use middleware which you either define globally, or on specific routes.
class CheckUserActive
{
public function handle($request, Closure $next)
{
// determine value of $is_wrong_browser
$is_wrong_browser = true;
if ($is_wrong_browser) {
return redirect()->route('is-wrong-browser-route');
}
return $next($request);
}
}
It is bad practice to return a view from middleware instead redirect your user to another route.
Alternatively, you could have a base Controller that your Controllers extend which has the checkBrowser function defined on it and the extending Controllers therefore have access to:
class WrongBrowserController extends \App\Http\Controllers\Controller
{
public function checkBrowser()
{
// determine value of $is_wrong_browser
$is_wrong_browser = true;
if ($is_wrong_browser)
{
return view('wrong-browser-view');
}
}
}
class PublicController extends WrongBrowserController
{
public function index(Request $request)
{
$this->checkBrowser();
return view('index');
}
}
I've been using backpack in Laravel but I want to replace action-domain-responder architecture with MVC.So I've created an Action which my route refers like below:
Route::get('post',[
'as' => 'post.index',
'uses' => 'Core\Post\Actions\ApiGetListOfPostsAction',
'operation' => 'list'
]);
class ApiGetListOfPostsAction extends BaseAction implements IAction
{
private $service;
public function __construct(ApiGetListOfPostsService $service)
{
$this->service = $service;
}
public function __invoke(Request $request): mixed
{
$data = $this->service->process();
return response()->json($data);
}
}
and my service has this code:
class ApiGetListOfPostsService extends CrudController
{
use ListOperation, CreateOperation, DeleteOperation, UpdateOperation;
public function setup()
{
CRUD::setModel(\App\Models\Post::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/post');
CRUD::setEntityNameStrings('post', 'posts');
}
protected function setupListOperation()
{
CRUD::column('title');
CRUD::column('content');
}
public function process()
{
return $this->index();
}
}
I've extended CrudController in my service class but I've got this error:
Call to a member function hasAccessOrFail() on null
which related to the ListOperation Trait and this code:
public function index()
{
$this->crud->hasAccessOrFail('list');
}
I need to send all requests to the Service class. How can I pass requests to the service class?
When I deleted middleware from CrudController I have no problem.
$this->middleware(function ($request, $next) {
$this->crud = app()->make('crud');
$this->crud->setRequest($request);
$this->setupDefaults();
$this->setup();
$this->setupConfigurationForCurrentOperation();
return $next($request);
});
I think your Action is missing something.
When using inheritance from a parent class, it might help to put this line in your constructor.
public function __construct(ApiGetListOfPostsService $service)
{
parent::__construct(); // <- Subclass constructor
$this->service = $service;
}
Doc: https://www.php.net/manual/en/language.oop5.decon.php
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
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'm trying to get selected language in my construct to use in any function in that class:
my route:
Route::group(['prefix' => 'admin', 'middleware' => ['AdminMiddleWare','auth','localization']], function(){
Route::get('/', 'AdminController#index')->name('admin.index');
});
My Middleware:
public function handle($request, Closure $next)
{
if (Session::has('locale') AND array_key_exists(Session::get('locale'), Config::get('languages'))) {
App::setLocale(Session::get('locale'));
}
else {
App::setLocale(Config::get('app.locale'));
}
return $next($request);
}
My controller :
public $lang;
public function __construct()
{
$this->lang = Language::where('lang','=',app()->getLocale())->first();
}
public function index()
{
$lang = $this->lang;
return $lang;
}
but i'm getting only the default locale;
but if i change the controller to this:
public function index()
{
$lang = Language::where('lang','=',app()->getLocale())->first();
return $lang;
}
it will work...
how to get in construct and use it in all functions??
In Laravel, a controller is instantiated before middleware has run. Your controller's constructor is making the query before the middleware has had a chance to check and store the locale value.
There are multiple ways you can set up to work around this - the important thing is to make the call after middleware runs. One way is to use a getter method on your controller:
class Controller
{
/**
* #var Language
*/
private $lang;
public function index()
{
$lang = $this->getLang();
// ...
}
private function getLang()
{
if ($this->lang) {
return $this->lang;
}
return $this->lang = Language::where('lang','=',app()->getLocale())->first();
}
}