I'm starting to work with Codeigniter 4 Shield.
I added this piece of code to my app/Config/Routes.php file.
$routes->get('/access/token', static function() {
$token = auth()->user()->generateAccessToken(service('request')->getVar('token_name'));
return json_encode(['token' => $token->raw_token]);
});
When I try to access the route in my web browser using the URL https://example.com/access/token, I obtain the error:
Call to a member function generateAccessToken() on null
produced by the line of code below:
$token = auth()->user()->generateAccessToken(service('request')->getVar('token_name'));
Background information:
I have installed Codeigniter 4 Shield using Composer, ran the respective database migrations, and everything else works fine.
My Codeigniter 4 Shield 'login' and 'register' pages work fine.
How can I load generateAccessToken() automatically in the app/Config/Routes.php file?
You need to submit the login credentials (email & password) along with your HTTP POST request to help identify the User requesting the access token. Otherwise, auth()->user() is empty, hence the error.
To generate an access token, you need to first authenticate the User.
For example: (Using email & password)
Define your 'access token' route. Notice the use of ->post(...) instead of ->get(...).
File: app/Config/Routes.php
$routes->post('auth/token', '\App\Controllers\Auth\LoginController::accessToken');
Define your Controller method that will handle the 'access token' generation. Read: Issuing the Tokens
File: app/Controllers/Auth/LoginController.php
<?php
namespace App\Controllers\Auth;
use App\Controllers\BaseController;
class LoginController extends BaseController
{
public function accessToken()
{
// Validate credentials
$rules = [
'email' => [
'label' => 'Auth.email',
'rules' => config('AuthSession')->emailValidationRules,
],
'password' => [
'label' => 'Auth.password',
'rules' => 'required',
],
];
if (!$this->validate($rules)) {
return $this->response
->setJSON(['errors' => $this->validator->getErrors()])
->setStatusCode(422);
}
if (auth()->loggedIn()) {
auth()->logout();
}
// Attempt to login
$result = auth()->attempt([
'email' => $this->request->getPost('email'),
'password' => $this->request->getPost('password')
]);
if (!$result->isOK()) {
return $this->response
->setJSON(['error' => $result->reason()])
->setStatusCode(401);
}
// Generate token and return to client
$token = auth()->user()->generateAccessToken($this->getDeviceName());
return $this->response
->setJSON(['token' => $token->raw_token]);
}
public function getDeviceName()
{
$agent = $this->request->getUserAgent();
if ($agent->isBrowser()) {
$currentAgent = $agent->getBrowser() . ' ' . $agent->getVersion();
} elseif ($agent->isRobot()) {
$currentAgent = $agent->getRobot();
} elseif ($agent->isMobile()) {
$currentAgent = $agent->getMobile();
} else {
$currentAgent = 'Unidentified User Agent';
}
return $agent->getPlatform() . " - " . $currentAgent;
}
}
Protect your /api routes using the $filters setting on app/Config/Filters.php. Read: Protecting Routes
Exclude your 'access token' ("auth/token") route together with all API routes ("api/*") from the global "session" & "toolbar" filters.
File: app/Config/Filters.php
<?php
// ...
class Filters extends BaseConfig
{
// ...
public array $globals = [
'before' => [
'session' => ['except' => [
"login*",
"register",
"auth/a/*",
"auth/token",
"api/*"
]],
],
'after' => [
'toolbar' => ['except' => ["auth/token", "api/*"]],
],
];
// ...
public array $filters = [
'tokens' => ['before' => ["api/*"]],
];
}
Make a one-time initial HTTP POST request to the auth/token route to receive the 'access token'. Upon receiving the token, store it with the client. I.e: in localStorage
$.ajax({
url: "https://your-site-domain.com/auth/token",
type: "POST",
data: {
"email": "USER-EMAIL-ADDRESS-HERE",
"password": "USER-PASSWORD-HERE",
},
success: function (response) {
window.localStorage.setItem('token', response.token);
},
error: function (jqXHR) {
console.log(jqXHR.responseText);
},
});
You may now send the received/stored access token using the Authorization header along with all your other protected API HTTP requests in your application without reauthenticating the user. i.e:
$.ajax({
url: "https://your-site-domain.com/api/rest/v1/employees",
type: "GET",
beforeSend: function (jqXHR) {
jqXHR.setRequestHeader(
"Authorization",
"Bearer " + window.localStorage.getItem('token')
);
},
data: {},
success: function (response) {
// Use the response here on success.
// I.e: listing all employees in a table.
},
error: function (jqXHR) {
console.log(jqXHR.responseText);
},
});
Related
I am using Laravel 7 and Vue.js 2.
When I edit a room I should update the rooms table and then to redirect to the admin page with a succesfull message.
Unfortunately when I submit the form I edit correctly the table but then the redirect fails. It appears the following error message:
message: "The PUT method is not supported for this route. Supported methods: GET, HEAD.
This is my two methods in AdminController:
public function index()
{
$permissions = Permission::select('id')->get();
$rooms = Room::all();
$languages = Language::all();
$users = UserResource::collection(User::all());
return view('admin')->with(['success_message' => '', 'permissions'=>$permissions, 'users'=>$users, 'rooms'=>$rooms, 'languages'=>$languages]);
}
public function edit_room (Request $request) {
$validator = Validator::make($request->all(), [
'id' => 'required',
'name' => 'required'
]);
if ($validator->fails()) {
return response($validator->errors());
}
$room = Room::find($request->id);
$room->name = $request->name;
$room->save();
$success_message = "The room " . $request->name . " has been correctly edited";
return Redirect::route('admin')->with( ['success_message' => $success_message] );
}
This is the axios call in my child component:
editRoom: function() {
axios.put('edit_room', { id: this.rooms[this.index].id, name: this.roomName })
.then((response) => {
console.log(response);
this.errors = response.data;
if (Object.keys(this.errors).length === 0) {
alert('viva');
this.user = {};
} else {
alert('noviva');
}
})
.catch(error => {
alert(noooooo);
console.log(error);
});
}
This is my two routes in web.php:
Route::put('edit_room', 'AdminController#edit_room')->name('edit_room');
Route::get('/admin', 'AdminController#index')->name('admin');
This is an update of the table so I suppose I should use a PUT method but for some reason it doesn't.
Which is the best way to solve this error?
I think the problem is that you send your request via XHR
So when you using
return Redirect::route('admin')->with( ['success_message' => $success_message]
it sends an response with 301 http code to redirect your browser
i think you should refactor your code like this for example
return 'success_message'
and then in your axios after console.log(response);
window.location.href = "http://www.your_address.com/admin";
js:
$.ajax({
url: '/site/updateuserdata',
method: 'POST',
//async: true,
//cache:false,
data: {
'type': 'sort'
//val: val
//csrfParam: csrfToken
},
dataType: 'text',
error: function(jqXHR, textStatus, errorThrown) {
alert('error by ajax');
},
success: function(data, status, jqXHR) {
alert('success by ajax');
}
});
Controller:
public function behaviors()
{
return [
'access' => [
'class' => AccessControl::className(),
'rules' => [
[
'actions' => ['updateuserdata'],
'allow' => true,
// 'roles' => ['*'],
],
],
],
'verbs' => [
'class' => VerbFilter::className(),
'actions' => [
'updateuserdata' => ['post'],
],
],
];
}
public function actionUpdateuserdata()
{
/*if (Yii::$app->request->isAjax) {
$message = 'Ваше сообщение успешно отправлено';
Yii::$app->response->format = Response::FORMAT_JSON;
$response = [
'success' => true,
'message' => $message
];
return $response;
}*/
$f = fopen('testajax.txt','a+');
fwrite($f, 'ajax: '.(isset($_POST['type'])?$_POST['type']:'error'));
fclose($f);
if(isset($_POST['type']))
return $_POST['type'];
else return 'error1';
// return Yii::$app->getResponse()->redirect(Yii::$app->request->referrer, 302, FALSE);
}
yii.js:
function initRedirectHandler() {
// handle AJAX redirection
$(document).ajaxComplete(function (event, xhr) {
var url = xhr && xhr.getResponseHeader('X-Redirect');
alert(url); //my code
if (url) {
window.location.assign(url);
}
});
}
I see first alert(ajax error) "error by ajax" and then alert(yii.js) "..../site/updateuserdata...", why ajax error? File testajax.txt not create.
I tried comment 'updateuserdata' => ['post'], and get error too.
Updated.
Also, tried:
public function beforeAction($action)
{
if ($action->id == 'updateuserdata') {
$this->enableCsrfValidation = false;
}
return parent::beforeAction($action);
}
and uncomment csrf parameters in ajax.
And 'error' return status 302(jqXHR.status).
dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd
I find problem, it's stupid problem:
url: '/mag/updateuserdata/',
i have in 'urlManager':
'suffix' => '/',
i think this added for all, but not.....
Cause you commented
//csrfParam: csrfToken
from your ajax js file you have 400 http status code in your ajax.
So you can solve it in two way:
one: disable csrf validation by adding
$this->enableCsrfValidation = false;
in your actionUpdateuserdata method.
two: add csrf token and csrf param to your ajax.
print this code in your js, where you commented it.
Yii::$app->request->csrfParam . ':' . Yii::$app->request->csrfToken
then your request sent successfully and if you have other error you must check your code.
I have a SPA using VUE and LARAVEL 5.8
I have setup an API_TOKEN associated to the logged in user. Everything works fine right after the login. I get the API_TOKEN, I save it into a var and I send it together with the Axios request. In Laravel I have a middleware that is taking care of the token and comparing it with the one setup on the logged in user.
the problem though occur when session expires. Because I still can navigate the private pages and make API requests to save and delete content. This is possible I think because I still have the same API_TOKEN saved in the var and the middleware apparently doesn't get that the session is expired.
So I want to obtain the API_TOKEN every time I'm doing an Ajax, request so when the session expires, I won't get the token and therefore, I won't be able to complete the request.
This is my setup.
web.php is where I have the only php route that points to a singlePageController:
Auth::routes();
Route::get('/{any}', 'SinglePageController#index')->where('any', '.*');
Then in the singlePageController I return the view:
class SinglePageController extends Controller
{
public function index() {
return view('app', ['loggedUser' => auth()->user()]);
}
}
Then I have the api.php where I have the API routes. As you can see at the end I have the middleware to make it private. Just to make an example this is the one I use for updating the content:
Route::put('event/update/{slug}', 'EventController#update')->middleware('auth:api');
Then the related controller of that API route:
public function update(Request $request, $slug)
{
$event = Event::where('slug', $slug)->first();
$event->title = $request->input('title');
return new EventResource($event);
}
And in the end this is the Resource I use to define what and how the API data is going to be displayed:
public function toArray($request)
{
// return parent::toArray($request);
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'curator' => $this->curator,
'featured_image' => $this->featured_image,
'body' => $this->body,
'date' => $this->date
];
}
So this above is the flow I have. Then when I do an axios call to update the content, I'm doing something like:
axios({
method: 'PUT',
url: '/api/event/update/' + this.$route.params.slug + '?api_token=' + this.isLogged.apiToken,
data: dataToSave,
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
})
.then((response) => {
this.getNotification('Success: The Event has been saved');
})
.catch((error) => {
this.getNotification('Error: Impossible saving the event');
console.log(error);
})
Do you know how to make it? or if there is a better way to accomplish that?
you and do like, your login method should like this.
public function login(Request $request)
{
if (Auth::attempt(['email' => $request['email'], 'password' => $request['password']])) {
$user = Auth::user();
$success = $user->createToken(config('app.name'))->accessToken;
return response()->json(["token" => $success, 'status' => 200]);
} else {
return response()->json(['message' => "Email or Password do not match"], 401);
}
}
In my project I have a piece of my form that sends an AJAX request:
$.ajax({
url: '/bio_control/sample',
type: 'POST',
dataType: 'json',
data: {sample_number: $(input_field).val()},
});
Which activates the following controller method:
/**
* #Route("/bio_control/sample", name="get_bio_control_sample")
*/
public function getBioControlSampleAction(Request $request)
{
$sample_number = $request->request->get('sample_number');
/**
* Additional logic not shown for brevity.
*/
$user_id = $user->getId();
$response = array("code" => 100, "success" => true, "sample_number" => $sample_number, "sample_data" => $sample[0], "new_user" => false, "user_id" => $user_id);
return new JsonResponse($response);
}
I'd like to be able to test this request in isolation, but I'm unsure how to write the request object.
So far my first attempt:
public function testGetBioControlSample()
{
$helper = $this->helper;
$client = $this->makeClient();
$crawler = $client->request('POST', "/bio_control/sample", array(), array('sample_number' => 67655), array(
'CONTENT_TYPE' => 'application/json',
'HTTP_X-Requested-With' => 'XMLHttpRequest'
));
$this->assertStatusCode(200, $client);
}
Fails because it appears to be submitting the form (I get an error related to a form field completely unrelated to the AJAX request being blank).
Can anyone demonstrate how to correctly write such a test?
Does this URL need authentication?
I like to use LiipFunctionalTestBundle for my functional tests and they usually looks like this:
<?php
declare(strict_types=1);
namespace Tests\Your\Namespace;
use Liip\FunctionalTestBundle\Test\WebTestCase;
class PostResourceActionTest extends WebTestCase
{
public function testShouldReturnResponseWithOkStatusCode(): void
{
$credentials = [
'username' => 'user',
'password' => 'pass'
];
$client = $this->makeClient($credentials);
$payload = ['foo' => 'bar'];
$client->request(
'POST',
'/the/url/',
$payload,
[],
['HTTP_Content-Type' => 'application/json']
);
$this->assertStatusCode(200, $client);
}
}
Maybe the error you are getting is the login form asking for authentication?
The exact syntax I used to solve this problem was:
public function testGetBioControlSample()
{
$helper = $this->helper;
$client = $this->makeClient();
$crawler = $client->request(
'POST',
"/bio_control/sample",
array('sample_number' => 67655),
array(),
array('HTTP_Content-Type' => 'application/json')
);
$JSON_response = json_decode($client->getResponse()->getContent(), true);
$this->assertStatusCode(200, $client);
$this->assertNotEmpty($JSON_response);
$this->assertEquals($JSON_response["code"], 100);
$this->assertEquals($JSON_response["success"], true);
$this->assertEquals($JSON_response["sample_number"], 67655);
}
I believe I didn't need: 'HTTP_X-Requested-With' => 'XMLHttpRequest' in the final array argument.
Additionally, I had array('sample_number' => 67655) in the wrong argument.
Symfony 4.1 has a new way of testing AJAX requests:
// Before
$crawler = $client->request('GET', '/some/path', [], [], [
'HTTP_X-Requested-With' => 'XMLHttpRequest',
]);
// After
$crawler = $client->xmlHttpRequest('GET', '/some/path');
i want to post ajax request using vue-resource this.$http.post request. it worked perfectly fine if i passed all validation rules but i want to get some validations if it fails. so far i keep getting 500 error if i don't fill out some input fields. it's hard for me to debug the error because it didn't appeared on the network tab.
here's what i've done so far
//my modal component
<script>
export default {
props: ['show'],
data() {
return {
input: {
id: '',
name: '',
address: '',
email: ''
},
errorInputs: {}
}
},
methods: {
createStudent() {
this.$http.post('/students', this.$data.input)
.then((response) => {
alert('added new row!)
}, (response) => {
console.log(response.data);
});
}
}
}
</script>
// my controller
public function store(Request $request) {
$validator = $this->validate($request,[
'id' => 'required',
'name' => 'required|unique:students',
'email' => 'required|unique:students|email',
'address' => 'required',
]);
if($validator->passes()){
Student::create($request->all());
return response()->json([], 201);
}
$errors = json_decode($validator->errors());
return response()->json([
'success' => false,
'message' => $errors
],422);
}
any helps and references would be appreciated. i am using laravel 5.3 and vue js 2
$this->validate() returns 422 error response alongside your validation errors, so you should get those errors in then() second callback (like you do now). Your vue component body should be like this:
{
data() {
// ...
},
createStudent() {
this.$http
.post('/students', this.input)
.then(this.handleSuccess, this.handleError)
},
handleSuccess(res) {
alert('student created')
},
handleError(res) {
if (res.status === 422) {
this.errorInputs = res.body
} else {
alert('Unkown error!')
}
}
}
Remember to add v-model="input.fieldName" properties to your inputs.
Remember to include your session token along with your post, unless of course you are disabling csrf tokens for that route.
Since Laravel 5.1 you can disable this in your verifytoken middleware
<?php namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as ...
class VerifyCsrfToken extends ... {
protected $except = [
'payment/*',
];
}