Hi I am new to redux and authentication. I am creating a react/redux app where a user can login and be able to add a story by submitting a form. I am able to login but when I get to the story creation page, I click submit and I get a POST http://localhost:3000/api/v1/stories 401 (Unauthorized) error.
I am logging in using an API that gives a token on login. I then save the username and token to sessionstorage. But how would I fix this error?
App.js
import './App.scss';
import Login from './components/Login';
import { Router, Switch, Route, NavLink } from 'react-router-dom';
import PrivateRoute from './utils/PrivateRoute';
import CreateStory from './components/CreateStory';
import history from './utils/history';
function App() {
return (
<div className="App">
<Router history={history}>
<Switch>
<Route exact path="/" component={Login} />
<PrivateRoute path="/user" component={CreateStory}/>
</Switch>
</Router>
</div>
);
}
export default App;
PrivateRoute.js
import { useSelector } from 'react-redux'
// handle the private routes
function PrivateRoute({ component: Component, ...rest }) {
const getToken = useSelector((state)=> state.loginReducer.token)
console.log(getToken)
return (
<Route
{...rest}
render={(props) => getToken ? <Component {...props} /> : <Redirect to={{ pathname: '/', state: { from: props.location } }} />}
/>
)
}
export default PrivateRoute;
CreateStory.js
import React, { useState } from 'react'
import { createStory } from '../redux/actions'
import { useDispatch } from "react-redux";
const CreateStory = () => {
const [summary, setSummary] = useState("");
const [description, setDescription] = useState("");
const [type, setType] = useState("");
const [complexity, setcomplexity] = useState("");
const usedispatch = useDispatch();
const userCreateStory = (summary, description, type, complexity) => usedispatch(createStory({
'summary': summary,
'description': description,
'type': type,
'complexity': complexity
}));
const handleSummaryChange = e => {
setSummary(e.target.value)
}
const handleDescriptionChange = e => {
setDescription(e.target.value)
}
const handleTypeChange = e => {
setType(e.target.value)
}
const handleComplexityChange = e => {
setcomplexity(e.target.value)
}
const handleSubmit = e => {
e.preventDefault();
userCreateStory('a','b','c','d')
// setTimeout(()=> history.push("/user"), 1000 );
}
return (
<div>
<form className='create-story-form'>
<label for="summary">Summary:</label>
<input name="summary" type='text' onChange={handleSummaryChange}/>
<label for="desc">Description:</label>
<textarea name="desc" type='text' onChange={handleDescriptionChange}/>
<label for="type">Type:</label>
<select name="type">
<option value="enhancement">Enchancement</option>
<option value="bugfix">Bugfix</option>
<option value="development">Development</option>
<option value="qa">QA</option>
</select>
<label for="complexity">Complexity:</label>
<select name="complexity">
<option value="Low">Low</option>
<option value="Mid">Mid</option>
<option value="High">High</option>
</select>
<label for="time">Estimated time for completion:</label>
<input name="time" type='text' />
<label for="cost">Cost:</label>
<input name="cost" type='number' />
<button onClick={handleSubmit}>Submit</button>
</form>
</div>
)
}
export default CreateStory;
Login.js
import React, { useState } from "react";
import { useDispatch } from "react-redux";
import { login, roleChange } from '../redux/actions' //OUR ACTIONS
import { useSelector } from 'react-redux'
import history from '../utils/history';
import { withRouter } from 'react-router-dom';
const Login = () => {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const usedispatch = useDispatch();
const userLogin = (email, password) => usedispatch(login({'email': email, 'password': password }));
const switchToAdmin = () => usedispatch(roleChange('admin'));
const switchToUser = () => usedispatch(roleChange('user'));
const currentRole = useSelector((state)=> state.loginReducer.role)
const handleRoleChange = e => {
e.preventDefault();
if(currentRole === 'user')
switchToAdmin();
else if(currentRole === 'admin' )
switchToUser()
}
const handleEmailChange = e => {
setEmail(e.target.value)
}
const handlePasswordChange = e => {
setPassword(e.target.value)
}
const handleSubmit = e => {
e.preventDefault();
userLogin(email, password)
setTimeout(()=> history.push("/user"), 1000 );
}
const disabled = () => {
return email === "" || password === ""
}
return (
<div>
<form className='login-form'>
<input type='email' name='email' placeholder='Email' onChange={handleEmailChange}/>
<input type='password' name='password' placeholder='Password' onChange={handlePasswordChange}/>
<button type='submit' disabled={disabled()} onClick={handleSubmit}>Login</button>
</form>
<button onClick={handleRoleChange}>Switch to {currentRole === 'user' ? 'admin' : 'user'}</button>
</div>
)
}
export default withRouter(Login);
actionTypes.js
export const SET_LOGIN_STATE = "SET_LOGIN_STATE"
export const SET_ROLE_STATE = "SET_ROLE_STATE"
export const CREATE_STORY = "CREATE_STORY"
initialState.js:
import { getToken } from '../utils/Common'
export const initialState = {
isLoggedIn: false,
userId: '',
role: 'user',
token: getToken,
data: '',
};
reducers.js
import { initialState } from './initialState';
import * as t from './actionTypes';
export const loginReducer = (state = initialState, action) => {
switch (action.type) {
case t.SET_ROLE_STATE:
return {
...state,
role: action.payload,
};
case t.SET_LOGIN_STATE:
return {
...state,
...action.payload, // this is what we expect to get back from API call and login page input
isLoggedIn: true, // we set this as true on login
};
default:
return state;
}
};
export const storyReducer = (state = initialState, action) => {
switch (action.type) {
case t.CREATE_STORY:
return {
...state,
role: action.payload,
};
default:
return state;
}
}
actions.js:
import * as t from './actionTypes';
import { setUserSession } from '../utils/Common';
// this is what our action should look like which dispatches the "payload" to reducer
const setLoginState = (loginData) => {
return {
type: t.SET_LOGIN_STATE,
payload: loginData, //{ ...json, userId: email }
};
};
const setStoryState = (storyData) => {
return {
type: t.CREATE_STORY,
payload: storyData,
};
};
export const login = (loginInput) => { //our login action
const { email, password } = loginInput;
return (dispatch) => { // don't forget to use dispatch here!
return fetch('http://localhost:3000/api/v1/signin', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(loginInput),
})
.then((response) => response.json()) //json will be the response body
.then((json) => {
// if (json.msg === 'success') { // response success checking logic could differ
// console.log(json)
dispatch(setLoginState({ ...json, userId: email })); // our action is called here with object as parameter, this is our payload
//we appended json object to our state
// } else {
// alert('Login Failed', 'Email or Password is incorrect');
// }
setUserSession(json.token, json.lastName)
})
.catch((err) => {
alert('Login Failed', 'Some error occured, please retry');
console.log(err);
});
};
};
export const roleChange = role => {
return {
type: t.SET_ROLE_STATE,
payload: role
};
}
/**
* story input:
{
"summary": "string",
"description": "string",
"type": "string",
"complexity": "string"
}
*/
export const createStory = storyInput => {
const { summary, description, type, complexity } = storyInput;
return (dispatch) => { // don't forget to use dispatch here!
return fetch('http://localhost:3000/api/v1/stories', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(storyInput),
})
.then((response) => response.json()) //json will be the response body
.then((json) => {
// if (json.msg === 'success') { // response success checking logic could differ
console.log(json)
// dispatch(setStoryState({ // our action is called here with object as parameter, this is our payload
// summary: summary,
// description: description,
// type: type,
// complexity: complexity
// })); // our action is called here
// } else {
// alert('Login Failed', 'Email or Password is incorrect');
// }
})
.catch((err) => {
alert('Some error occured, please retry');
console.log(err);
});
};
}
Common.js
// return the user data from the session storage
export const getUser = () => {
const userStr = sessionStorage.getItem('user');
if (userStr) return JSON.parse(userStr);
else return null;
}
// return the token from the session storage
export const getToken = () => {
return sessionStorage.getItem('token') || null;
}
// remove the token and user from the session storage
export const removeUserSession = () => {
sessionStorage.removeItem('token');
sessionStorage.removeItem('user');
}
// set the token and user from the session storage
export const setUserSession = (token, user) => {
sessionStorage.setItem('token', token);
sessionStorage.setItem('user', JSON.stringify(user));
}
You'll have to pass the auth token from the sessionStorage to the header of API you are posting your story to :-
const token = sessionStorage.getItem('token'); //Add this line
return fetch('http://localhost:3000/api/v1/stories', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${token}` //Add this line
},
body: JSON.stringify(storyInput),
})
Related
When a user logs in, the background of their liked photos should change. Why does the background color only change when the page is reloaded and not after a successful login? There are too many requests happening at the same time and I don't know how to handle them properly. Maybe someone has any ideas.?
Login.js
import { useAppContext } from "../context/appContext";
function Login() {
const { handleClick } = useAppContext();
return (
<div>
<button onClick={() => handleClick()}>Log in</button>
</div>
);
}
export default Login;
Home.js
import { useAppContext } from "../context/appContext";
function Home() {
const [page, setPage] = useState(1);
const [query, setQuery] = useState("landscape");
const [images, setImages] = useState([]);
const [loading, setLoading] = useState(false);
const { token, username, getUserProfile, getToken } =
useAppContext();
const clientId = process.env.REACT_APP_UNSPLASH_KEY;
useEffect(() => {
if (window.location.search.includes("code=") && !token) {
getToken();
}
if (token) {
getUserProfile();
}
}, [token]);
const fetchImages = () => {
setLoading(true);
let params = {
page: page,
query: query,
per_page: 30,
};
let headers = {};
if (username) {
headers = {
Authorization: `Bearer ${token}`,
};
} else {
params = { ...params, client_id: clientId };
}
axios
.get("https://api.unsplash.com/search/photos", { headers, params })
.then((response) => {
setImages([...images, ...response.data.results]);
})
.catch((error) => {
console.log(error);
})
.finally(() => {
setLoading(false);
});
setPage(page + 1);
};
useEffect(() => {
fetchImages();
setQuery("");
}, []);
return (
<div>
<Login />
{loading && <Loader />}
<ImageList images={images} fetchImages={fetchImages} />
</div>
);
}
export default Home;
appContext.js
import React, { useReducer, useContext } from "react";
import reducer from "./reducer";
import axios from "axios";
import {
SET_TOKEN,
SET_USERNAME,
LOGOUT_USER,
} from "./actions";
const token = localStorage.getItem("token");
const username = localStorage.getItem("username");
const initialState = {
token: token,
username: username,
};
const client_id = process.env.REACT_APP_UNSPLASH_KEY;
const redirect_uri = "http://localhost:3000/";
const api_auth_uri = "https://unsplash.com/oauth/authorize";
const api_token_uri = "https://unsplash.com/oauth/token";
const response_type = "code";
const scope = [
"public",
"read_user",
"write_user",
"read_photos",
"write_photos",
"write_likes",
"write_followers",
"read_collections",
"write_collections",
];
const client_secret = process.env.REACT_APP_UNSPLASH_SECRET;
const grant_type = "authorization_code";
const AppContext = React.createContext();
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const handleClick = async () => {
window.location.href = `${api_auth_uri}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope.join(
"+"
)}`;
};
const getToken = async () => {
const coder = window.location.search.split("code=")[1];
try {
const { data } = await axios.post(
`${api_token_uri}?client_id=${client_id}&client_secret=${client_secret}&redirect_uri=${redirect_uri}&code=${coder}&grant_type=${grant_type}`
);
const { access_token } = data;
localStorage.setItem("token", access_token);
dispatch({
type: SET_TOKEN,
payload: { access_token },
});
const newUrl = window.location.href.split("?")[0];
history.replaceState({}, document.title, newUrl);
} catch (error) {
logoutUser();
}
};
const getUserProfile = async () => {
try {
const { data } = await axios.get(`https://api.unsplash.com/me`, {
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + state.token,
},
});
const { username } = data;
localStorage.setItem("username", username);
dispatch({
type: SET_USERNAME,
payload: { username },
});
} catch (error) {
console.log(error)
}
};
const logoutUser = () => {
dispatch({ type: LOGOUT_USER });
localStorage.removeItem("token");
};
return (
<AppContext.Provider
value={{
...state,
handleClick,
getToken,
getUserProfile,
logoutUser,
}}
>
{children}
</AppContext.Provider>
);
};
const useAppContext = () => useContext(AppContext);
export { AppProvider, initialState, useAppContext };
ImageList.js
import React, { useState } from "react";
import "../styles/ImageList.scss";
function ImageList({ images }) {
return (
<div className="result">
{images?.map((image, index) => (
<div
style={{
backgroundColor: image.liked_by_user ? "red" : "",
}}
key={image.id}
>
<div key={image.id}>
<img src={image.urls.small} alt={image.alt_description} />
</div>
</div>
))}
</div>
);
}
export default ImageList;
For some reason my Login component is not redirecting a successful login to the right page. I am using a Spring boot backend with a React frontend and am pretty sure I can do this with react on the frontend by using history.push('/profile') to allow a successful login to be redirected to the /profile page but for some reason it stays on /login even after successfully logging in. Any ideas what I am doing wrong? Thank you!
Login.jsx:
import React, {useState, useEffect} from 'react';
import {useDispatch, useSelector} from 'react-redux';
import {Link} from 'react-router-dom';
import {Formik, Field, Form, ErrorMessage} from 'formik';
import * as Yup from 'yup';
import {login} from '../../slices/auth';
import {clearMessage} from '../../slices/messages';
const Login = (props) => {
const [loading, setLoading] = useState(false);
const {isLoggedIn} = useSelector((state) => state.auth);
const {message} = useSelector((state) => state.message);
const dispatch = useDispatch();
useEffect(() => {
dispatch(clearMessage());
}, [dispatch]);
const initialValues = {
username: '',
password: '',
};
const validationSchema = Yup.object().shape({
username: Yup.string().required('Please enter your username'),
password: Yup.string().required('Please enter your password'),
});
const handleLogin = (formValue) => {
const {username, password} = formValue;
setLoading(true);
dispatch(login({username, password}))
.unwrap()
.then(() => {
props.history.push('/profile');
window.location.reload();
})
.catch(() => {
setLoading(false);
});
};
if (isLoggedIn) {
return <Link to='/profile' />;
}
return (
<div className='login'>
<div className='card card-container'>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleLogin}
>
<Form>
<div className='form-group'>
<Field
name='username'
type='text'
className='form-control'
placeholder='Username'
/>
<ErrorMessage
name='username'
component='div'
className='alert alert-danger'
/>
</div>
<div className='form-group'>
<Field
name='password'
type='password'
className='form-control'
placeholder='Password'
/>
<ErrorMessage
name='password'
component='div'
className='alert alert-danger'
/>
</div>
<div className='form-group'>
<button
type='submit'
className='btn btn-primary btn-block'
disabled={loading}
>
{loading && (
<span className='spinner-border spinner-border-sm'></span>
)}
<span>Login</span>
</button>
</div>
</Form>
</Formik>
</div>
{message && (
<div className='form-group'>
<div className='alert alert-danger' role='alert'>
{message}
</div>
</div>
)}
</div>
);
};
export default Login;
auth.js:
// We’re gonna import AuthService to make asynchronous HTTP requests with trigger one or more dispatch in the result.
// – register(): calls the AuthService.register(username, email, password) & dispatch setMessage if successful/failed
// – login(): calls the AuthService.login(username, password) & dispatch setMessage if successful/failed
// – logout(): calls the AuthService.logout().
// setMessage is imported from message slice that we’ve created above.
// We also need to use Redux Toolkit createAsyncThunk which provides a thunk that will take care of the action types and dispatching the right actions based on the returned promise.
//There are 3 async Thunks to be exported:
// register
// login
// logout
import {createSlice, createAsyncThunk} from '#reduxjs/toolkit';
import {setMessage} from './messages';
import AuthService from '../services/auth.service';
const user = JSON.parse(localStorage.getItem('user'));
export const register = createAsyncThunk(
'auth/register',
async ({username, email, password}, thunkAPI) => {
try {
const response = await AuthService.register(username, email, password);
thunkAPI.dispatch(setMessage(response.data.message));
return response.data;
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const login = createAsyncThunk(
'auth/login',
async ({username, password}, thunkAPI) => {
try {
const data = await AuthService.login(username, password);
return {user: data};
} catch (error) {
const message =
(error.response &&
error.response.data &&
error.response.data.message) ||
error.message ||
error.toString();
thunkAPI.dispatch(setMessage(message));
return thunkAPI.rejectWithValue();
}
}
);
export const logout = createAsyncThunk('auth/logout', async () => {
await AuthService.logout();
});
const initialState = user
? {isLoggedIn: true, user}
: {isLoggedIn: false, user: null};
const authSlice = createSlice({
name: 'auth',
initialState,
extraReducers: {
[register.fulfilled]: (state, action) => {
state.isLoggedIn = false;
},
[register.rejected]: (state, action) => {
state.isLoggedIn = false;
},
[login.fulfilled]: (state, action) => {
state.isLoggedIn = true;
state.user = action.payload.user;
},
[login.rejected]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
[logout.fulfilled]: (state, action) => {
state.isLoggedIn = false;
state.user = null;
},
},
});
const {reducer} = authSlice;
export default reducer;
messages.js:
// This updates message state when message action is dispatched from anywhere in the application. It exports 2 action creators:
// setMessage
// clearMessage
import {createSlice} from '#reduxjs/toolkit';
const initialState = {};
const messageSlice = createSlice({
name: 'message',
initialState,
reducers: {
setMessage: (state, action) => {
return {message: action.payload};
},
clearMessage: () => {
return {message: ''};
},
},
});
const {reducer, actions} = messageSlice;
export const {setMessage, clearMessage} = actions;
export default reducer;
Remove the window.location.reload(); from the handleLogin function, it is reloading your app and killing the navigation action.
Since it appears you are using react-router-dom v6, there are no route props and there is no history object. Instead, there is a useNavigate hook.
import { useNavigate } from 'react-router-dom';
...
const Login = (props) => {
const navigate = useNavigate();
...
const handleLogin = (formValue) => {
const { username, password } = formValue;
setLoading(true);
dispatch(login({ username, password }))
.unwrap()
.then(() => {
navigate('/profile', { replace: true });
})
.catch(() => {
setLoading(false);
});
};
...
I am trying to update user information (name, email, password) but getting two errors.
If I try to enter & update name or Email (DATA DOES NOT GET UPDATED) I get 404 error :
Request:
Response:
But, if I try to enter and update Password it gets UPDATED (as I have to enter new password while logging in again) but it shows these error's immediately after submit button:
I have tried updating user data by putting data directly through thunder client & it's getting updated:
Here is my source code:
FRONTEND
ProfileScreen.js
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import ErrorMessage from "../../components/ErrorMessage/ErrorMessage";
import Message from "../../components/Message/Message";
import Loader from "../../components/Loader/Loader";
import {
getUserDetails,
updateUserProfile,
} from "../../redux/actions/userActions";
import "./ProfileScreen.scss";
import { USER_UPDATE_PROFILE_RESET } from "../../redux/constants/userConstants";
const ProfileScreen = ({ location, history }) => {
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [message, setMessage] = useState(null);
const regex =
/^(?=.*[A-Za-z])(?=.*\d)(?=.*[#$!%*#?&])[A-Za-z\d#$!%*#?&]{8,}$/;
const dispatch = useDispatch();
const userDetails = useSelector((state) => state.userDetails);
const { loading, error, user } = userDetails;
const userLogin = useSelector((state) => state.userLogin);
const { userInfo } = userLogin;
const userUpdateProfile = useSelector((state) => state.userUpdateProfile);
const { success } = userUpdateProfile;
useEffect(() => {
if (!userInfo) {
history.push("/login");
} else {
if (!user.name || !user || success) {
dispatch({ type: USER_UPDATE_PROFILE_RESET });
dispatch(getUserDetails("profile"));
} else {
setName(user.name);
setEmail(user.email);
}
}
}, [history, userInfo, dispatch, user, success]);
const passwordHandler = (e) => {
e.preventDefault();
!regex.test(password)
? setMessage(
"Password must contain atleast 8 characters & one alphabet, number & special character"
)
: password !== confirmPassword
? setMessage("Passwords do not match!")
: dispatch(updateUserProfile({ id: user._id, password }));
};
const enameHandler = (e) => {
e.preventDefault();
dispatch(updateUserProfile({ id: user._id, name, email, password }));
};
return (
<>
<div className="profile-container">
<div className="profile">
{message && <ErrorMessage error={message} />}
{error && <ErrorMessage error={error} />}
{success && <Message success={"Profile Updated"} />}
<div className="profile-form">
<h2>User Profile</h2>
{loading ? (
<Loader />
) : (
<div>
<form onSubmit={enameHandler}>
<div className="profile-form-items">
<h3> Update Name or Email</h3>
<input
className="profile-input"
type="name"
placeholder="New Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
className="profile-input"
type="email"
placeholder="New Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<button type="submit" value="submit">
Update
</button>
</div>
</form>
<form onSubmit={passwordHandler}>
<div className="profile-form-items">
<h3>Update Password</h3>
<input
className="profile-input"
type="password"
placeholder="New Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<input
className="profile-input"
type="password"
placeholder="Confirm New Password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<button type="submit" value="submit">
Update
</button>
</div>
</form>
</div>
)}
</div>
</div>
</>
);
};
export default ProfileScreen;
userAction.js
import axios from "axios";
import {
USER_DETAILS_FAIL,
USER_DETAILS_REQUEST,
USER_DETAILS_SUCCESS,
USER_LOGIN_FAIL,
USER_LOGIN_REQUEST,
USER_LOGIN_SUCCESS,
USER_LOGOUT,
USER_REGISTER_FAIL,
USER_REGISTER_REQUEST,
USER_REGISTER_SUCCESS,
USER_UPDATE_PROFILE_FAIL,
USER_UPDATE_PROFILE_REQUEST,
USER_UPDATE_PROFILE_SUCCESS,
} from "../constants/userConstants";
export const login = (email, password) => async (dispatch) => {
try {
dispatch({
type: USER_LOGIN_REQUEST,
});
const config = {
headers: {
"Content-Type": "application/json",
},
};
const { data } = await axios.post(
"/api/users/login",
{ email, password },
config
);
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
dispatch({
type: USER_LOGIN_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
export const logout = () => (dispatch) => {
localStorage.removeItem("userInfo");
dispatch({
type: USER_LOGOUT,
});
};
export const register = (name, email, password) => async (dispatch) => {
try {
dispatch({
type: USER_REGISTER_REQUEST,
});
const config = {
headers: {
"Content-Type": "application/json",
},
};
const { data } = await axios.post(
"/api/users",
{ name, email, password },
config
);
dispatch({
type: USER_REGISTER_SUCCESS,
payload: data,
});
dispatch({
type: USER_LOGIN_SUCCESS,
payload: data,
});
localStorage.setItem("userInfo", JSON.stringify(data));
} catch (error) {
dispatch({
type: USER_REGISTER_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
export const getUserDetails = (id) => async (dispatch, getState) => {
try {
dispatch({
type: USER_DETAILS_REQUEST,
});
const {
userLogin: { userInfo },
} = getState();
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`,
},
};
const { data } = await axios.get(`/api/users/${id}`, config);
dispatch({
type: USER_DETAILS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: USER_DETAILS_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
export const updateUserProfile = (user) => async (dispatch, getState) => {
try {
dispatch({
type: USER_UPDATE_PROFILE_REQUEST,
});
const {
userLogin: { userInfo },
} = getState();
const config = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${userInfo.token}`,
},
};
console.log("UPDATE Action called");
const { data } = await axios.put(`/api/users/profile`, user, config);
dispatch({
type: USER_UPDATE_PROFILE_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: USER_UPDATE_PROFILE_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
userReducer.js
import {
USER_UPDATE_PROFILE_FAIL,
USER_UPDATE_PROFILE_REQUEST,
USER_UPDATE_PROFILE_SUCCESS,
} from "../constants/userConstants";
export const userUpdateProfileReducer = (state = {}, action) => {
switch (action.type) {
case USER_UPDATE_PROFILE_REQUEST:
return { loading: true };
case USER_UPDATE_PROFILE_SUCCESS:
return { loading: false, success: true, userInfo: action.payload };
case USER_UPDATE_PROFILE_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
store.js
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
// reducers
import {
userLoginReducer,
userRegisterReducer,
userDetailsReducer,
userUpdateProfileReducer,
} from "./reducers/userReducers";
const reducer = combineReducers({
userLogin: userLoginReducer,
userRegister: userRegisterReducer,
userDetails: userDetailsReducer,
userUpdateProfile: userUpdateProfileReducer,
});
const userInfoFromStorage = localStorage.getItem("userInfo")
? JSON.parse(localStorage.getItem("userInfo"))
: null;
const initialState = {
userLogin: { userInfo: userInfoFromStorage },
};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
BACKEND
userRoutes.js
const express = require("express");
const {
authUser,
getUserProfile,
registerUser,
updateUserProfile,
} = require("../controllers/userController");
const protect = require("../middleware/authMiddleware");
const router = express.Router();
router.route("/").post(registerUser);
router.post("/login", authUser);
router
.route("/profile")
.get(protect, getUserProfile)
.put(protect, updateUserProfile);
module.exports = router;
userController.js
// #description: Update user profile
// #route: PUT /api/users/profile
// #access: Private
exports.updateUserProfile = async (req, res, next) => {
try {
const user = await User.findById(req.user._id);
if (user) {
user.name = req.body.name || user.name;
user.email = req.body.email || user.email;
if (req.body.password) {
user.password = req.body.password;
}
const updatedUser = await user.save();
res.json({
_id: updatedUser._id,
name: updatedUser.name,
email: updatedUser.email,
isAdmin: updatedUser.isAdmin,
token: generateToken(updatedUser._id),
});
}
} catch (error) {
error = new Error("User not found");
error.status = 404;
next(error);
}
};
authMiddleware.js
const jwt = require("jsonwebtoken");
const User = require("../models/userModel");
const protect = async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
try {
token = req.headers.authorization.split(" ")[1];
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select("-password");
next();
} catch (error) {
error = new Error("Not Authorized!!");
error.status = 401;
next(error);
}
}
if (!token) {
const error = new Error("Not Authorized!!, No Token!!");
error.status = 401;
next(error);
}
};
module.exports = protect;
The error is (most probably) in
const user = await User.findById(req.user._id);
check if you are getting the "req.user._id" correctly. And also check your authMiddlwere if there is any error in getting req.user._id .
I am building a react application and part of the application is a quiz section. At the end of the quiz there is a button which can save the user score in the quiz to the database.
This is my express route
// #route Put api/profile/saveScore/:id
// #desc Save users quiz score to profile
// #access Private
router.put('/saveScore/:topic_id', checkObjectId('topic_id'), auth, async (req, {params: {topic_id } }, res) => {
const score = req.body.score
const topic = topic_id
const newUserTopic = {
score,
topic,
}
try {
const profile = await Profile.findOne({ user: req.user.id });
profile.topics.unshift(newUserTopic);
await profile.save();
res.json(profile)
} catch (err) {
console.error(err.message);
res.status(500).send('Server Error');
}
})
The express route works no bother in postman so thinking the issue must be more on the react side.
This is my action route
// Save Quiz Score to users profile
export const saveScore = (topicId, payload) => async (dispatch) => {
try {
const res = await api.put(`/profile/saveScore/${topicId}`, payload);
dispatch({
type: GET_PROFILE,
payload: res.data
});
dispatch(setAlert('Topic Saved', 'success'));
} catch (err) {
const errors = err.response.data.errors;
if(errors) {
errors.forEach(error => dispatch(setAlert(error.msg, 'danger')))
}
dispatch({
type: PROFILE_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
This is my component
import React, { useEffect, useState, Fragment } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import Spinner from '../layout/Spinner';
import QuizItem from './QuizItem';
import { getTopicById } from '../../actions/topic';
import { saveScore} from '../../actions/profile';
import { SaveScoreForm } from './SaveScoreForm';
const Quiz = ({ getTopicById, saveScore, topic: { topic, loading }, match }) => {
useEffect(() => {
getTopicById(match.params.id);
}, [getTopicById, match.params.id])
const [currentIndex, setCurrentIndex] = useState(0);
const [score, setScore] = useState(0);
const [showAnswers, setShowAnswers] = useState(false)
const [formData, setFormData] = useState({ score })
const handleAnswer = (answer) => {
if(!showAnswers) {
if(answer === topic[currentIndex].correct_answer) {
setScore(score + 1);
}
}
setShowAnswers(true);
};
const handleNextQuestion = () => {
setShowAnswers(false);
setCurrentIndex(currentIndex + 1);
}
console.log(currentIndex)
const onChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value })
}
const onSubmit = (e) => {
e.preventDefault();
const payload = new FormData();
payload.append('score', formData.score)
saveScore(payload, match.params.id);
}
return topic.length > 0 ? (
<div className='container'>
{currentIndex >= topic.length ? (
<Fragment>
<SaveScoreForm topic={topic} score={score} />
<form
onSubmit={e => onSubmit(e)}
>
<input
type='hidden'
value={score}
onChange={(e) => onChange(e)}
/>
<input type='submit' className='btn btn-primary1 my-1' />
</form>
</Fragment>
) : (
<QuizItem
key={topic.question}
topic={topic[currentIndex]}
showAnswers={showAnswers}
handleNextQuestion={handleNextQuestion}
handleAnswer={handleAnswer}
/>
)}
</div>
) : (
<Spinner/>
)
}
Quiz.prototype = {
getTopicById: PropTypes.func.isRequired,
topic: PropTypes.object.isRequired
}
const mapStateToProps = state => ({
topic: state.topic,
showAnswers: state.showAnswers,
handleNextQuestion: state.handleNextQuestion,
handleAnswer: state.handleAnswer
})
export default connect(mapStateToProps, { getTopicById })(Quiz)
Child component
import React from 'react'
export const SaveScoreForm = ({ score, topic, }) => {
return (
<div>
<div className='bg-primary1 p-2 my-4'>
<h1 className='large'>Review Your Score</h1>
<p className="lead">Quiz ended! Your score is: {(score/topic.length) * 100}%</p>
<p>Save your score to your profile or take the quiz again!</p>
</div>
</div>
);
};
export default SaveScoreForm;
TypeError: saveScore is not a function
Any help or pointers in the right direction would be very much appreciated.
Thanks
You are importing import { saveScore} from '../../actions/profile';
But then you have this prop
const Quiz = ({ getTopicById, saveScore
// ----------------------------^
which is overriding saveScore in your components context. Unless you are passing a saveScore prop while initialising <Quiz> it'll be undefined.
If you want to import the saveScore module just remove this prop variable.
So basically, I am calling loadUser which gets the User from the backend, it consistently works fine but appearantly whenever i refresh Dashboard page it does not fire off any actions, even though i would try calling loadUser inside of the useEffect which is in Dashboard page, still it no actions gets fired and i do not have access to the user, this is something i need cause i have to have access to the users ID. Also I am using redux thunk, I heard there are side effects that do exists, but still i would truly love to get help :)
I will link the github repo down below and paste code that seem related to this issue. If you do need anymore code the repo is here too:
https://github.com/tigerabrodi/eBuy
Dashboard Component
import React, {useEffect, Fragment, useState} from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import Pagination from '../products/Pagination';
import ProductItem from '../products/ProductItem';
import { getUserProducts } from '../../redux/product/product.actions';
import {loadUser} from "../../redux/auth/auth.actions";
const Dashboard = ({product: {products, loading, totalProducts}, loadUser, getUserProducts, auth: {user}}) => {
const [currentPage, setCurrentPage] = useState(1);
const [productsPerPage] = useState(6);
const paginate = pageNumber => setCurrentPage(pageNumber);
useEffect(() => {
getUserProducts(user._id, currentPage);
}, [currentPage, getUserProducts, user._id]);
return (
<Fragment>
<div className="container">
<div className="row">
<div className="col text-center">
<h1 className="text-monospace text-info display-2">Dashboard</h1>
<Link to="/add-product" className="btn btn-block btn-warning">Add Product <i className="far fa-money-bill-alt" /> </Link>
</div>
</div>
</div>
<br />
<div className="container">
<div className="row">
{products.map(product => (
<div className="col-md-4 col-6">
<ProductItem key={product._id} product={product} />
</div>
))};
<div className="col-12">
{products && (
<Pagination productsPerPage={productsPerPage} totalProducts={totalProducts} paginate={paginate} />
)}
</div>
</div>
</div>
</Fragment>
);
}
const mapStateToProps = state => ({
product: state.product,
auth: state.auth
})
export default connect(mapStateToProps, {getUserProducts, loadUser})(Dashboard);
auth reducer
import {AuthActionTypes} from "./auth.types";
const initialState = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
user: null
}
const authReducer = (state = initialState, action) => {
const {type, payload} = action;
switch (type) {
case AuthActionTypes.USER_LOADED:
return {
...state,
isAuthenticated: true,
loading: false,
user: payload
};
case AuthActionTypes.REGISTER_SUCCESS:
case AuthActionTypes.LOGIN_SUCCESS:
localStorage.setItem('token', payload.token);
return {
...state,
...payload,
isAuthenticated: true,
loading: false
};
case AuthActionTypes.REGISTER_FAIL:
case AuthActionTypes.AUTH_ERROR:
case AuthActionTypes.LOGIN_FAIL:
case AuthActionTypes.LOGOUT:
case AuthActionTypes.ACCOUNT_DELETED:
case AuthActionTypes.USER_ERROR:
localStorage.removeItem('token');
return {
...state,
token: null,
isAuthenticated: false,
loading: false
};
default:
return state;
}
}
export default authReducer
auth actions
import axios from "axios";
import {setAlert} from "../alert/alert.actions"
import {AuthActionTypes} from "./auth.types"
import setAuthToken from "../../utils/setAuthToken"
// Load User
export const loadUser = () => async dispatch => {
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get('/auth');
dispatch({
type: AuthActionTypes.USER_LOADED,
payload: res.data
});
} catch (err) {
dispatch({
type: AuthActionTypes.AUTH_ERROR
});
}
};
// Register User
export const register = ({ name, email, password }) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ name, email, password });
try {
const res = await axios.post('/auth/signup', body, config);
dispatch({
type: AuthActionTypes.REGISTER_SUCCESS,
payload: res.data
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach(error => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: AuthActionTypes.REGISTER_FAIL
});
}
};
// Login User
export const login = (email, password) => async dispatch => {
const config = {
headers: {
'Content-Type': 'application/json'
}
};
const body = JSON.stringify({ email, password });
try {
const res = await axios.post('/auth/signin', body, config);
dispatch({
type: AuthActionTypes.LOGIN_SUCCESS,
payload: res.data
});
dispatch(loadUser());
} catch (err) {
const errors = err.response.data.errors;
if (errors) {
errors.forEach(error => dispatch(setAlert(error.msg, 'danger')));
}
dispatch({
type: AuthActionTypes.LOGIN_FAIL
});
}
};
// Logout / Clear Profile
export const logout = () => dispatch => {
dispatch({ type: AuthActionTypes.LOGOUT });
};