Is there a way to prevent form state being reset after server 400 error on an Edit form in react-admin? - react-admin

I have an Edit component that contains a TabbedForm that has been set to pessimistic mode. I am also using a custom data provider based on react-admin's simple rest provider.
<Edit
title="Edit entity"
mutationMode="pessimistic"
{...props}
>
<TabbedForm>
<FormTab label="Details">
<TextInput source="name" />
</FormTab>
</TabbedForm>
</Edit>
There is a validation rule in the API that ensures that another entity with the same name does not exist.
When this validation fails, the server returns a 400 error with a message. I've set this up to display this error in a toast and this is working fine.
The problem I am having is that the entire form state gets reset when there is a 400 error response, so all progress that the user has made on the form has been lost. For example, if the original value of the name field was "Entity A", and I change this to "Entity B" and submit the form, the API discovers that there is already an entity with that name, returns back the error and then resets the name field back to "Entity A". If there were other fields on this form, they are all reset back to the original state as well.
It appears that there is another HTTP request that is sent to the API to get the entity by ID again after the 400 response comes back (i.e. the getOne method on the data provider is being called again).
Is there a way to prevent the form state being reset after a 400 response from the server?
I'm thinking an alternative solution is to prevent 400 errors coming back at all by using client-side validation - perhaps by adding a custom validator that performs an API request to check if there's an entity with that name.

I figured out a way to prevent the form being refreshed after a failure on submit by overriding the onFailure behaviour.
const onFailure = (error: any) => {
notify(
typeof error === "string"
? error
: error.message || "ra.notification.http_error",
{ type: "warning" }
);
};
I have only emitted the logic to refresh() the page as described in the react-admin documentation.
Reference: https://marmelab.com/react-admin/CreateEdit.html#onfailure

Related

Uploading a file in Cypress when no input type="file"

I need to upload a file, but the thing is that the button doesn't have the tag input and attr type="file". How do I solve this?
The DOM:
I tried:
cy.contains('div#dropdown-grouped', "Bon d'intervention").siblings('div.d-none').find('input[type="file"]')
.selectFile('cypress/fixtures/bon.pdf', {force: true})
But this returns a statusCode 422
Also tried:
const filePath = 'My reportis.jpg'
cy.contains('#dropdown-group-1', "Bon d'intervention").attachFile(filepath)
But this obviously does nothing.
What if I would change the tag of the button from button to input, and add an attr type="file", would that work? If yes, how do you I change the tag with cypress?
Thank you very much!
Solution:
cy.contains('#dropdown-group-1', "Bon d'intervention").click()
cy.get('input[type="file"]').selectFile('cypress/fixtures/bon.pdf', {force: true})
Your first code is correct,
cy.contains('div#dropdown-grouped', "Bon d'intervention")
.siblings('div.d-none')
.find('input[type="file"]')
.selectFile('cypress/fixtures/Call order.pdf', {force: true})
Status code 422 is a response from the server, so selectFile() worked and the app fired a POST, but the server did not like what it was given.
422 Unprocessable Entity
The HyperText Transfer Protocol (HTTP) 422 Unprocessable Entity response status code indicates that the server understands the content type of the request entity, and the syntax of the request entity is correct, but it was unable to process the contained instructions.
From further discussion, it looks like following the steps a user takes gives a working test, which means adding a button click() before the selectFile().
This is the final working test:
cy.contains('#dropdown-group-1', "Bon d'intervention").click()
cy.get('input[type="file"]')
.selectFile('cypress/fixtures/bon.pdf', {force: true})

AWS Cognito Respond to New_Password_Required challenge returns "Cannot modify an already provided email"

