Storing CognitoUser in cache make him lose some properties - reactjs

I'm currently using AWS Cognito in my application.
When a user first connects whit his account, Cognito returns NEW_PASSWORD_REQUIRED as a challenge, which is fine.
I want to redirect to a page where the user can set his new password, so I put the response from Auth.signIn in storage (I tried local storage, session storage and Cache from AWS Amplify) but when I get it back on the other page, it lose some properties and Auth.completeNewPassword returns the error : 'user.completeNewPasswordChallenge is not a function'
Login.js :
try {
var authPromise = Auth.signIn(this.state.email, this.state.password);
authPromise.then((result) => {
console.log(result);
if (result.challengeName === 'NEW_PASSWORD_REQUIRED') {
Cache.setItem("CognitoUser", result);
this.props.history.push("/login/newPassword");
}
else {
this.props.userHasAuthenticated(true);
this.props.history.push("/");
}
});
} catch (e) {
alert(e.message);
this.setState({ isLoading: false });
}
NewPassword.js :
try {
var user = Cache.getItem("CognitoUser");
if (user) {
await Auth.completeNewPassword(user, this.state.newPassword);
this.props.history.push("/");
Cache.removeItem("CognitoUser");
}
} catch (e) {
alert(e.message);
this.setState({ isChanging: false });
}
Any ideas ?

It's javascript so when you write to your localcache and serializes your result into the "CognitoUser" key , it's stored as a a string, which afterwards deserialized will be a plain old Object unaware of the original type before serialization.
Original cause is maybe that your "result" type may expose functions which are not serializable (if not a getter, or if a getter with arguments).
I suggest you to call and store all the data you want into separate keys and re-read them later.
Cache.setItem("CognitoUser", result);
Cache.setItem("CognitoUser-value-1", result.myFunction1("myArg1"));
Cache.setItem("CognitoUser-value-2", result.myFunction2("myArg2"));
// ..
var user = Cache.getItem("CognitoUser");
var myVal1 = Cache.getItem("CognitoUser-value-1");
var myVal2 = Cache.getItem("CognitoUser-value-2");
You can also keep one single key "CognitoUser" in your localStorage if you make all said functions serializable. For instance, extend the type of your result adding prototypes getter functions (no arguments), each calling and returning respective myFunctionX("myArgX") functions, so that they'll appear in the JSON.stringify process.

My work around,
So this problem troubled me for some time. Amplify Cache didn't seem to work and caching username and password is a bad idea, however my work around was just include the username and password in the Require-New-Password form, so I have 4 inputs instead of just newPassword & confirmPassword which now is username, oldPassword, newPassword, and confirmPassword.
https://github.com/aws-amplify/amplify-js/issues/1715#issuecomment-642733574

Related

React JS & Axios chaining promies

I am developing a react js application and we are using a promise based library axios for calling APIs.
Now, in the initial part of application, user gets a login page, when the login is successful, we contact different systems to retrieve some extra information about user.
axios
.get('url to authentication endpoint') // 1st call
.then(response => {
// if login is successful
// 1. retrieve the user preferences like, on the customised screens what fields user wanted to see
axios.get('user preference endpoint') // 2nd call
// 2. send a request to one more external systems, which calculates what user can see and not based on LDAP role
axios.get('role calculation endpoint') // 3rd call
})
.catch(error => {
})
Now I can see that I can use
axios.all()
for second and third call, but with promised based client, how to chain first and second call? To retrieve user preferences, I have to wait for user to be authenticated.
How to chain this calls in a promise based way, rather than callback style?
as mentioned in the thread for this Github issue, axios() and axios.all() return Promise objects which can be chained however you see fit:
axios.get('/auth')
.then(function(response) {
return axios.all([ axios.get('/preferences'), axios.get('/roles') ]);
})
.then(function(responses) {
const [
preferencesResponse,
rolesResponse
] = responses;
// do more things
})
.catch(function(error) {
console.log(error);
});
Dan O's answer is very good and it works perfectly but it's much readable using async/await although it's also working with promises under the hoood
async yourReactClassFunction(){
try{
let getAuth = await axios.get('/auth');
//if login not successful return;
let result = await Promise.all([axios.get('/preferences'), axios.get('/roles')]);
//Do whatever with the results.
}catch(e){
//TODO error handling
}
}
Although it's the same thing, 'feels' more readable in my very subjective opinion

Using React to render flash messages from Express

