GraphQL + Redux + JWT User Authentication - reactjs

I am working on trying to set up JWT authentication using qraphql and redux.
Currently the register function is working and posts the new user to the database, but I cant seem to get the loadUser function to work.
authAction.js
const API_URL = 'https://my-server.com/api';
export const register = ({name, email, password}) => dispatch => {
axios.post(
API_URL, {
query: `mutation {
userSignup( name: "${name}", email: "${email}", password: "${password}"){
name,
email,
password
}
}
`,
})
.then(res =>
dispatch({
type: REGISTER_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch({
type: REGISTER_FAIL
})
})
}
export const loadUser = (email, password) => dispatch => {
dispatch({type: USER_LOADING})
axios.post(
API_URL, {
query: `query {
userLogin(email: "${email}", password: "${password}"){
email,
password,
token
}
}
`,
})
.then(res => dispatch({
type: USER_LOADED,
type: LOGIN_SUCCESS,
payload: res.data.data
}))
.catch(err => {
dispatch(retrunErrors(err.response.data, err.response.status));
dispatch({
type: AUTH_ERROR
});
});
}
Both of these functions should return the token, and the reducer should set the token to localStorage.
authReducer.js
...
case LOGIN_SUCCESS:
case REGISTER_SUCCESS:
localStorage.setItem('token', action.payload.token)
console.log(action.payload)
return {
...state,
...action.payload,
isAuthenticated: true,
isLoading: false,
}
...
I did have the login working using hooks (did not get to configuring the signUp), but it was all in app.js and it really needed to be broken down. I was told that i needed to move everything to redux, so here i am. I have dug through tons of documentation, but cant find the solution.
FWIW, here is the old app.js (its long)
const API_URL = 'https://my-server.com/api';
function App() {
const initialLoginState = {
isLoading: false,
userName: null,
userToken: null,
};
const setUser = (token, user) => {
if (token) {
axios.defaults.headers.common.Authorization = `Bearer ${token}`;
} else {
delete axios.defaults.headers.common.Authorization;
}
return {type: 'LOGIN', id: user.email, token: token};
};
// Set a user after login or using local (AsyncStorage) token
const setUserLocally = async (key, value) => {
// Set token & user object
// const items = [
// ['userToken', token],
// ['user', JSON.stringify(user)],
// ];
await localStorage.setItem(key, value);
};
const unsetUserLocally = () => {
// Remove token
localStorage.removeItem('userToken');
localStorage.removeItem('user');
return {type: 'LOGOUT'};
};
const loginReducer = (prevState, action) => {
switch (action.type) {
default:
return {
...prevState,
userToken: action.token,
isloading: false,
}
case 'RETRIEVE_TOKEN':
return {
...prevState,
userToken: action.token,
isloading: false,
};
case 'LOGIN':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
case 'LOGOUT':
return {
...prevState,
userName: null,
userToken: null,
isloading: false,
};
case 'REGISTER':
return {
...prevState,
userName: action.id,
userToken: action.token,
isloading: false,
};
}
};
const [loginState, dispatch] = useReducer(loginReducer, initialLoginState);
const auth = {
signIn: async (userName, password) => {
try {
const response = await axios
.post(
API_URL,
query({
operation: 'userLogin',
variables: {
email: userName,
password: password,
},
fields: ['user {name, email, role}', 'token'],
})
);
let message = 'Please try again.';
if (response.data.errors && response.data.errors.length > 0) {
message = response.data.errors[0].message;
} else if (response.data.data.userLogin.token !== '') {
const token = response.data.data.userLogin.token;
const user = response.data.data.userLogin.user;
setUserLocally('userToken', token).then(() => {
return setUserLocally('user', JSON.stringify(user));
});
dispatch(setUser(token, user));
}
} catch (error) {
console.log(error);
}
},
signOut: () => {
dispatch(unsetUserLocally());
},
signUp: () => {
// setUserToken('sdf');
// setIsLoading(false);
},
getCurrentUser: () => {
//return 'test'; //JSON.parse(AsyncStorage.getItem('userToken'));
let userArr = {};
const value = async () => {
await localStorage.multiGet(['user', 'userToken']).then(
(response) => {
response.forEach((item) => {
userArr[item[0]] = item[1];
});
return userArr;
},
);
};
return value;
},
};
console.log(loginState.userToken)
useEffect(() => {
let users = async () => {
let userToken;
try {
userToken = await localStorage.getItem('userToken');
} catch (e) {
console.log(e);
}
dispatch({type: 'RETRIEVE_TOKEN', token: userToken});
};
users();
}, []);
// if (loginState.isLoading === true) {
// return <Loading />;
// }
return (
<AuthContext.Provider value={auth}>
<Router>
<AuthSwitcher user={loginState.userToken}/>
</Router>
</AuthContext.Provider>
)
}
export default App

Related

How to use Context with PassportJs Auth

I'm trying to let users log in and register using google Auth, I'm using Context to let the app knows if the user is signed in or not, Register and Login works fine, but i didn't know how to configure Google login.
Register.jsx (Register Page):
const [username,setUsername] = useState("");
const [email,setEmail] = useState("");
const [password,setPassword] = useState("");
const [repeatPassword, setRepeatPassword] = useState("");
const { dispatch, isFetching } = useContext(Context);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "REGISTER_START" });
setError(false);
try {
const res = await axios.post("/register", {
username,
email,
password,
repeatPassword,
});
dispatch({ type: "REGISTER_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "REGISTER_FAILURE" });
setError(true);
}
};
Login.jsx (Login Page):
const userRef = useRef();
const passwordRef = useRef();
const { dispatch, isFetching } = useContext(Context);
const [error, setError] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
dispatch({ type: "LOGIN_START" });
try {
const res = await axios.post("/login", {
email: userRef.current.value,
password: passwordRef.current.value,
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.data });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
}
};
Context Actions.js
export const LoginStart = (userCredentials) => ({
type: "LOGIN_START",
});
export const LoginSuccess = (user) => ({
type: "LOGIN_SUCCESS",
payload: user,
});
export const LoginFailure = () => ({
type: "LOGIN_FAILURE",
});
// THE SAME FOR REGISTER
Context Reducer.js
const Reducer = (state, action) => {
switch (action.type) {
case "LOGIN_START":
return {
user: null,
isFetching: true,
error: false,
};
case "LOGIN_SUCCESS":
return {
user: action.payload,
isFetching: false,
error: false,
};
case "LOGIN_FAILURE":
return {
user: null,
isFetching: false,
error: true,
};
// THE SAME FOR REGISTER
default:
return state;
}
};
export default Reducer;
I tried something like this but it didn't work
const handleGoogleLogin = async (e) => {
window.open("http://localhost:4000/auth/google/callback", "_self");
dispatch({ type: "LOGIN_START" });
try {
const res = await fetch("/login/success", {
method: "GET",
credentials: "include",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"Access-Control-Allow-Credentials": true,
},
});
dispatch({ type: "LOGIN_SUCCESS", payload: res.user });
} catch (err) {
dispatch({ type: "LOGIN_FAILURE" });
setError(true)
console.log(err)
}
};
I don't if I have to make new actions just for social media authentication, or if this is the correct way but I'm missing something.
This is Auth.js route:
router.get("/login/success", (req, res) => {
if (req.user) {
res.status(200).json({
error: false,
message: "succesfull",
user: req.user,
});
} else {
res.status(403).json({ error: true, message: "Not Authorized" });
}
});