An app that has been working successfully for a couple years has started throwing the following error whenever trying to respond to the NEW_PASSWORD_REQUIRED challenge with AWS Cognito:
{"__type":"NotAuthorizedException","message":"Cannot modify an already provided email"}
I'm sending the below, which all seems to match the docs.
{
"ChallengeName": "NEW_PASSWORD_REQUIRED",
"ClientId": <client_id>,
"ChallengeResponses": {
"userAttributes.email": "test#example.com",
"NEW_PASSWORD": "testP#55w0rd",
"USERNAME": "testfake"
},
"Session": <session_id>
}
Nothing has changed on the front end; is there a configuration change we might have done on the Cognito/AWS side that might cause this error?
I started getting the same error recently. I'm following Use case 23 Authenticate a user and set new password for a user. After some investigation, I found that it is the email attribute in userAttributes that's causing completeNewPasswordChallenge to throw the error. The userAttributes I get from authenticateUser used to be an empty object {}, but it now looks like this:
{ email_verified: 'true', email: 'test#example.com' }
I had to delete the email attribute (as well as the email_verified attribute as shown in the example code in Use case 23) before using the userAttribute for a completeNewPasswordChallenge. So my code is now like this:
cognitoUser.authenticateUser(authenticationDetails, {
...
newPasswordRequired: function(userAttributes, requiredAttributes) {
// the api doesn't accept this field back
delete userAttributes.email_verified;
delete userAttributes.email; // <--- add this line
// store userAttributes on global variable
sessionUserAttributes = userAttributes;
}
});
// ... handle new password flow on your app
handleNewPassword(newPassword) {
cognitoUser.completeNewPasswordChallenge(newPassword, sessionUserAttributes);
}
I guess aws changed their api recently, but I haven't found any doc about this change. Even though the value of the email attribute is the same as the actual email of the user, it throws the Cannot modify an already provided email error if you include it in the request. Deleting it solves the issue.

Invalid CSRF token error ( symfony 5 ) with VueJs frontend