I've searched around a lot but have been unable to find a simple way to get flash messages from Express and render them in React.
I need to access the data on my Express server, but what is the best way of storing this and passing it down to React? I was thinking of passing an object down when the React index.html file is rendered, but I'm not sure how I can access this data, or send the correct data when certain events happen, for example a user enters the wrong password.
I solved the issue.
I simply have a variable in my session called flash which is set to false by default.
In the correct part of the passport flow I redefine this to a string, depending on the error. I have a React action and reducer to get this data and if it's truthy, render it to the screen. When the component unmounts or the site is refreshed I reset it to false.
EDIT: I have found a better solution
1. In the passport middleware set an optional message if something goes wrong.
return done(null, false, { message: 'Email not found' });
2. In the login route send this information as a response.
router.post('/login', (req, res, next) => {
passport.authenticate('local-login', (e, user, info) => {
if(e) return next(e);
if(info) return res.send(info);
req.logIn(user, e => {
if(e) return next(e);
return res.send(user);
});
})(req, res, next);
});
3. Handle the submission and response in a Redux action generator. If the user authenticates, then the message property will be undefined.
const res = await axios.post('/auth/login', { email, password });
dispatch({
type: 'FLASH',
payload: res.data.message
});
4. In the reducer, the state will be either a string or false:
return action.payload || false;
5. Then it's a question of rendering the state to the screen. Another action can be sent when the component unmounts to reset the state.
Hope this helps someone else out there.
expressjs/flash will place an array of flash objects onto res.locals. Per the docs: https://github.com/expressjs/flash#reslocalsflash
res.locals.flash
An array of flash messages of the form:
{
"type": "info",
"message": "message"
}
From my understanding, anything placed on res.locals is available in the global scope. In other words, you should be able to do window.flash which should return an Array of flash objects.
So you would simply loop over the array as you would normally in JavaScript. That is just my guess.
const makeFlashElement = ({type, message}) => {
return (
<div>
<h1>message</h1>
<h2>type</h2>
</div>
)
}
for (message in flash) {
makeFlashElement(message)
// ...
}
Typically you'd return a JSON response which React can easily digest.
See Karl Taylor's comment.

auth0 - invalid user_metadata type

I am using Auth0 for user management and I am trying to add some default user_metadata on signup. My signup code looks like this:
// signs a user up
signup(email, password, callback, metadata){
const defaultVals = {
app_complete: false,
app_decision: 'unknown',
app_term: this.getAppTerm(),
nickname: '',
middle_initial: '',
current_age: '10',
}
const meta = Object.assign({}, defaultVals, metadata);
console.log(meta);
this.auth0.redirect.signupAndLogin({
connection: 'Username-Password-Authentication',
email,
password,
user_metadata: meta,
}, function(err, authResult) {
if (err != undefined) {
callback(err);
console.log(err);
return;
}
});}
The problem is, whenever I try to pass the user_metadata attribute app_complete: false, the API returns an error saying that the data type false is invalid, and that only strings are allowed. Error message: invalid user_metadata.app_complete type (only strings are allowed).
I know JSON allows for types other than strings, because I can change this to false through the Auth0 user dashboard. Why is the signup method for the auth0 WebAuth object returning this error?
Thanks.
This is because for signup in the user_metadata you can provide only strings as values.
The user metadata to be associated with the user. If set, the field must be an object containing no more than ten properties. Property names can have a maximum of 100 characters, and property values must be strings of no more than 500 characters.
PATCH on the other hand supports full JSON types in user_metadata.
Here is the reference in the github auth0-js issue
You can pass the user_metadata in the signupAndLogin() (/dbconnections/signup endpoint) method. However, it has some restriction as described below.
The user metadata to be associated with the user. If set, the field
must be an object containing no more than ten properties. Property
names can have a maximum of 100 characters, and property values must
be strings of no more than 500 characters.
To solve the issue, pass the property value as string.
Alternatively, instead of passing the user_metadata in the request, It is possible to use auth0 rules and update the user_metadata on the first login.
function (user, context, callback) {
var count = context.stats && context.stats.loginsCount ? context.stats.loginsCount : 0;
if (count > 1) {
return callback(null, user, context);
}
//update metadata
// https://auth0.com/docs/rules/guides/metadata#update-user_metadata
callback(null, user, context);
}

Caching in React

