I am fetching user data from the graphql backend through the apollo client. My goal is to keep the user logged in, so I signed jwt token with all user data, passed it to localStorage(only the token), decoded it, and passed all values to redux-store.
UserModel:
userSchema.methods.createJWT = function (payload) {
return jwt.sign({ ...payload }, process.env.JWT_SECRET, {
expiresIn: '1d',
});
};
userLogin:
await user.save();
return {
...user._doc,
id: user._id,
token: user.createJWT(user._doc),
}
reduxSlice
const userSlice = createSlice({
name: 'user',
initialState: {
userInfo: localStorage.getItem('jwtToken')
? jwtDecode(localStorage.getItem('jwtToken'))
: null,
},
reducers: {
loginUser: (state, action) => {
localStorage.setItem('jwtToken', action.payload.token);
state.userInfo = action.payload;
},
I am wondering if this is ok that the token is holding too much info
like:
{
"_id": "62a9ee3878c4979fedb471c5",
"username": "***",
"email": "***",
"password": "$2a$12$hN2lfCtEbqOOFSlHpapyfuxYAHdEGUYKeHY4BMK1YvYOtSG7zHwcS",
"isAdmin": false,
"shippingAddress": [],
"createdAt": "2022-06-15T14:35:36.877Z",
"updatedAt": "2022-06-16T09:04:59.367Z",
"__v": 0,
"firstName": "***",
"lastName": "***",
"shoeSize": 4,
"iat": 1655371413,
"exp": 1655457813
}
There is another effective way to save user data and keep him logged in?
it's not recommended (actually very dangerous) to return all information with the jwt token especially password. I think userId is enough!
But you can also return username, firstName, lastName, etc.
But in some cases even returning email address is not a good approach for some user privacy reasons.
I mean by that you have only to get the userId once there is a user and the credentials are correct, then :
const userToken = {
userId: user._id,
username: user.username,
};
return {
user,
token: user.createJWT(userData)
};
Now after signing the jwt token , you can set whatever data from user inside some redux state or even react context , (choose your favorite) , but DON'T set any password in the localStorage.
Update: at the end you should store the user from the payload like this :
state.userInfo = action.payload.user;
Btw you should check the localStorage only to get the token and verify it , then based on userId you need to fetch the user and store it, here is an example :
const getUser = React.useCallback(async (userId) => {
try {
const res = await axios.post('/auth/login', {userId}, {
credentials: 'include',
withCredentials: true
});
const { accessToken, user } = res.data;
setState((currentState: IState) => ({
...currentState,
user,
loading: false,
isAuth: !!accessToken,
accessToken
}));
} catch (err) {
setState((currentState: IState) => ({
...currentState,
user: null,
loading: false,
isAuth: false,
accessToken: ''
}));
}
}, []);
useEffect(() => {
getUser(userId);
}, [getUser]);
useEffect(() => {
const jwtToken = localStorage.getItem('jwtToken');
if (jwtToken && jwt_decode(jwtToken)) {
const { exp } = jwt_decode(jwtToken);
const currentTime = Date.now() / 1000;
if (exp < currentTime) {
getUserById(userId);
}
}
}, [getUser]);
Related
I am trying to make an ecommerce using react, redux toolkit and axios
the problem is that I want the user to log in and get his cart from the backend right after the login
it always fails the and says (unauthorized) when i first login because it can't find the token
then after refresh it says unauthorized one more time
after the third refresh it works
this is my get cart
export const getCart = createAsyncThunk("cart/getcart", async () => {
const response = await axios.get("http://127.0.0.1:8000/techcart/get_cart/", {
headers: {
Authorization: `Token ${token}`,
},
});
return response.data;
});
const cartSlice = createSlice({
name: "cart",
initialState: {
cart: [],
cartItemsIds :[],
},
builder.addCase(getCart.fulfilled, (state, action) => {
state.cart = action.payload;
and this is my login function
export const login = createAsyncThunk(
"auth/login",
async ({ email, password }, thunkAPI) => {
try {
const response = await axios.post(
"http://127.0.0.1:8000/techcart/login/",
{ username: email, password }
);
localStorage.setItem("user", JSON.stringify(response.data));
return response.data;
} catch (error) {}
}
);
const initialState = user
? { isLoggedIn: true, user }
: { isLoggedIn: false, user: null };
builder.addCase(login.fulfilled, (state, action) => {
state.isLoggedIn = true;
state.user = action.payload;
here is where i am doing the login
const HandleLogin = () => {
dispatch(login({ email, password }));
};
useEffect(()=> {
if(isLoggedIn){
navigate('/')
dispatch(getCart())
}
},[isLoggedIn])
Cart page
useEffect(() => {
dispatch(getCart());
}, []);
here is where im defining my token :
export let user = JSON.parse(localStorage.getItem("user")) ? JSON.parse(localStorage.getItem("user")) : null;
export let userId = user ? user.user_id : null;
export let token = user!=null ? user.token : null;
and here is where im importing it in my cart slice
import { user, token } from "../../constants";
im using redux persist to persist the state of my cart
if anyone can help me i'm so thankful
here is what happens
You're initializing your token directly when your js is executed. So when you retrieve it, it is undefined.
Ans when you do the login, you're indeed storing your token, but you're not updating it in your application.
I can see you're using redux, so store your token in your redux store, and before sending your api call to retrieve your cart, retrieve your token from redux, to always have the latest value of your token
I am implementing a JWT authentication in React and I've got this authentication context with register, login and logout:
function AuthProvider({children}) {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
const initialize = async () => {
try {
const accessToken = localStorage.getItem('accessToken');
if (accessToken && isValidToken(accessToken)) {
setSession(accessToken);
const response = await axios.get('/api/account/my-account');
const {user} = response.data;
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: true,
user,
},
});
} else {
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
user: null,
},
});
}
} catch (err) {
console.error(err);
dispatch({
type: 'INITIALIZE',
payload: {
isAuthenticated: false,
user: null,
},
});
}
};
initialize();
}, []);
const login = async (email, password) => {
const response = await axios.post('/api/account/login', {
email,
password,
});
const {accessToken, user} = response.data;
setSession(accessToken);
dispatch({
type: 'LOGIN',
payload: {
user,
},
});
};
const register = async (email, password, firstName, lastName) => {
const response = await axios.post('/api/account/register', {
email,
password,
firstName,
lastName,
});
const {accessToken, user} = response.data;
localStorage.setItem('accessToken', accessToken);
dispatch({
type: 'REGISTER',
payload: {
user,
},
});
};
const logout = async () => {
setSession(null);
dispatch({type: 'LOGOUT'});
};
return (
<AuthContext.Provider
value={{
...state,
method: 'jwt',
login,
logout,
register,
}}
>
{children}
</AuthContext.Provider>
);
}
export {AuthContext, AuthProvider};
As you can see, my API would generate an access token and would pass the user object so I can get his name and role on the next protected page.
Is this secure enough or are there better alternatives?
Thank you.
It is secure enough as long as you enforce security in the server.
Basically, whenever a user authenticates, you provide the JWT and an unencrypted payload with user information, such as identifiers like username and email as well as access rights. So far so good.
Now you are thinking: If I save this stuff in say, local storage, the user could change the stored access rights and give him/herself more rights. Well, while this could be true, it should serve for nothing because the user can only save this copy of the data, which probably controls the visibility or availability of menu items, buttons and the like according to the level of access. What should really drive the ability to perform an action is the JWT, and the user cannot alter this JWT without access to the secret or private key used to digitally sign the token.
So yes, I'd say it is secure enough. If you have a naughty user, know that said user cannot really post new data (for instance) if the access right is not digitally signed in the JWT.
Using redux filter to remove an item yet it's removing from the DB but the UI isn't updating, not sure if i'm missing something stupid or being dumb.
The expected functionality would be that it removes the item from the database and UI would update
If i manually refresh the page, it's as it should be.
This is inside my goalService
const deleteGoal = async (goalId, token) => {
const config = {
headers: {
Authorization: `Bearer ${token}`,
},
};
const response = await axios.delete(API_URL + goalId, config);
return response.data;
};
inside goalSlice
export const deleteGoal = createAsyncThunk(
"goals/delete",
async (id, thunkAPI) => {
try {
const token = thunkAPI.getState().auth.user.token;
return await goalService.deleteGoal(id, token);
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
return thunkAPI.rejectWithValue(message);
}
}
);
export const goalSlice = createSlice({
name: "goal",
initialState,
reducers: {
reset: (state) => initialState,
},
extraReducers: (builder) => {
builder
.addCase(deleteGoal.pending, (state) => {
state.isLoading = true;
})
.addCase(deleteGoal.fulfilled, (state, action) => {
state.isLoading = false;
state.isSuccess = true;
state.goals = state.goals.filter(
(goal) => goal._id !== action.payload.id
);
})
.addCase(deleteGoal.rejected, (state, action) => {
state.isLoading = false;
state.isError = true;
state.message = action.payload;
});
},
});
Edit 1:
goals: {
goals: [
{
user: '624dfed264387649da83d8db',
text: 'dylanjcain',
_id: '624f53d6fd65e29ed17506e3',
createdAt: '2022-04-07T21:12:54.748Z',
updatedAt: '2022-04-07T21:12:54.748Z',
__v: 0
}
],
isError: false,
isSuccess: true,
isLoading: false,
message: ''
}
Response from API
{
"_id": "624f554afd65e29ed17506e6",
"user": "624dfed264387649da83d8db",
"text": "test123",
"createdAt": "2022-04-07T21:19:06.435Z",
"updatedAt": "2022-04-07T21:19:06.435Z",
"__v": 0
}
Remember that the action payload for the thunk actions is the return value of the thunk. So in the delete case its return await goalService.deleteGoal(id, token); which ultimately resolves to return response.data; which is the response from your API.
So unless the API is returning a shape like { id: 123 } when you make your delete request your filter won't filter anything. Check to see that the API is giving the ID back. Otherwise you'll want to return {id: goalId} rather than response.data from your deleteGoal async function.
Solved:
(goal) => goal._id !== action.payload.id
So turns out i was being dumb, here's what i changed.
(goal) => goal._id !== action.payload._id
I have an issue with adding data from react app to DynamoDB. I have this code in my react app to submit data from a form to DynamoDB using axios:
export const addTodo = createAsyncThunk(
'todoApp/todos/addTodo',
async (todo, { dispatch, getState }) => {
const response = await axios.post('https://aws.us-west-2.amazonaws.com/default/todoApp-newTodo', todo);
const data = await response.data;
dispatch(getTodos());
return data;
}
);
and my Lambda function is this:
const AWS = require('aws-sdk');
const docClient = new AWS.DynamoDB.DocumentClient({region: "us-west-2"});
exports.handler = (event, context, callback) => {
console.log("Processing...");
const params = {
Item: {
id: "",
title: "",
notes: "",
startDate: "new Date(2018, 8, 3)",
dueDate: new Date(2018, 8, 5),
completed: false,
starred: false,
important: false,
deleted: false,
labels: [1]
},
TableName: "new-todo"
};
const response = {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
body: JSON.stringify('Hello from new Lambda!'),
};
docClient.put(params, function(err, data) {
if(err){
callback(err, null);
} else {
callback(null, data);
}
})
};
When I run the app, and submit the form, I get error message for: unique "key" prop.
I tried following code, and it successfully adds random key in database, but the info I entered in the form, will be gone.
this.setState({ id: response.data.id });
Have you tried adding unique value with "key" property?
And you can use this code for update state.
this.setState(prevState => ({
...prevState, // rest of the object
value: 'something' // the value we want to update
}}))
I have React web application with firebase auth (mail, Facebook, Google).
Google and Facebook work only after 2 login clicks.
The code is equal, just the provider is different.
import React from 'react';
import firebase from "firebase/app";
import { app } from "../../../../config/firebase";
const signupWithGoogle = (user, userInfo)=>{
app.firestore().collection('users').doc(user.uid).set({
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name});
const batch = app.firestore().batch();
const initData = [
{ Applied: { positionIds: [], title: 'Applied' } },
{ Contract: { positionIds: [], title: 'Contract' } },
{ Denied: { positionIds: [], title: 'Denied' } },
{ InProgress: { positionIds: [], title: 'In Progress' } },
{ ReceivedTask: { positionIds: [], title: 'Received Task' } },
];
initData.forEach((doc) => {
const docRef = app
.firestore()
.collection('users')
.doc( user.uid)
.collection('columns')
.doc(Object.keys(doc)[0]);
batch.set(docRef, Object.values(doc)[0]);
});
const batchCommit= batch.commit();
return batchCommit;
}
export const googleLogin = async (
history
) => {
var provider = new firebase.auth.GoogleAuthProvider();
await firebase.auth()
.signInWithPopup(provider)
.then( resp => {
let {user, credential,additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser) signupWithGoogle(user, userInfo);
}).then(()=>
history.push('/')
)
.catch((error) => {
console.error(error.message);
});
};
I saw this question, but didn't help.(Firebase Authentication Requires Two 'Login' Calls)
I had the same problem with Firebase Authentication with Facebook, I had to register two times to make it works.
The problem was in my HTLM, I used a form.
I changed for a simpler code, and it worked.
While waiting for where you call your function from, as your issue would relate to improper state management, here are some edits you can make to the code you have shared so far to squash some problems that it has.
In your signupWithGoogle function, you create a floating promise that should be included in the write batch that you use to create the /users/{userId}/columns collection. Because you use Object.keys(doc)[0] and Object.values(doc)[0], you should consider using an array of [docId, docData] pairs or a JSON-like object structure like so:
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
As you mentioned that a lot of your code is shared aside from the provider implementation, you should consider extracting the common code from those functions:
const initUserData = (user, userDocData) => {
// init write batch
const batch = app.firestore().batch();
// init ref to user data
const userDocRef = app.firestore().collection('users').doc(user.uid);
// queue user data upload
batch.set(userDocRef, userDocData);
// prepare data to add to the user's columns collection
const initColumnsData = {
Applied: { positionIds: [], title: 'Applied' },
Contract: { positionIds: [], title: 'Contract' },
Denied: { positionIds: [], title: 'Denied' },
InProgress: { positionIds: [], title: 'In Progress' },
ReceivedTask: { positionIds: [], title: 'Received Task' }
};
// queue columns data upload
Object.entries(initColumnsData)
.forEach(([docId, docData]) => {
const docRef = userDocRef
.collection('columns')
.doc(docId);
batch.set(docRef, docData);
});
// make the changes
return batch.commit();
}
const initUserDataForGoogle(user, userInfo) {
return initUserData(user, {
firstName: userInfo.profile.given_name,
lastName: userInfo.profile.family_name
});
}
const initUserDataForFacebook(user, userInfo) {
return initUserData(user, {
firstName: /* ... */,
lastName: /* ... */
});
}
When exporting a function to be called elsewhere, avoid causing "side effects" (like navigating using the History API) and don't trap errors (using .catch() without rethrowing the error). The calling code should handle the result and any errors itself.
export const loginWithGoogle = async () => {
const provider = new firebase.auth.GoogleAuthProvider();
return firebase.auth()
.signInWithPopup(provider)
.then(async resp => {
const {user, credential, additionalUserInfo: userInfo} = resp;
if (userInfo.isNewUser)
await initUserDataForGoogle(user, userInfo);
return user;
});
};
Then in your components, you'd use:
setLoading(true);
/* await/return */ loginWithGoogle()
.then(() => {
history.push('/');
// or
// setLoading(false)
// then do something
})
.catch((err) => {
console.error("loginWithGoogle failed: ", err);
setLoading(false);
setError("Failed to log in with Google!"); // <- displayed in UI to user
});