Using React Hooks with AWS Cognito for auth - reactjs

I'm using the hook below for auth with AWS Cognito. When a user signs in, it doesn't run and therefore React thinks the user is still not authenticated. If I refresh the page, everything works as it should. What am I doing wrong?
import { useState, useEffect, useMemo } from "react";
import { Auth } from "aws-amplify";
interface User {
email: string;
id: string;
isAuthenticated: boolean;
loading: boolean;
}
const useAuth = () => {
const [user, setUser] = useState<User>({
email: "",
id: "",
isAuthenticated: false,
loading: true,
});
const auth = useMemo(() => {
Auth.configure({
userPoolId: process.env.REACT_APP_COGNITO_USER_POOL_ID,
userPoolWebClientId: process.env.REACT_APP_COGNITO_APP_CLIENT_ID,
});
return Auth;
}, []);
useEffect(() => {
const checkUser = async () => {
try {
const user = await auth.currentAuthenticatedUser();
setUser({
email: user.attributes.email,
id: user.attributes.sub,
isAuthenticated: true,
loading: false,
});
return null;
} catch (e) {
setUser({
email: "",
id: "",
isAuthenticated: false,
loading: false,
});
return null;
}
};
checkUser();
}, []);
return user;
};
export default useAuth;

Related

Apollo Client useQuery not getting data because of cache