I am having trouble in making authentication work using an external frontend ( vue ) with my symfony app. The main problem is the "Invalid CSRF token" error. I have a login form in vue which sends an object containing the username, password, and the csrf token ( which I get from symfony tokengenerator ). I have a custom authenticator where I create the user passport and add the token to it.
public function authenticate(Request $request): PassportInterface
{
$username = $request->request->get('username', '');
$request->getSession()->set(Security::LAST_USERNAME, $username);
$this->logger->info('The token is', [$request->get('_csrf_token')]);
$passport = new Passport(
new UserBadge($username),
new PasswordCredentials($request->request->get('password', '')),
);
$passport->addBadge(new CsrfTokenBadge('authenticate', $request->get('_csrf_token')));
return $passport;
}
It makes it through to the AuthenticationManager.php, where it enters the executeAuthenticator method. The error comes after the CheckPassportEvent is dispatched, from CSRFProtectionListener. It fails on the
if (false === $this->csrfTokenManager->isTokenValid($csrfToken)).
I have tried to get the tokenmanager instance inside of my authenticator and create the token there and add it to the passport.
$token = $this->csrfTokenManager->getToken('authenticate'); $passport->addBadge(new CsrfTokenBadge($token->getId(), $token->getValue()));
This lets me get past the authentication, but immediately afterwards, when it redirects to the next path, it gives me an error "Access denied, the user is not fully authenticated; redirecting to authentication entry point.". After some debugging, it seems that the token storage is empty ( the token is saved to the storage when the getToken() method is called ).
When I do the authentication with the twig template, it works flawlessly. How exactly {{ csrf_token('authenticate') }} makes and handles the token I do not understand. Any input would be appreciated.
You have to pass the Authenticationintention as a string. In your example its "authenticate".
$passport->addBadge(new CsrfTokenBadge(' ---> authenticate <--- ', $request->get('_csrf_token')));
To check it you should use a code like this:
if (false === $this->csrfTokenManager->isTokenValid(new CsrfToken('authenticate' $YOUR_TOKEN_HERE))
or from a controller:
$this->isCsrfTokenValid('authenticate', $YOUR_TOKEN_HERE)
enter code here
In Symfony 5 you should work with the CSRF Protection like this:
In Twig, you can generate a CSRF Token with the "csrf_token" method. This Method is described here https://symfony.com/doc/current/security/csrf.html#generating-and-checking-csrf-tokens-manually.
You can validate the token in a controller using the "isCsrfTokenValid" function which lives in the controller class which you are extending.
Check this for more information:
https://symfony.com/doc/4.4/security/csrf.html#generating-and-checking-csrf-tokens-manually
I think the problem is that youre using a new Symfony version but using old practicies.

Intercept api errors

I'm using react-admin with standard crud options, I want to react to the RA/CRUD_GET_LIST_FAILURE (and all others in the future) to logout the user if the error is a 401 (like when the token timed out)
How am I supposed to do this ?
On custom requests I have my own sagas which handle this in the catch part and correctly do the job.
If I try to intercept the RA/CRUD_GET_LIST_FAILURE like this :
function * loadingErrorRA (action) {
var e = action.payload;
console.error('loadingError',action);
if(action.error === "Unauthorized"){
//I can't find a better way because I don't have access to the fetch response object in here
yield put(userLogout());
yield put(showNotification('Disconnected','warning'));
} else {
yield put(showNotification('Error '+action.error,'warning'));
}
}
function * errorLoadingSaga() {
yield takeLatest('RA/CRUD_GET_LIST_FAILURE', loadingErrorRA);
}
I have a blank screen and an error pops :
ListController.js:417 Uncaught TypeError: Cannot read property 'list' of undefined
at Function.mapStateToProps [as mapToProps] (ListController.js:417)
at mapToPropsProxy (wrapMapToProps.js:43)
at handleNewPropsAndNewState (selectorFactory.js:34)
at handleSubsequentCalls (selectorFactory.js:67)
at pureFinalPropsSelector (selectorFactory.js:74)
at Object.runComponentSelector [as run] (connectAdvanced.js:26)
at Connect.componentWillReceiveProps (connectAdvanced.js:150)
at callComponentWillReceiveProps (react-dom.development.js:11527)
....
index.js:2178 The above error occurred in the <Connect(TranslatedComponent(undefined))> component:
in Connect(TranslatedComponent(undefined)) (created by List)
in List (created by WithStyles(List))
in WithStyles(List) (at SalesByOrganismList.js:40)
in div (at SalesByOrganismList.js:39)
in SalesByOrganismList (created by WithPermissions)
in WithPermissions (created by Connect(WithPermissions))
in Connect(WithPermissions) (created by getContext(Connect(WithPermissions)))
...
And then saga catch it with :
index.js:2178 uncaught at loadingErrorRA TypeError: Cannot read property 'list' of undefined
at Function.mapStateToProps [as mapToProps]
...
Thanks for the help
https://marmelab.com/react-admin/DataProviders.html#error-format
When the API backend returns an error, the Data Provider should throw an Error object. This object should contain a status property with the HTTP response code (404, 500, etc.). React-admin inspects this error code, and uses it for authentication (in case of 401 or 403 errors). Besides, react-admin displays the error message on screen in a temporary notification.
And https://marmelab.com/react-admin/Authentication.html#catching-authentication-errors-on-the-api
Each time the API returns an error, the authProvider is called with the AUTH_ERROR type. Once again, it’s up to you to decide which HTTP status codes should let the user continue (by returning a resolved promise) or log them out (by returning a rejected promise).

What's the easiest way to display content depending on the URL parameter value in DocPad

I'd like to check for an URL parameter and then display the confirmation message depending on it.
E.g. if I a GET request is made to /form?c=thankyou docpad shows the form with thank you message
I think there is two basic ways to do this.
look at the url on the server side (routing) and display differing content according to URL parameters
Look at the parameter on the client side using JavaScript and either inject or show a dom element (eg div) that acts as a message box.
To do this on the server side you would need to intercept incoming requests in the docpad.coffee file in the serverExtend event. Something like this:
events:
# Server Extend
# Used to add our own custom routes to the server before the docpad routes are added
serverExtend: (opts) ->
# Extract the server from the options
{server} = opts
docpad = #docpad
# As we are now running in an event,
# ensure we are using the latest copy of the docpad configuraiton
# and fetch our urls from it
latestConfig = docpad.getConfig()
oldUrls = latestConfig.templateData.site.oldUrls or []
newUrl = latestConfig.templateData.site.url
server.get "/form?c=thankyou", (req,res,next) ->
document = docpad.getCollection('documents').findOne({relativeOutPath: 'index.html'});
docpad.serveDocument({
document: document,
req: req,
res: res,
next: next,
statusCode: 200
});
Similar to an answer I gave at how to handle routes in Docpad
But I think what you are suggesting is more commonly done on the client side, so not really specific to Docpad (assumes jQuery).
if (location.search == "?c=thankyou") {
$('#message-sent').show();//show hidden div
setTimeout(function () {
$('#message-sent').fadeOut(1000);//fade it out after a period of time
}, 1000);
}
This is a similar answer I gave in the following Docpad : show error/success message on contact form
Edit
A third possibility I've just realised is setting the document to be dynamically generated on each request by setting the metadata property dynamic = true. This will also add the request object (req) to the template data passed to the page. See Docpad documentation on this http://docpad.org/docs/meta-data.
One gotcha that gets everyone with setting the page to dynamic is that you must have the docpad-plugin-cleanurls installed - or nothing will happen. Your metadata might look something like this:
---
layout: 'default'
title: 'My title'
dynamic: true
---
And perhaps on the page (html.eco):
<%if #req.url == '/?c=thankyou':%>
<h1>Got It!!!</h1>
<%end%>