In my react App I have a input element. The search query should be memoized, which means that if the user has previously searched for 'John' and the API has provided me valid results for that query, then next time when the user types 'Joh', there should be suggestion for the user with the previously memoized values(in this case 'John' would be suggested).
I am new to react and am trying caching for the first time.I read a few articles but couldn't implement the desired functionality.
You don't clarify which API you're using nor which stack; the solution would vary somewhat depending on if you are using XHR requests or something over GraphQL.
For an asynchronous XHR request to some backend API, I would do something like the example below.
Query the API for the search term
_queryUserXHR = (searchTxt) => {
jQuery.ajax({
type: "GET",
url: url,
data: searchTxt,
success: (data) => {
this.setState({previousQueries: this.state.previousQueries.concat([searchTxt])
}
});
}
You would run this function whenever you want to do the check against your API. If the API can find the search string you query, then insert that data into a local state array variable (previousQueries in my example).
You can either return the data to be inserted from the database if there are unknowns to your view (e.g database id). Above I just insert the searchTxt which is what we send in to the function based on what the user typed in the input-field. The choice is yours here.
Get suggestions for previously searched terms
I would start by adding an input field that runs a function on the onKeyPress event:
<input type="text" onKeyPress={this._getSuggestions} />
then the function would be something like:
_getSuggestions = (e) => {
let inputValue = e.target.value;
let {previousQueries} = this.state;
let results = [];
previousQueries.forEach((q) => {
if (q.toString().indexOf(inputValue)>-1) {
result.push(a);
}
}
this.setState({suggestions: results});
}
Then you can output this.state.suggestions somewhere and add behavior there. Perhaps some keyboard navigation or something. There are many different ways to implement how the results are displayed and how you would select one.
Note: I haven't tested the code above
I guess you have somewhere a function that queries the server, such as
const queryServer = function(queryString) {
/* access the server */
}
The trick would be to memorize this core function only, so that your UI thinks its actually accessing the server.
In javascript it is very easy to implement your own memorization decorator, but you could use existing ones. For example, lru-memoize looks popular on npm. You use it this way:
const memoize = require('lru-memoize')
const queryServer_memoized = memoize(100)(queryServer)
This code keeps in memory the last 100 request results. Next, in your code, you call queryServer_memoized instead of queryServer.
You can create a memoization function:
const memo = (callback) => {
// We will save the key-value pairs in the following variable. It will be our cache storage
const cache = new Map();
return (...args) => {
// The key will be used to identify the different arguments combination. Same arguments means same key
const key = JSON.stringify(args);
// If the cache storage has the key we are looking for, return the previously stored value
if (cache.has(key)) return cache.get(key);
// If the key is new, call the function (in this case fetch)
const value = callback(...args);
// And save the new key-value pair to the cache
cache.set(key, value);
return value;
};
};
const memoizedFetch = memo(fetch);
This memo function will act like a key-value cache. If the params (in our case the URL) of the function (fetch) are the same, the function will not be executed. Instead, the previous result will be returned.
So you can just use this memoized version memoizedFetch in your useEffect to make sure network request are not repeated for that particular petition.
For example you can do:
// Place this outside your react element
const memoizedFetchJson = memo((...args) => fetch(...args).then(res => res.json()));
useEffect(() => {
memoizedFetchJson(`https://pokeapi.co/api/v2/pokemon/${pokemon}/`)
.then(response => {
setPokemonData(response);
})
.catch(error => {
console.error(error);
});
}, [pokemon]);
Demo integrated in React

NodeJS create json object from array

I'm working with some arrays in node and I want to send if as one JSON object to the front-end. I use express to do this. I have a model called User where I find users based on their email. That email is provided in an array. I do get the user object but I can't create one JSON object out of them!
I have tried some middleware but that didn't give me any result! https://www.npmjs.com/package/node.extend
var users = {};
for (var i = 0; i <emails.length; i++) {
User.findOne({
'email': project.students[i]
}, function (err, user) {
if (err) {
res.send(err);
}
// Fill the users object with each user found based on the email
});
}
console.log(users); // Should be one JSONObject
Thanks for the help!
You should be able to do this in a single query. It looks like you're using mongoose, so try something like this:
User.find({ email: { $in: emails } }, function(err, results) {
if (err) return res.send(err);
res.send(results);
});
It's also worth noting that javascript is single threaded. This means that a lot of operations happen asynchronously, meaning you have to wait for the operation to get done before you can move on. Your console logging statement above doesn't wait for the database operation to complete. You have to wait for the callback function to execute.
UPDATE: Also just noticed that you are looping over emails but then using project.students[i] within each iteration. I can't see the rest of your code, but this is just buggy code. You should be either looping over project.students or using emails[i] within each iteration.
UPDATE 2: It appears that you are wanting to send more than just an array of user with the response. So the first goal is to use a single query using the $in operator (see example above - you should be able to pass a list of emails to mongoose). Anything mongo-related, you always want to reduce the amount of queries to the database if you care at all about performance. The second task is to reformat your users and other data accordingly:
var finalResponse = { token: "12341234", users: null };
User.find({ email: { $in: emails } }, function(err, results) {
if (err) return res.send(err);
if (!results.length) return res.send(finalResponse);
// OPTION 1: Array of users (recommended)
finalResponse.users = results;
// OPTION 2: users object, keyed by the users email
finalResponse.users = {};
results.forEach(function(user) {
finalResponse.users[user.email] = user;
});
// FINALLY, send the response
resp.send(finalResponse);
});

Resources