how to catch login error authorization 401 in React, Reducer

I'm trying to handle the login of a react app using reducer.
Here is the reducer fuction =>
export const AuthReducer = (initialState, action) => {
switch (action.type) {
case "REQ_LOGIN":
return {
...initialState,
loading: true
};
case "REQ_REGISTER":
return {
...initialState,
loading: true
};
case "LOGIN_OK":
return {
id: action.payload.user,
token: action.payload.token,
rol: action.payload.rol,
loading: false
};
case "LOGIN_FAIL":
return {
...initialState,
loading: false,
errorMessage: action.error
};
case "LOGOUT":
return {
id: undefined,
token: undefined,
loading: false
};
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
};
and this is the fuction I user to dispatch the login =>
const requestOptions = (sendData) => {
return {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sendData)
}
};
const fetchUserToDatabase = async (urlPetition, reqData) => {
try {
const res = await fetch(`${BASE_URL}/${urlPetition}`, requestOptions(reqData));
const data = await res.json();
return data;
} catch (error) {
console.error(error)
}
};
export const loginUser = async (dispatch, loginData) => {
localStorage.removeItem('currentUser');
try {
dispatch({ type: "REQ_LOGIN" });
const data = await fetchUserToDatabase("users/login", loginData)
if (data.data) {
dispatch({ type: "LOGIN_OK", payload: data.data });
localStorage.setItem('currentUser', JSON.stringify(data.data));
}
} catch (error) {
dispatch({ type: 'LOGIN_FAIL', error: error });
}
};
Then whenever I need to login I call loginUser(dispatch, loginForm), for example.
The problem is that the 401 unauthorized error is not being handled by the try, catch but is being sent in the response as res.status. so I literally don't know how to catch it outside the reducer and the actions function.
Error auth 401