I'm using Apollo client GraphQL with React Native,
I have a query that launches but data stays undefined, I get data only when I change fetch policy from network-only to cache-only, but then if I logout and login the problem persists and I get nothing.
This is how the component works
// in MyComponent.tsx
export default function MyComponent(): JSX.Element {
const [flashCards, setFlashCards] = useState<any>([]);
const { loading, error, data, refetch } = useQuery<
{
getAllFlashcards: {
id: string;
flashcard: Array<{ id: string; title: string }>;
};
},
{ classroomId: string }
>(GET_ALL_FLASH_CARDS, {
fetchPolicy : "network-only", //<--------- 1. if i comment this
fetchPolicy: "cache-only", //<------- 2. then uncomment this then i get data
variables: {
classroomId: classroomId,
},
});
if (loading && data) {
console.log("loading and adata");
}
if (loading && !data) {
console.log("loading and no data");
}
if (error) {
console.log(error);
}
useEffect(() => {
if (data) {
setFlashCards(data.getAllFlashcards);
}
}, [data]);
return(<>...<>)
}
I followed Apollo client docs when implementing the authentication by clearing the store when I signin and signout, but still... the problem persist
// in App.tsx
export default function App() {
const [classroomId, setClassroomId] = useState<any>("");
const [state, dispatch] = React.useReducer(
(prevState: any, action: any) => {
switch (action.type) {
case "SIGN_IN":
return {
...prevState,
isSignout: false,
userToken: action.token,
};
case "SIGN_OUT":
return {
...prevState,
isSignout: true,
userToken: null,
};
}
},
{
isLoading: true,
isSignout: false,
userToken: null,
}
);
//passed to the application using a context provider.
const auth = React.useMemo(
() => ({
signIn: async (data: any) => {
await client.resetStore();
await SecureStore.setItemAsync("userToken", data.token);
dispatch({ type: "SIGN_IN", data });
},
signOut: async() => {
await client.resetStore();
await SecureStore.deleteItemAsync("userToken")
dispatch({ type: "SIGN_OUT" })
}
}),
[]
);
Why fetched data is undefined but visible only when I change fetch policy even though I am using fetchPolicy : "network-only" ?, your help is appreciated, thank you.

Firebase sign in and register methods not working on Next.js app

I am using the Material-UI Devias Kit Pro Next.js template that uses Firebase for authentication. Neither the signInWithEmailAndPassword nor the registerWithEmailAndPassword methods work. I could really use some help or tips for debugging this.
My login form calls the handleSubmit method when the user clicks "login" which calls the Firebase signInWithEmailAndPassword method. The app correctly routes to the return URL after the login button is clicked, but the app is not authenticated.
Also assume all the Firebase configs are setup correctly.
firebase-login.tsx
<form
noValidate
onSubmit={formik.handleSubmit}
>
<TextField
...
/>
<Box sx={{ mt: 2 }}>
<Button
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
>
Log In
</Button>
</Box>
</form>
firebase-login.tsx
const { signInWithEmailAndPassword } = useAuth();
const formik = useFormik({
initialValues: {
...
},
validationSchema: Yup.object({
...
}),
onSubmit: async (values, helpers): Promise<void> => {
try {
await signInWithEmailAndPassword(values.email, values.password);
if (isMounted()) {
const returnUrl = (router.query.returnUrl as string | undefined) || '/';
router.push(returnUrl).catch(console.error);
}
} catch (err) {
console.error(err);
if (isMounted()) {
helpers.setStatus({ success: false });
helpers.setErrors({ submit: err.message });
helpers.setSubmitting(false);
}
}
}
});
use-auth.ts
import { useContext } from 'react';
import { AuthContext } from '../contexts/firebase-auth-context';
export const useAuth = () => useContext(AuthContext) as any;
firebase-auth-context.tsx
const auth = getAuth(firebaseApp);
interface State {
isInitialized: boolean;
isAuthenticated: boolean;
user: User | null;
}
export interface AuthContextValue extends State {
platform: 'Firebase';
createUserWithEmailAndPassword: (
email: string,
password: string
) => Promise<any>;
signInWithEmailAndPassword: (email: string, password: string) => Promise<any>;
signInWithGoogle: () => Promise<any>;
logout: () => Promise<void>;
}
interface AuthProviderProps {
children: ReactNode;
}
enum ActionType {
AUTH_STATE_CHANGED = 'AUTH_STATE_CHANGED'
}
type AuthStateChangedAction = {
type: ActionType.AUTH_STATE_CHANGED;
payload: {
isAuthenticated: boolean;
user: User | null;
};
};
type Action = AuthStateChangedAction;
const initialState: State = {
isAuthenticated: false,
isInitialized: false,
user: null
};
const reducer = (state: State, action: Action): State => {
if (action.type === 'AUTH_STATE_CHANGED') {
const { isAuthenticated, user } = action.payload;
return {
...state,
isAuthenticated,
isInitialized: true,
user
};
}
return state;
};
export const AuthContext = createContext<AuthContextValue>({
...initialState,
platform: 'Firebase',
createUserWithEmailAndPassword: () => Promise.resolve(),
signInWithEmailAndPassword: () => Promise.resolve(),
signInWithGoogle: () => Promise.resolve(),
logout: () => Promise.resolve()
});
export const AuthProvider: FC<AuthProviderProps> = (props) => {
const { children } = props;
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => onAuthStateChanged(auth, (user) => {
if (user) {
// Here you should extract the complete user profile to make it available in your entire app.
// The auth state only provides basic information.
dispatch({
type: ActionType.AUTH_STATE_CHANGED,
payload: {
isAuthenticated: true,
user: {
id: user.uid,
avatar: user.photoURL || undefined,
email: user.email || 'anika.visser#devias.io',
name: 'Anika Visser',
plan: 'Premium',
}
}
});
} else {
dispatch({
type: ActionType.AUTH_STATE_CHANGED,
payload: {
isAuthenticated: false,
user: null
}
});
}
}), [dispatch]);
const _signInWithEmailAndPassword = async (email: string, password: string): Promise<void> => {
await signInWithEmailAndPassword(auth, email, password)
};
const signInWithGoogle = async (): Promise<void> => {
const provider = new GoogleAuthProvider();
await signInWithPopup(auth, provider);
};
const _createUserWithEmailAndPassword = async (email: string, password: string): Promise<void> => {
await createUserWithEmailAndPassword(auth, email, password);
}
const logout = async (): Promise<void> => {
await signOut(auth);
};
return (
<AuthContext.Provider
value={{
...state,
platform: 'Firebase',
createUserWithEmailAndPassword: _createUserWithEmailAndPassword,
signInWithEmailAndPassword: _signInWithEmailAndPassword,
signInWithGoogle,
logout
}}
>
{children}
</AuthContext.Provider>
);
};
AuthProvider.propTypes = {
children: PropTypes.node.isRequired
};
export const AuthConsumer = AuthContext.Consumer;

React Context is creating an infinite loop when fetching user data

