I'm new with Symfony4 so maybe this is a noob question. But can't figure
it out.
I'm trying to register a user with an ajax call:
this.form.post('/register').then((response) => {
console.log(response);
});
Then in symfony my controller method looks like this:
/**
* #Route("/register", name="register", methods={"POST"})
* #param Request $request
* #param UserPasswordEncoderInterface $passwordEncoder
* #return JsonResponse
*/
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder)
{
$user = new User();
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$request->request->get('password')
)
);
$user->setEmail($request->request->get('email'));
$user->setName($request->request->get('name'));
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
return $this->json(['success' => 'User created']);
}
The problem is that the data is always empty. The data is send correctly. When I dd the response in my controller I see this:
My headers etc. look like this:
Request payload looks like this:
When I try it with postman I get the same result. What could I be doing wrong?
You need to create a form type on the back-end side to validate your new registrated user, check this documentation. Don't forget to update your controller also.
In your twig, you must define
<script>
let registerPath = "{{ app.request.getSchemeAndHttpHost() ~ path('register') }}";
</script>
And in your js you can use your ajax post like that :
this.form.post(registerPath).then((response) => {
console.log(response);
});
Hope that can help :)
Related
I am building a single-page-app with Vue (2.5) using Laravel (5.5) as the backend. Everything works well, except for directly logging in again after having logged out. In this case, the call to /api/user (to retrieve the user's account information and to verify the user's identity once more) fails with a 401 unauthorized (even though the log-in succeeded). As a response, the user is bounced back directly to the login screen (I wrote this measure myself as a reaction to 401 responses).
What does work is to log out, refresh the page with ctrl/cmd+R, and then log in again. The fact that a page refresh fixes my problem, gives me reason to believe that I am not handling refresh of the X-CSRF-TOKEN correctly, or may be forgetting about certain cookies that Laravel uses (as described here ).
This is a snippet of the code of the login form that is executed after a user clicks the login button.
login(){
// Copy the form data
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( this.refreshTokens )
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
},
refreshTokens(){
return new Promise((resolve, reject) => {
axios.get('/refreshtokens')
.then( response => {
window.Laravel.csrfToken = response.data.csrfToken;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = response.data.csrfToken;
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
return resolve(response);
})
.catch( error => {
this.showErrorMessage(error.message);
reject(error);
});
});
},
the authenticate() method is a vuex action, which calls the login endpoint at the laravel side.
The /refreshTokens endpoint simply calls this Laravel controller function that returns the CSRF token of the currently logged-in user:
public function getCsrfToken(){
return ['csrfToken' => csrf_token()];
}
After the tokens have been refetched, the user is redirected to the main page (or another page if supplied)
with this.$router.replace(this.$route.query.redirect || '/'); and there the api/user function is called to check the data of the currently logged in user.
Are there any other measures I should take to make this work, that I am overlooking?
Thanks for any help!
EDIT: 07 Nov 2017
After all the helpful suggestions, I would like to add some information. I am using Passport to authenticate on the Laravel side, and the CreateFreshApiToken middleware is in place.
I have been looking at the cookies set by my app, and in particular the laravel_token which is said to hold the encrypted JWT that Passport will use to authenticate API requests from your JavaScript application. When logging out, the laravel_token cookie is deleted. When logging in again directly afterwards (using axios to send an AJAX post request) no new laravel_token is being set, so that's why it doesn't authenticate the user. I am aware that Laravel doesn't set the cookie on the login POST request, but the GET request to /refreshTokens (which is not guarded) directly afterwards should set the cookie. However, this doesn't appear to be happening.
I have tried increasing the delay between the request to /refreshTokens and the request to /api/user, to maybe give the server some time to get things in order, but to no avail.
For completeness sake, here is my Auth\LoginController that is handling the login request server-side:
class LoginController extends Controller
{
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* #var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* #return void
*/
public function __construct()
{
// $this->middleware('guest')->except('logout');
}
/**
* Get the needed authorization credentials from the request.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
protected function credentials(\Illuminate\Http\Request $request)
{
//return $request->only($this->username(), 'password');
return ['email' => $request->{$this->username()}, 'password' => $request->password, 'active' => 1];
}
/**
* The user has been authenticated.
*
* #param \Illuminate\Http\Request $request
* #param mixed $user
* #return mixed
*/
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
/**
* Log the user out of the application.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function logout(\Illuminate\Http\Request $request)
{
$this->guard()->logout();
$request->session()->invalidate();
}
}
Considering that you are using an api for authentication, I would suggest using Passport or JWT Authentication to handle authentication tokens.
Finally fixed it!
By returning the UserResource directly in the LoginControllers authenticated method, it is not a valid Laravel Response (but I guess raw JSON data?) so probably things like cookies are not attached. I had to attach a call to response() on the resource and now everything seems to work fine (though I need to do more extensive testing).
So:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
);
}
becomes
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
...
return (new UserResource($user))->additional(
['permissions' => $user->getUIPermissions()]
)->response(); // Add response to Resource
}
Hurray for the Laravel docs on attributing a section to this:
https://laravel.com/docs/5.5/eloquent-resources#resource-responses
Additionally, the laravel_token is not set by the POST request to login, and the call to refreshCsrfToken() also didn't do the trick, probably because it was protected by the guest middleware.
What worked for me in the end is to perform a dummy call to '/' right after the login function returned (or the promise was fulfilled).
In the end, my login function in the component was as follows:
login(){
// Copy the user object
const data = {...this.user};
// If remember is false, don't send the parameter to the server
if(data.remember === false){
delete data.remember;
}
this.authenticating = true;
this.authenticate(data)
.then( csrf_token => {
window.Laravel.csrfToken = csrf_token;
window.axios.defaults.headers.common['X-CSRF-TOKEN'] = csrf_token;
// Perform a dummy GET request to the site root to obtain the larevel_token cookie
// which is used for authentication. Strangely enough this cookie is not set with the
// POST request to the login function.
axios.get('/')
.then( () => {
this.authenticating = false;
this.$router.replace(this.$route.query.redirect || '/');
})
.catch(e => this.showErrorMessage(e.message));
})
.catch( error => {
this.authenticating = false;
if(error.response && [422, 423].includes(error.response.status) ){
this.validationErrors = error.response.data.errors;
this.showErrorMessage(error.response.data.message);
}else{
this.showErrorMessage(error.message);
}
});
and the authenticate() action in my vuex store is as follows:
authenticate({ dispatch }, data){
return new Promise( (resolve, reject) => {
axios.post(LOGIN, data)
.then( response => {
const {csrf_token, ...user} = response.data;
// Set Vuex state
dispatch('setUser', user );
// Store the user data in local storage
Vue.ls.set('user', user );
return resolve(csrf_token);
})
.catch( error => reject(error) );
});
},
Because I didn't want to make an extra call to refreshTokens in addition to the dummy call to /, I attached the csrf_token to the response of the /login route of the backend:
protected function authenticated(\Illuminate\Http\Request $request, $user)
{
$user->last_login = \Carbon\Carbon::now();
$user->timestamps = false;
$user->save();
$user->timestamps = true;
return (new UserResource($user))->additional([
'permissions' => $user->getUIPermissions(),
'csrf_token' => csrf_token()
])->response();
}
You should use Passports CreateFreshApiToken middleware in your web middleware passport consuming-your-api
web => [...,
\Laravel\Passport\Http\Middleware\CreateFreshApiToken::class,
],
this attaches attach the right csrftoken() to all your Request headers as request_cookies
How can I handle request data from the $_POST data itself. I mean if I try to handle the form like this: $form->handleRequest($request);, Symfony would try to get the data from $_POST['form_classname'], but I want to fill my form class straight from base $_POST variables.
Actually I want to handle the information from the outer site. And I have to develop something like an API. But without authorization, tokens, etc...
So I decided to build the form with some properties I need. After validation the form might do some logic.
Here is an example of $_POST I have to handle
Function=TransResponse&RESULT=0&RC=00&AUTHCODE=745113
As you can see, there is no form name in request. The $form->handleRequest($request); works only if the request was like an
[form_name][Function]=TransResponse&[form_name][RESULT]=0&[form_name][RC]=00&[form_name][AUTHCODE]=745113
But I can't change the request format.
Just put in your form class
/** #inheritdoc */
function getBlockPrefix() {
return '';
}
Here is the information about this method Documentation
Use
$this->get('form.factory')->createNamed('')
$this->get('form.factory.)->createNamedBuilder('')
to create a Form or FormBuilder respectively that uses the whole $_POST/$_GET array for its parameters.
Example:
/**
* #Route("/testRoute")
* #param Request $request
* #return Response
*/
public function testAction(Request $request): Response
{
$form = $this->get('form.factory')->createNamedBuilder('', FormType::class, null, ['csrf_protection' => false])
->add('text', TextType::class)
->setMethod('GET')
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
return new Response($form['text']->getData());
}
return new Response('Submit me');
}
I am using Laravel 5.4. I have a custom form request class where I have my validation rules and messages and I use it in my controller like following :
public function store(CustomFormRequest $request)
{
//
}
I am using ajax to send the request and when there is any validation error, Laravel throws an error with an HTTP response with a 422 status code including a JSON representation of the validation errors.
But I don't want that. Instead, inside my controller's method, I want to find out if there is any validation error and if there is any then I want to return a response with some additional data along with the validation messages, like this:
// Inside my Controller
public function store(CustomFormRequest $request)
{
if ($validator->fails())
{
$errors = $validator->errors();
return response()->json(array('status' => 2, 'msg' => $errors->all() ));
}
}
Could you please help ?
Thanks in advance.
The easiest way to do this would be to override the response() method for the Form Request class.
To do this you can simply add something like the following to your class:
public function response(array $errors)
{
if ($this->expectsJson()) {
return new JsonResponse(['status' => 2, 'msg' => $errors], 422);
}
return parent::response($errors);
}
Don't for get to import Illuminate\Http\JsonResponse
Hope this helps!
I know you want the logic in your controller, but you can still leverage your Request file for this. In the Laravel documentation (assuming you are using the latest version) it is described as Adding After Hooks To Form Requests:
If you would like to add an "after" hook to a form request, you may use the withValidator method. This method receives the fully constructed validator, allowing you to call any of its methods before the validation rules are actually evaluated:
/**
* Configure the validator instance.
*
* #param \Illuminate\Validation\Validator $validator
* #return void
*/
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something is wrong with this field!');
}
});
}
try this:
form.ajaxSubmit({
async: false,
type: yourMethod,
url: yourRoute,
headers: { 'X-CSRF-TOKEN': "{{csrf_token()}}" },
dataType: 'json',
success: function(data){
location.href = data.redirect_to;
},
error: function(data) {
var errors = data.responseJSON;
var errorsArr = [];
for (error in errors) {
errorsArr.push(errors[error]);
}
errorsArr = $.map(errorsArr, function(n){
return n;
});
alert("<strong class='text-danger'>" + errorsArr.join("<br>") + "</strong>");
console.log(errors);
}
});
and in your controller store method make return:
return response()->json(['redirect_to' => '/your_route']);
After the update of laravel documentation, you don't need to override the response() anymore, all you have to do is just write your business logic inside the protected failedValidation() inside your custom FormRequest class like follows:
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Contracts\Validation\Validator;
/**
* [failedValidation [Overriding the event validator for custom error response]]
* #param Validator $validator [description]
* #return [object][object of various validation errors]
*/
public function failedValidation(Validator $validator) {
// write your business logic here otherwise it will give same old JSON response
throw new HttpResponseException(response()->json($validator->errors(), 422));
}
I have made a method called by an Ajax request when a button is clicked.
/**
* #param Request $request
*
* #Route("/add", name="rapid_access_add", options={"expose"=true})
* #Method({"GET"})
*
* #return Response
*/
public function addRouteAction(Request $request)
{
$title = $request->query->get('title');
$user = $this->getUser();
$url = $this->get('request_stack')->getMasterRequest()->getUri();
$rapidAccess = new RapidAccess();
$rapidAccess->setUrl($url)
->setTitle($title)
->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($rapidAccess);
$em->flush();
$this->addFlash('success', $this->get('translator')->trans('user.flash.rapid_access_added', ['%title%' => $title], 'front'));
return new Response('OK');
}
I'm trying to get the URL of the current page, render by another controller (this method is in a fragment controller).
But when I use $this->get('request_stack')->getMasterRequest()->getUri(); this give me the URL of the addRouteAction method.
This should give me the master request URL but I don't understand why this send me this method URL. How can I get the current page URL instead of this method URL ?
Maybe I should get the URL with JS instead ?
Thanks
Why do use request stack ?
You can use directly Request from Controller :
$request->getUri();
I have a page with a form and want to know if it is possible to access it using GET, but only allow logged in users to POST to it.
I know this can be done in security.yml, but am not sure how to do it with annotations.
/**
* #param Request $request
* #return Response
* #Security("has_role('ROLE_USER')")
* #Method(methods={"POST"})
*/
public function calculatorAction(Request $request)
{
$form=$this->createForm(new CallRequestType(),$callReq=new CallRequest());
$form->handleRequest($request);
if($form->isValid()){
//blabla
}
return $this->render('MyBundle:Pages:calculator.html.twig', array('form' => $form));
}
This will secure the whole function, but I want to access it, just not POST to it without being logged in. An alternative would be to check if there is a logged in user in the $form->isValid() bracket. But I'm still wondering if it can be done with annotations.
You could do something like this.
You can allow both method types anonymously, and check just inside the controller to see if the user is authenticated and is POSTing.
(You don't state which version of symfony you're using, so you might have to substitute the authorization_checker (2.8) for the older security.context service)
/**
* #param Request $request
* #return Response
*
* #Route("/someroute", name="something")
* #Method(methods={"POST", "GET"})
*/
public function calculatorAction(Request $request)
{
if ( !$this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY') && $request->getMethod() == 'POST') {
throw new AccessDeniedHttpException();
}
$form=$this->createForm(new CallRequestType(),$callReq=new CallRequest());
$form->handleRequest($request);
// you also need to check submitted or youll fire the validation on every run through.
if($form->isSubmitted() && $form->isValid()){
//blabla
}
return $this->render('MyBundle:Pages:calculator.html.twig', array('form' => $form));
}