1
votes

I've been stuck on this for 2 days now, and all the resolutions I looked for on StackOverflow and Laracasts proved inconclusive.

I am using Laravel 5.5 with jQuery and I do my testing in Firefox.

My AJAX GET calls are working fine, however, when I try and push an entry in my database, I get a 403 error.

My header does contain the CSRF token:

<meta name="csrf-token" content="{{ csrf_token() }}">

Models are created for every table called in my controller:

public function pushProfile(Request $request){
        $userid = \Auth::user()->id;
        $data = $request->message;
        $stat = \App\Character::where('owner', $userid)->first();
        $mess = \App\Message::firstOrCreate([
            'posterID' => $userid,
            'loc_x' => '0',
            'loc_y' => '0',
            'characterID' => $stat->id,
            'type' => 'profile'
        ]);
        $mess->content = $data;
        $mess->save();
        return response()->json(['success'=>'Message has been saved!']);
    }

Here is the AJAX call, it basically checks for my Quilljs Delta. This Deltais a JSON object formatting a message from WYSIWYG. Then every 5th second, it should try to push it to my database.

I know the Quilljs side works fine because my deltas show properly in my console. But the POST call itself doesn't seem to pass authentication for some reason? (This is just my guess, to me it seems like the only reason it would send a 403.)

setInterval(function() {
      if (change.length() > 0) {
        console.log('Saving changes', change);
        /* AJAX setup */
        $.ajaxSetup({
          headers: {
            'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
          }
        });
        $.ajax({
            headers: {
              'Content-Type':'application/json'
            },
            method: 'POST',
            url: '{{ url("/pushProfile") }}',
            data: {
              message:
              { 
                doc: JSON.stringify(quill.getContents())
              },
              _token: $('meta[name="csrf-token"]').attr('content')
            },
            dataType: 'JSON',
            error: function(jqXHR, textStatus, errorThrown) {
              console.log(JSON.stringify(jqXHR));
              console.log("AJAX error: " + textStatus + ' : ' + errorThrown);
            },
            success: function (data) { 
                $(".writeinfo").append(data.msg); 
                console.log("Success!!!");
            }
        });
        change = new Delta();
      }
    }, 5*1000);

To make sure the issue didn't come from CSRF, I went a bit overkill and, after trying to set up the token first in ajaxSetup, then in the AJAX data only, I just assigned it in both. None of these scenarios changed anything.

Of course, I assigned the 'Web' middleware on my post route to check for the above-mentioned CSRF token. The route I use is as follows:

Route::group(['middleware' => ['web']], function () {
  Route::post('/pushProfile','MessageSend@pushProfile')->name('pushProfile');
});

I also tried to assign the URL as:

url: '/pushProfile',

To no avail, unfortunately... This just returns a 404 instead of the 403 I currently have:

{
"readyState":4,
"responseText":"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\n<html>
    <head>\n<title>403 Forbidden</title>\n</head>
    <body>\n<h1>Forbidden</h1>\n
    <p>You don't have permission to access 
    /folder/public/{{ route(&quot;pushProfile&quot;) }}\n
    on this server.<br />\n</p>\n
    <hr>\n
    <address>Apache/2.4.35 (Win64) PHP/7.2.10 Server at localhost Port 80</address>\n
    </body>
    </html>\n",
"status":403,
"statusText":"Forbidden"
}

Did I miss anything? Thanks!

2
try url: '{{ route("pushProfile") }}',madalinivascu
Unfortunately, network monitoring still returns a 403 on url: '{{ route("pushProfile") }}', through my localhost. Thanks though :)mbuss
Does allow "post" method your route?FGDeveloper
My route is Route::post, and my controller calls "use Illuminate\Http\Request;". Or do you mean setting up Guards and Providers? I didn't see it as a requirement in any docs :/mbuss

2 Answers

2
votes

I found the solution, and oh god it was so simple I'm ashamed of myself:

I was calling the AJAX from a .js file, using the blade route. Moving the call into a blade file solved the whole issue, as blade routes are only rendered in .blade.php files...

I'm leaving the issue out there in case anyone stumbles upon the same trouble :)

0
votes

If someone stumbles upon this question and solution keep in mind that if you want to keep your blade templates clean and separate your logic properly into JS files you should create a routes.js file inside your project that holds an object with key-value pairs the same as your named routes in web.php file. I suggest placing the file in public/assets/js directory.

This way you only have to be mindful when changing your routes to change them in both places. Example of a routes.js and web.php file in my current project: routes.js

let routes = {
admDashboard: '/admin',
userOverview: '/admin/users/',
userSearch: '/admin/users/search',
}

web.php

Route::get('/admin', [AdminController::class, 'dashboard'])->name('admDashboard');
// users management
//Route::get('/admin/users/{id}', [AdminController::class,'userDetails'])->name('userDetails'); //noob way
Route::get('admin/users', [UserManagementController::class, 'userOverview'])->name('userOverview');  //proper way - controller expects User obj
Route::get('admin/users/search', [AdminController::class, 'userSearch'])->name('userSearch');

And finally the actual usage in an AJAX call:

import routes from '../../routes.js'
function getLatestPosts(callbacksArr) {
$.ajax({
    url: routes['postOverview'],
    type: 'GET',
    dataType: 'json',
    data: '',
    success: callbacksArr
});

}

The code does not fetch a proper url because '{{ url("/pushProfile") }}' is a Blade syntax and in Laravel 8 the proper way is {{ route('yourNamedRoute') }}. Your AJAX script doesn't know what this is, but a string. A workaround for using this route with Blade syntax is to use a href and generate from the backend your URL OR put your submission button in a form that has an

<form action="{{ route('yourNamedRoute') }}"></form>

This way when you call the AJAX function you can prevent the default behavior, grab the url from the action and pass this value by using jQuery:

let url = $("#my_form_id").prop('action');
ajaxFunctionThatDoesTheJob(url);

or even force the function to expect an object and allow named arguments like so:

function searchPosts({url: url, search: search=null,filter: filter=null, page: page=null,
                     callbacks: callbacksArr, request: request='GET'}) 

then we would call the function like in the example below and pass arbitrary number of arguments:

ajaxFunctionThatDoesTheJob({url: url});

The benefit of embedding your URL in the form is that you can use CSRF tokens and bunch of other usefull stuff such as method spoofing and hidden input fields.

It took me quite some time to figure this all out so I hope this saves someone a lot of research and helps to write good code :)