I have a React Context, so I can save some data and reuse it where I want in my React-Project. Here is the code I'm working on:
import React, { useState, createContext } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({
loading: false
})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
if (accessToken && !value.loggedIn && !value.loading) {
updateData({ loading: true })
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken) // if I comment this out, then no infinite loop
}
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}
I have commented the line, where it creates this loop. Can anyone tell me why it is behaving like that?
You need to do the fetch request in the useEffect so that it is fired only when the component is mounted or when the cookie value changes.
Try this;
import React, { useState, createContext, useEffect } from "react"
import apiRequest from "../axios"
import { getCookie } from "../Utils"
import jwt_decode from "jwt-decode"
export const UserContext = React.createContext()
export const UserProvider = (props) => {
const [value, setValue] = useState({
loading: false,
isLoggedIn: false,
userId: "",
username: ""
})
const updateData = (toUpdate) => {
setValue({...value, ...toUpdate})
}
const fetchUser = async (userId, key) => {
updateValue({ loading: true });
await apiRequest("get", "/user/" + userId, null, () => {}, (data) => {
updateData({
loading: false,
isLoggedIn: true,
userId: userId,
username: data.user.username
})
}, (errMessage) => {
updateData({loading: false})
}, key)
}
// load user data if access token is set
const accessToken = getCookie("access")
useEffect(() => {
if (accessToken && !value.loggedIn && !value.loading) {
const { sub: userId } = jwt_decode(accessToken)
fetchUser(userId, accessToken);
}
}, [accessToken, value.loggedId, value.loading]);
const methods = {
fetchUser,
updateData
}
return (
<UserContext.Provider value={[value, methods]}>
{props.children}
</UserContext.Provider>
)
}

Hook won't get properly mocked Jest

For the last 2 hours, I've read unnumerous posts from StackOverflow, medium, and other independent blogs, and I haven't been able to crack or decipher how to properly mock a simple custom useAuth() hook.
I'm getting:
[TypeError: Cannot destructure property 'user' of '(0 , _auth.useAuth)(...)' as it is undefined.]
Here's my code:
The <Dashboard/> component which includes the useAuth() hook. (Code omitted due to brevity)
import { useAuth } from '../../../auth';
export const Dashboard: React.FC<RouteComponentProps> = (props) => {
const { user } = useAuth();
The dashboard.test.tesx file.
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
jest.mock("../../../auth");
describe("Tests the Patient's dashboard", () => {
beforeAll(() => {
(useAuth as any).mockReturnValue({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
});
});
it("Will be able to assign a consultation now", async () => {
const renderer = await render(<Dashboard />);
waitFor(async () => {
await expect(renderer.getByText(`Hola ${mockPatientDetails.username}`));
});
expect(true).toBe(true);
});
});
Other variations tried:
import { render, waitFor } from "../../../__tests_setup__/app-test-utils";
// Sets Up useAuth
import { mockPatientDetails } from "../../../setupTestPatient";
import { mockBaseAuth } from "../../../setupTestShared";
import { Dashboard } from "./index";
import { useAuth } from "../../../auth";
// Variation 1
import * as auth from '../../../auth'
jest.spyOn(auth, "useAuth").mockImplementation(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}));
// Variation 2
jest.mock("../../../auth/index", () => ({
__esModule: true,
useAuth: jest.fn(() => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
})),
}));
// Variation 3
There have been many other variations which I haven't included as I've completely forgotten about them.
Here's my folder structure, just in case.
P.S: Here are the variables shown above:
src/setupTestShared.ts
import { GENDER, InteractionMedias, UserProfile } from "./#dts";
import { useAuth } from "./auth";
const success = Promise.resolve({
type: "success" as const,
result: true,
});
export const mockBaseAuth: ReturnType<typeof useAuth> = {
login(u: string, p: string) {
return success;
},
authenticated: true,
logout() {},
register(p: UserProfile, pass: string) {
return success;
},
userExists(u: string) {
return Promise.resolve("USER_REGISTERED" as const);
},
user: {
date_of_birth: "1990-12-21",
email: "test#gmail.com",
emails: ["test#gmail.com"],
first_name: "Maria",
gender: GENDER.FEMALE,
id: 1,
identification_id: "402-2066666-1",
interaction_media_preferred: InteractionMedias.VIDEO,
last_name: "Anabelle",
loggedInDate: new Date(),
phones: ["809-544-5111"],
profile: "ANONYMOUS",
profile_id: 1,
username: "anonymous",
},
};
export const mockPatientDetails = {
username: "PAC123456",
profile: "patient" as const,
profile_id: 2,
};
What could it be?
It's working now!
This answer helped me:
https://stackoverflow.com/a/60282832/1057052
// https://stackoverflow.com/a/60282832/1057052
jest.mock("../../../auth", () => ({
// this isn't needed - __esModule: true,
useAuth: () => ({
...mockBaseAuth,
user: {
...mockBaseAuth.user,
profile: mockPatientDetails.profile,
profile_id: mockPatientDetails.profile_id,
username: mockPatientDetails.username,
},
}),
}));
The trick was not assigning the jest.fn() to the useAuth().

