I'm making an app that uses jwt as authentication system ,
when I try to update my Category model the policy always returns 403 unauthorized,
I'm using apiResource to crud my model.
my code
in api.php:
Route::apiResource('category', CategoryController::class);
in CategoryController.php:
public function update(Request $request, $id)
{
// print_r($request->all());
$validator = Validator::make(
$request->all(),
[
'name' => 'required|min:2|unique:categories,name,' . $request->id,
'description' => 'required|min:1',
],
[
"name.unique" => "اسم الصنف مستخدم مسبقا",
"name.required" => "اسم الصنف مطلوب",
"name.min" => "اسم الصنف يجب أن يحتوي على حرفين على الأقل",
"description.required" => "وصف الصنف مطلوب",
]
);
if ($validator->fails()) {
return response()->json(['errors' => $validator->messages(), 'status' => 422], 200);
}
$category = Category::find($id);
$category->name = $request->name;
$category->description = $request->description;
$category->save();
return response()->json([
"message" => "تم تحديث الصنف",
"status" => 200
], 200);
}
in CategoryPolicy.php:
public function update(User $user, Category $category)
{
return $category->user_id === $user->id;
}
It seems like the request is not even reaching the update method in CategoryPolicy.php
because even if the method always returning true it's not working :
public function update(User $user, Category $category)
{
return true;
}
any way the viewAny method is working as expected.
I'm using axios to fetch and update data and I'm sending the request with the bearer token and every thing is working ok except the issue above.
In CategoryController.php, instead of injecting $id:
public function update(Request $request, $id)
Try injecting the type-hinted model instance:
public function update(Request $request, Category $category)
And remove the find() command:
//$category = Category::find($id);
When generating new controllers, you can also use this artisan command to include type-hinted models in the function arguments.
php artisan make:controller CategoryController --api --model=Category
It's hard to see what is going wrong because it can also be the middleware and JWT token. What you could do is in your update method check if the user is logged in, add the following as the first line in the method. If false please check your JWT implementation
dd(auth()->check());
I would also suggest clean up your controller:
class CategoryController
{
/**
* CateogryController constructor.
*/
public function __construct()
{
$this->authorizeResource(Category::class); // if your are using CRUD, validate like this
}
/**
* Update specific resource.
*
* #param Category $category
* #param Request $request
* #return \Illuminate\Http\JsonResponse
*/
public function update(Category $category, CategoryRequest $request): JsonResponse
{
// notice the model route binding.
$this->authorize('update', $category); // If you only have update method, but remove the __construct.
$category->update([
'name' => $request->get('name'),
'description' => $request->get('description')
]);
return response()->json(['message' => 'تم تحديث الصنف']); // take the 200 from the headers, not add it in as text.
}
}
Your request looks similar to this:
class CategoryRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true; // you could consider to validate the user->category relation. I like it more separated and put it in a separated policy.
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'name' => 'required|min:2|unique:categories,name',
'description' => 'required|min:1',
];
}
/**
* #return string[]
*/
public function messages()
{
return [
"name.unique" => "اسم الصنف مستخدم مسبقا",
"name.required" => "اسم الصنف مطلوب",
"name.min" => "اسم الصنف يجب أن يحتوي على حرفين على الأقل",
"description.required" => "وصف الصنف مطلوب",
];
}
}
And your policy like:
class CategoryPolicy
{
use HandlesAuthorization;
/**
* Determine if the user can update category resource.
*
* #param User $user
* #param Category $category
* #return bool
*/
public function update(User $user, Category $category): bool
{
return $user->categories()->where('id', $category->id)->exists(); // or somthing like this.
}
}
Related
I am using Request validation to validate the user's input.
This is UpdateUser:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
class UpdateUser extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return Gate::allows('update-user');
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
$user_id = Arr::get($this->request->get('user'), 'id');
return [
'user.firstname' => 'required|string|max:255',
'user.lastname' => 'required|string|max:255',
'user.email' => "required|string|email|max:255|unique:users,email,{$user_id}",
'user.password' => 'sometimes|nullable|string|min:4|confirmed',
];
}
}
As you can see, there is some update-specific stuff happening:
The authorize() method checks whether the user is allowed to update-user and inside the rules I am excluding the row of the current user from being unique:
'user.email' => "required|string|email|max:255|unique:users,email,{$user_id}",
As I would like to merge UpdateUser and StoreUser, what would be the most efficient and readable way to determine, whether I am updating or saving?
This is my current approach:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Gate;
class UpdateUser extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
if($this->isUpdating())
{
return Gate::allows('update-user');
}
return Gate::allows('create-user');
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
if($this->isUpdating()){
$user_id = Arr::get($this->request->get('user'), 'id');
return [
...
];
}
return [];
}
/**
* #return bool
*/
protected function isUpdating(){
return $this->isMethod('put') || $this->isMethod('patch');
}
}
I am wondering if I may extend the FormRequest class and provide isUpdating() by default.
Your update and store method are not the same request type, you have PUT and PATCH method on your request instance, so you can check the request type as like :
switch ($request->method()) {
case 'PATCH':
// do anything in 'patch request';
break;
case 'PUT':
// do anything in 'put request';
break;
default:
// invalid request
break;
}
I learnt about a new approach to validation some time ago using separate validator class and I kinda like it a lot. Let me show you
Create a directory Validators and a class inside UserValidator
class UserValidator
{
public function rules(User $user)
{
return [
'user.firstname' => [
'required',
'string',
'max:255',
],
'user.lastname' => [
'required',
'string',
'max:255',
],
'user.email' => [
$user->exists ? 'sometimes' : null,
'required',
'string',
'email',
'max:255',
Rule::unique('users', 'email')->ignore($user->exists ? $user->id : null)
],
'user.password' => [
$user->exists ? 'sometimes' : null,
'required',
'string',
'min:8'
],
];
}
public function validate(array $data, User $user)
{
return validator($data, $this->rules($user))
//->after(function ($validator) use ($data, $user) {
// Custom validation here if need be
//})
->validate();
}
}
Then authorization can be done in Controller
class UserController
{
use AuthorizesRequests;
/**
* #param Request $request
*/
public function store(Request $request)
{
$this->authorize('create_user', User::class);
$data = (new UserValidator())->validate(
$request->all(),
$user = new User()
);
$user->fill($data)->save();
}
/**
* #param Request $request
* #param \App\user $user
*/
public function update(Request $request, User $user)
{
$this->authorize('update_user', $user);
$data = (new UserValidator())->validate(
$request->all(),
$user
);
$user->fill($data)->save();
}
}
This was proposed and explained by (twitter handle) #themsaid
this is PostsRequest.php in http/request:
<?php
namespace App\Http\Requests;
use App\Post;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class PostsRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'title' => ['required','max:255', Rule::unique('posts')->ignore($this->id)],
'slug' => ['required', Rule::unique('posts')->ignore($this->id),],
'content' => 'required',
'type' => 'required|in:blog,download,page',
'status' => 'required',
];
}
}
and this is edit() method in PostController.php
public function update(PostsRequest $request, $id)
{
$validated = $request->validated();
$validated['user_id'] = auth()->user()->id;
$post = Post::find($id)->fill($validated);
$post->save();
return redirect()->action('PostController#index');
}
Problem: show error in update page that this value is already exists.
who to resolve problem unique fields in edit form?
Problem Solved
change:
Rule::unique('posts')->ignore($this->route('id'))
with:
Rule::unique('posts')->ignore($this->route('post'))
If you're wanting to resolve the $id from the route then you can use the route() method in your request class e.g.
Rule::unique('posts')->ignore($this->route('id'))
I'm trying to authorize whether a user is allowed to invite other users.
InvitedUserController
public function store(InvitedUserRequest $request)
{
$data = $request->all();
$data['user_id'] = auth()->user()->id;
$data['account_id'] = $request->session()->get('account_id');
InvitedUser::create($data);
}
I created a FormRequest class to handle validation:
class InvitedUser extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize(Request $request)
{
$account = Account::find($request->session()->get('account_id'));
return $this->user()->can('manageUsers', $account);
}
/**
* Validation error message
*/
public function messages() {
return [
'max' => 'You may only enter up to :max characters'
];
}
public function invalid() {
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|max:255|email',
'role_id' => 'required|exists:roles,id',
];
}
}
Then my policy to handle authorization:
class InvitedUserPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can view any models.
*
* #param \App\User $user
* #return mixed
*/
public function manageUsers(User $user, Account $account)
{
dd('test');
$role = $user->roles()->wherePivot('account_id', $account->id)->first();
return $role->manage_users;
}
}
I registered the policy:
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
'App\InvitedUser' => 'App\Policies\InvitedUserPolicy'
];
For some reason, that dd() call doesn't even occur. So it's not reaching my policy and all requests are returning unauthorized.
even if i changed my policy to return true
public function manageUsers(User $user, Account $account)
{
return true;
}
I would still get unauthorized
How Can I call my policy from a FormRequest? Why is this not working?
So, while this is confusing to me, I've figured out the issue.
I needed to change my registered policies to the following:
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
'App\Account' => 'App\Policies\InvitedUserPolicy'
];
It's using the Account model instead of InvitedUser model. I think because that's what I'm passing in as a model?
im having problems with error handler in my backend laravel api, don't show error message from validation.
routes/api
<?php
Route::group([
'middleware' => 'api',
], function ($router) {
Route::post('access/sign-up', 'AuthenticationController#signUp');
});
AuthenticationController
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;
use App\Http\Requests\SignUpRequest;
use Illuminate\Http\Request;
use App\User;
class AuthenticationController extends Controller
{
/**
* Create a new AuthenticationController instance.
*
* #return void
*/
public function __construct()
{
$this->middleware('auth:api', ['except' => ['signUp']]);
}
/**
*
*
* #return \Illuminate\Http\JsonResponse
*/
public function signUp(SignUpRequest $request)
{
//User::create($request->all());
return response()->json([
'data' => 'Usuario creado correctamente.'
]);
}
}
SignUpRequest
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class SignUpRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:users',
'firstName' => 'required',
'password' => 'required',
];
}
}
The thing is that when by postman i send password in blank or not send mail for example it send me to main page insted of sending me a json with all the errors like before. I don't know what to do.
I tryed with custom message in SignUpRequest, but that's not the problem, think some kind of redirect in error handler.
You need to check just one setting must add Accept Type in header data in Postman
Accept : application/json
that is work in my case...
Just add that code to SignUpRequest
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json($validator->errors(), 422));
}
Using the Form request; If validation fails, a redirect response will be generated to send the user back to their previous location. That's why you are redirecting back to the page instead of the response JSON.
Laravel have one protected method "failedValidation" method. Overwrite this method in your form request class.
protected function failedValidation(\Illuminate\Contracts\Validation\Validator $validator)
{
throw new \Illuminate\Validation\ValidationException(response()->json($validator->errors(), 422));
}
Update for laravel 8, the first parameter in ValidationException is the Validator class:
protected function failedValidation(Validator $validator)
{
throw new ValidationException($validator, response()->json($validator->errors(), 422));
}
TL;DR Add the following header: Accept: application/json
As mentioned by a few users over here, by default, when a Form Request fails, the application will attempt to redirect the user back to their previous location. That's why you're receiving a webpage instead of a JSON response.
We can also override the "failedValidation" method. But I would recommend setting the following header on the request Accept: application/json.
Laravel will return the appropriate response based on Accept header. For application/json Laravel will return a JSON response with form errors.
Simply, in your App\Exceptions\Handler, you have to register your custom exception handling in register() method:
public function register() {
$this->renderable( function ( ValidationException $ex, $request ) {
$response = [
'ErrorCode' => 'my_error_code',
'ErrorMessage' => $ex->validator->errors()
];
return response()->json( $response );
} );
}
add this method to your formRequest class. It works in laravel 9
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Http\Exceptions\HttpResponseException;
class PostRequest extends FormRequest
{
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(response()->json([
'success' => false,
'message' => 'Validation errors',
'data' => $validator->errors(),
]));
}
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
switch ($this->method()) {
case 'GET':
case 'DELETE':{
return [];
}
case 'POST':{
return [
'title' => 'string|unique:posts|required',
'body' => 'required',
'image' => 'string|nullable',
];
}
case 'PUT':{
return [
'title' => 'string|unique:posts|required',
'body' => 'required',
'image' => 'string|nullable',
];
}
}
}
}
Just add that code to App\Exceptions\Handler:
public function render($request, Exception $exception)
{
if ($exception instanceof ValidationException) {
return response()->json([
'status' => false,
'error' => [
'type' => 'form',
'code' => 400,
'description' => 'Bad request.',
'detail' => $exception->validator->errors()
]
], 422);
}
return parent::render($request, $exception);
}
Here is my validation request :rules
<?php
namespace App\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Support\Facades\Auth;
class UpdateCommentRequest extends Request {
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize() {
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules() {
$user = Auth::user()->id;
return [
'comment' => 'required|between:15,600',
'projectID' => "required|exists:project_group,project_id,user_id,$user|numeric",
'order' => "required|numeric",
'level' => "required|numeric"
];
}
}
And in my model I have like this:
public function apiUpdateComment(UpdateCommentRequest $request){
$comment = Comment::find(Input::get("order"));
$comment->text = Input::get('comment');
if($comment->save()){
return 'success';
}
}
This fileds I need to validate agins rules array:
array(
'comment' => Input::get('comment'),
'projectID' => Input::get('projectID'),
'order' => Input::get("order"),
'level' => Input::get("level"),
);
I need to check if all rules are ok and then update comment... Anyone can help?
public function apiUpdateComment(UpdateCommentRequest $request){
$comment = Comment::find($request->get("order"));
$comment->text = $request->get('comment');
if($comment->save()){
return 'success';
}
}
The logic behind the code:
A post request is send the the server and the route file sends it the the apiUpdateComment with all variables inside the $request. But before the code of the function is executed the validator checks the rules in your UpdateCommentRequest. If the test fails it will return errors. If it pass a comment with the id will be updated.