I got Type-errors while converting my Slice.js file to ts (redux tool kit)

I just created my sample Redux tool kit with js files. now I am trying to convert them to typescript. some of the error is now fixed. but I have no idea how to fix the two errors below.
Property 'name' does not exist on type 'void'. <<<< also for email, password... and token
A computed property name must be of type 'string', 'number', 'symbol', or 'any' <<<< for signupUser.fulfilled, signupUser.pending... so on
Please refer to my comments within the code below and please help me to how to solve the problems
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
import axios from "axios";
export interface UserState {
username: string;
email: string;
isFetching: boolean;
isSuccess: boolean;
isError: boolean;
errorMessage: string;
}
interface check {
name: string;
email: string;
password: string;
}
const initialState: UserState = {
username: "",
email: "",
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: "",
}
export const signupUser = createAsyncThunk(
'users/signupUser',
//
//
// async ({ name, email, password} ... <<< This is the problem 1.
//
async ({ name, email, password }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
name,
email,
password,
}),
},
);
const data = await response.json();
console.log('data', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return { ...data, username: name, email };
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
return thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const loginUser = createAsyncThunk(
'users/login',
async ({ email, password }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/auth',
{
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
}),
},
);
const data = await response.json();
console.log('response', data);
if (response.status === 200) {
localStorage.setItem('token', data.token);
return data;
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const fetchUserBytoken = createAsyncThunk(
'users/fetchUserByToken',
async ({ token }, thunkAPI) => {
try {
const response = await fetch(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: token,
'Content-Type': 'application/json',
},
},
);
/* test case for Axios
const response = await axios.get(
'https://mock-user-auth-server.herokuapp.com/api/v1/users',
{
headers: {
Accept: 'application/json',
Authorization: token,
'Content-Type': 'application/json',
},
},
);
*/
const data = await response.json();
console.log('data', data, response.status);
if (response.status === 200) {
return { ...data };
}
return thunkAPI.rejectWithValue(data);
} catch (e) {
console.log('Error', e.response.data);
return thunkAPI.rejectWithValue(e.response.data);
}
},
);
export const userSlice = createSlice({
name: 'user',
initialState: {
username: '',
email: '',
isFetching: false,
isSuccess: false,
isError: false,
errorMessage: '',
},
reducers: {
clearState: state => {
state.isError = false;
state.isSuccess = false;
state.isFetching = false;
return state;
},
},
////
//// [signupUser.fulfilled] ... <<<< this is the problem 2.
////
////
extraReducers: {
[signupUser.fulfilled]: (state, { payload }) => {
console.log('payload', payload);
state.isFetching = false;
state.isSuccess = true;
state.email = payload.user.email;
state.username = payload.user.name;
},
[signupUser.pending]: state => {
state.isFetching = true;
},
[signupUser.rejected]: (state, { payload }) => {
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.message;
},
[loginUser.fulfilled]: (state, { payload }) => {
state.email = payload.email;
state.username = payload.name;
state.isFetching = false;
state.isSuccess = true;
return state;
},
[loginUser.rejected]: (state, { payload }) => {
console.log('payload', payload);
state.isFetching = false;
state.isError = true;
state.errorMessage = payload.message;
},
[loginUser.pending]: state => {
state.isFetching = true;
},
[fetchUserBytoken.pending]: state => {
state.isFetching = true;
},
[fetchUserBytoken.fulfilled]: (state, { payload }) => {
state.isFetching = false;
state.isSuccess = true;
state.email = payload.email;
state.username = payload.name;
},
[fetchUserBytoken.rejected]: state => {
console.log('fetchUserBytoken');
state.isFetching = false;
state.isError = true;
},
},
});
export const { clearState } = userSlice.actions;
export const userSelector = (state: { user: UserState}) => state.user;
You'll need to define a type for the argument you put in there. If you only want to type the first argument and are fine with the defaults for thunkAPI, you can use
export const signupUser = createAsyncThunk(
'users/signupUser',
async ({ name, email, password }: ArgumentType, thunkAPI) => {
Otherwise, you'll have to declare generic arguments as described in https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk
const fetchUserById = createAsyncThunk<
// Return type of the payload creator
MyData,
// First argument to the payload creator
number,
{
dispatch: AppDispatch
state: State
extra: {
jwt: string
}
}
>('users/fetchById', async (userId, thunkApi) => {
For problem #2 please see that we discourage the object map notation for extraReducers as it is less typesafe in general and does not work in TypeScript. Please use the builder notation as described in https://redux-toolkit.js.org/api/createSlice#the-extrareducers-builder-callback-notation

useSelector doesn't load the state in time

I'm working on a project of mine using React and Redux.
I'm trying to retrieve the token field from my auth state from my store (which contains an authentication key that I intend to send over to my server side) using useSelectorbut the thing is, that it doesn't load in time. How do I fix this?
I'll add below parts of my code that are associated with this issue:
LikeButton.js: (here is the part where I'm trying to retrieve token)
...
const LikeButton = ({ postId }) => {
const classes = useStyles();
const [isLiked, setIsLiked] = useState(false);
const isMount = useIsMount();
const dispatch = useDispatch();
const { token } = useSelector((state) => state.auth);
const { likedPostsIds } = useSelector((state) => state.likes);
useEffect(() => {
if (token) dispatch(fetchLikedPosts(token));
}, [token]);
...
likeActions.js:
...
export const fetchLikedPosts = ({ token }) => (dispatch) => {
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: fetchLikedPostsQuery(),
})
.then((res) => res.json())
...
authActions.js
...
export const signIn = ({ password, email }) => (dispatch) => {
dispatch({
type: IS_AUTH_LOADING,
});
fetch("http://localhost:8080/graphql", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: signInQuery(password, email),
})
.then((res) => res.json())
.then((resData) => {
if (resData.errors) {
dispatch(showNotification("User authentication has failed!", ERROR));
dispatch({
type: AUTH_ERROR,
});
} else {
setLocalStorageAuth(resData.data.signIn.token, expiryDate);
dispatch({
type: SIGN_IN,
payload: resData.data.signIn,
});
}
})
.catch((err) => {
dispatch(
showNotification(
"User authentication has failed. Please try again later.",
ERROR
)
);
dispatch({
type: AUTH_ERROR,
});
emptyLocalStorage();
});
};
...
auth.js:
...
const initialState = {
token: localStorage.getItem("token"),
isAuth: localStorage.getItem("isAuth"),
expiryDate: localStorage.getItem("expiryDate"),
isLoading: false,
};
export default function authReducer(state = initialState, action) {
switch (action.type) {
case REGISTER_LOADING:
case IS_AUTH_LOADING:
return {
...state,
isLoading: true,
};
case SIGN_IN:
return {
token: action.payload.token,
isAuth: true,
isLoading: false,
};
case AUTH_ERROR:
case LOGIN_FAIL:
case REGISTER_FAIL:
case LOGOUT_SUCCESS:
return {
token: null,
isAuth: false,
isLoading: false,
};
case CREATE_USER:
return {
...state,
isLoading: false,
};
default:
return state;
}
}
...
The fetchLikedPosts function expects an object with a token key as an argument. the effect currently passes just the token as an argument. Try
useEffect(() => {
// note the change from token to {token}
if (token) dispatch(fetchLikedPosts({token}));
}, [token]);

react & redux with hooks: Actions must be plain objects. Use custom middleware for async actions

i tried looking for similar answers to help solve my problem but i couldn't find anything using react redux hooks. This code was from a tutorial and originally written using the Context api. I wanted to trying using it with react-redux-hooks, but i got stuck. Basically i'm trying to register a user with a name, email and password, then pass these three as an object to the express server which will validated it and give me back a jwt token. Then come back to the client side and send the token to the reducer, which adds the token to localstorage and sets the state to isAuthenticated. The error i get is on the dispatch.
Dispatch
const onSubmit = e => {
e.preventDefault();
if (name === "" || email === "" || password === "") {
dispatch(setAlert("Please enter all fields", "danger"));
} else if (password !== password2) {
dispatch(setAlert("Passwords do not match", "danger"));
} else {
dispatch(register({ name, email, password })); // Error is here
}
setTimeout(() => {
dispatch(removeAlert());
}, 5000);
};
Action
export const register = async formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
try {
const res = await axios.post("/api/users", formData, config);
return {
type: "REGISTER_SUCCESS",
payload: res.data
};
} catch (err) {
return {
type: "REGISTER_FAIL",
payload: err.response.data.msg
};
}
};
Reducer
const authReducer = (
state = {
token: localStorage.getItem("token"),
isAuthenticated: null,
loading: true,
user: null,
error: null
},
action
) => {
switch (action.type) {
case "REGISTER_SUCCESS":
console.log("register success");
localStorage.setItem("token", action.payload.token);
return {
...state,
...action.payload,
isAuthenticated: true,
loading: false
};
case "REGISTER_FAIL":
console.log("register failed");
localStorage.removeItem("token");
return {
...state,
token: null,
isAuthenticated: false,
loading: false,
user: null,
error: action.payload
};
default:
return state;
}
};
Store
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
|| compose;
const store = createStore(
allReducers,
composeEnhancers(applyMiddleware(thunk))
);
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
Express server
router.post(
"/",
[
check("name", "Please a name")
.not()
.isEmpty(),
check("email", "Please include a valid email").isEmail(),
check(
"password",
"Please enter a password with 6 or more characters"
).isLength({
min: 6
})
],
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array()
});
}
const { name, email, password } = req.body;
try {
let user = await User.findOne({ email });
if (user) {
return res.status(400).json({
msg: "User already exists"
});
}
user = new User({
name,
email,
password
});
// hash passsword
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(password, salt);
await user.save();
const payload = {
user: {
id: user.id
}
};
jwt.sign(
payload,
config.get("jwtSecret"),
{
expiresIn: 360000
},
(err, token) => {
if (err) throw err;
res.json({
token
});
}
);
} catch (err) {
console.error(err.message);
res.status(500).send("Server Error");
}
}
);
I believe this question has answers to the issue your experiencing here: how to async/await redux-thunk actions?
Using this example, it may look something like this (wasn't able to test it):
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const request = axios.post("/api/users", formData, config);
return dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
request.then(onSuccess, onError);
};
};
export const register = formData => {
const config = {
headers: {
"Content-Type": "application/json"
}
};
return async dispatch => {
const onSuccess = success => {
dispatch({
type: "REGISTER_SUCCESS",
payload: success.data
});
return success;
};
const onError = error => {
dispatch({
type: "REGISTER_FAIL",
payload: error.response.data.msg
});
return error;
};
try {
const success = await axios.post("/api/users", formData, config);
return onSuccess(success);
} catch (error) {
return onError(error);
}
}
};

Resources