React.js: How to get and show current user when logged in?

In my React app I am working in user login. My goal is to show current user's username when the user is logged in. I'm fetching the user data in redux actions and, as I followed some tutorials, I need to get jwt token coming from backend in fetch function. In login Fetch function I'm trying to get and save the token(see fetching function), but it shows undefined in devtools/localStorage. This is how InitialState updates in LoginSuccess in Reducers.
state
{user: {…}, loading: true, error: "", isAuthenticated: false, users: {…}}
error: ""
isAuthenticated: false
loading: true
user: {user: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRVc…xMTB9.hNsYTKGYIFRsPXw66AhB1o0EXyyfgfRTzOFzqBfjaTg"}
users: {user: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRVc…xMTB9.hNsYTKGYIFRsPXw66AhB1o0EXyyfgfRTzOFzqBfjaTg"}
__proto__: Object
I don't know how to get access to the current logged in user data: username or firstName for instanse.
Any help will be appreciated.
Actions
import axios from 'axios'
import { Dispatch } from 'redux'
import {
FETCH_USER_REQUEST,
UserActions,
User,
LOGIN_USER_SUCCESS,
FETCH_LOGIN_FAILURE,
LOGOUT,
} from '../../types/UserType'
export const fetchUserRequest = () => {
return {
type: FETCH_USER_REQUEST,
}
}
export const fetchLoginFailure = (error: UserActions) => {
return {
type: FETCH_LOGIN_FAILURE,
payload: error,
}
}
export function logout(): UserActions {
return {
type: LOGOUT,
}
}
export function loginSuccess(user: User): UserActions {
return {
type: LOGIN_USER_SUCCESS,
payload: {
user,
},
}
}
export const login = ({ email, password }: any) => {
return (dispatch: Dispatch) => {
dispatch(fetchUserRequest())
axios
.post('http://localhost:8000/logIn', {
email: email,
password: password,
})
.then((response) => {
const users = response.data
dispatch(loginSuccess(users))
localStorage.setItem('jwt', users.auth_token)
console.log('users', users) // undefined
})
.catch((error) => {
dispatch(fetchLoginFailure(error.message))
})
}
}
Reducer
import {
LOGIN_USER_SUCCESS,
UserActions,
UserState,
LOGOUT,
} from '../../types/UserType'
const initialState: UserState = {
user: {},
loading: false,
error: '',
isAuthenticated: false,
}
const UserReducer = (state = initialState, action: UserActions) => {
switch (action.type) {
case LOGIN_USER_SUCCESS:
console.log('state', state) // initialState update see above
return {
...state,
loading: false,
user: action.payload,
users: action.payload,
isAuthenticated: true,
error: '',
}
case LOGOUT:
return {
...state,
isAuthenticated: false,
user: null,
users: [],
}
default:
return state
}
}
export default UserReducer
And I assume I am going to show user userName or firstName in logout component
import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { Link } from 'react-router-dom'
import { Icon, Button } from 'semantic-ui-react'
import { logout } from '../../../redux/User/UserActions'
import { AppState } from '../../../types'
function Logout() {
const dispatch = useDispatch()
const user = useSelector((state: AppState) => state.user.user)
console.log('user', user)
const logoutOnClick = () => {
dispatch(logout())
localStorage.clear()
}
return (
<Button
color="black"
as={Link}
to="Login"
name="logout"
onClick={logoutOnClick}
>
<Icon name="sign out"> </Icon>Logout
</Button>
)
}
export default Logout
You save your logged-in data to localStorage like auth_token you did and clear in logout function.
axios
.post('http://localhost:8000/logIn', {
email: email,
password: password,
})
.then((response) => {
const users = response.data
dispatch(loginSuccess(users))
localStorage.setItem('jwt', users.auth_token)
localStorage.setItem('user', JSON.stringify(users))
console.log('users', users) // undefined
})
.catch((error) => {
dispatch(fetchLoginFailure(error.message))
})
and access inside your logout component or wherever you need that
let userDetails = JSON.parse(localStorage.getItem('user'));
and clear it inside logout function
const logoutOnClick = () => {
dispatch(logout())
localStorage.clear() // already clearing
}

Resources