How to access firebase auth userMetadata - reactjs

I'm trying to find a way to access the creationTime and lastSignInTime described in this documentation.
Are there any examples of using it within react hooks?
I can't make sense of the firebase documentation generally - it's just words on a page. I think it is designed for people who intuitively know how to fill in the blanks. I remain mystified as to how to do that in general.
I can access auth.user.email using a react hook as follows:
import React, { useState, useEffect, useContext, createContext } from "react";
import firebase from "../firebase";
import {auth} from "../firebase";
const authContext = createContext();
// Provider wraps app and makes auth object available by useAuth().
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
// Hook to get the auth
export const useAuth = () => {
return useContext(authContext);
};
// Provider hook that creates auth state
function useProvideAuth() {
const [user, setUser] = useState(null);
const signin = (email, password) => {
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signup = (email, password) => {
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(response => {
setUser(response.user);
return response.user;
});
};
const signout = () => {
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
});
};
const sendPasswordResetEmail = email => {
return firebase
.auth()
.sendPasswordResetEmail(email)
.then(() => {
return true;
});
};
const confirmPasswordReset = (code, password) => {
return firebase
.auth()
.confirmPasswordReset(code, password)
.then(() => {
return true;
});
};
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged(user => {
if (user) {
setUser(user);
} else {
setUser(false);
}
});
return () => unsubscribe();
}, []);
return {
user,
signin,
signup,
signout,
sendPasswordResetEmail,
confirmPasswordReset
};
}
Now, I'm trying to figure out what I need to do to either access the string values described here or the timestamps described here.
I tried each of (all guesses):
{auth.user.UserMetadata().creationTime}
{auth.user.creationTime}
{auth.user.UserMetadata.creationTime}

This works.
{auth.user.metadata.creationTime}
I don't understand why. The references in the firebase documentation refer to metadata as UserMetadata. I don't know how to find the piece of information that tells people to make the leap between UserMetadata and metadata.
If anyone knows what the key to this is, I'd be forever grateful for the insight.

Related

How can I make React app verify user token regularly?

I have a React app using Firebase Auth and an Express backend. I have React contexts set up for the user's authentication process and for the loading state of the app. Currently, when a user signs in, the following happens:
The app goes into a loading state
The app sends an API request to the backend to verify the user's token
The backend queries the database and then sets the user's custom claims with their permissions and sends a response with the verified token & claims
The loading state is cleared, and the app becomes useable
The user's routes / nav menu options etc are then determined by the user's permissions according to the backend - i.e, if a user doesn't have permission for a certain area of the site, its routes and nav menu items are not loaded.
My authentication context is as follows:
import { createContext, useContext, useState, useEffect } from "react";
/**
* auth = getAuth()
* provider = new GoogleAuthProvider()
*/
import { auth, provider } from "providers/firebase";
import {
getAuth,
onAuthStateChanged,
signInWithPopup,
signOut as firebaseSignOut
} from "firebase/auth";
import { api } from "providers/axios";
import { useLoading } from "providers/loading";
const UserContext = createContext(null);
export const useAuth = () => useContext(UserContext);
const verifyToken = (token) =>
api({
method: "post",
url: "/user/auth",
headers: {
token
}
});
const UserProvider = (props) => {
const [user, setUser] = useState(null);
const { loading, setLoading } = useLoading();
const signIn = async () => {
setLoading(true);
try {
const result = await signInWithPopup(auth, provider);
console.log("auth signInWithPopup", result.user.email);
} catch (e) {
setUser(null);
console.error(e);
setLoading(false);
}
};
const signOut = async () => {
let userSigningOut = user;
try {
await firebaseSignOut(auth);
setUser(null);
console.log("signed out");
} catch (e) {
console.error(e);
} finally {
return (userSigningOut = null);
}
};
const verifyUser = async (user) => {
try {
if (!user) {
throw "no user";
}
const token = await getAuth().currentUser.getIdToken(true);
if (!token) {
throw "no token";
}
const jwt = await getAuth().currentUser.getIdTokenResult();
if (!jwt) {
throw "no jwt";
}
const verifyTokenResponse = await verifyToken(token);
if (verifyTokenResponse.data.role !== jwt.claims.role) {
throw "role level claims mismatch";
} else {
user.verifiedToken = verifyTokenResponse.data;
console.log(`User ${user.uid} verified`);
setUser(user);
}
} catch (e) {
signOut();
console.error(e);
}
};
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
setLoading(true);
try {
if (user) {
console.log("onAuthStateChanged", user?.email);
await verifyUser(user);
} else {
throw "no user";
}
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
});
return unsubscribe;
}, []);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user
}}
>
{props.children}
</UserContext.Provider>
);
};
export default UserProvider;
The problem is that if the user or their permissions are modified, the changes are not reflected in the app until the user performs a hard refresh.
What I'd like to achieve is for the user's token to be re-verified via the backend upon every page change (or similar) and then if their permissions etc have changed, the app then rerenders reflecting the changes. I think this could be achieved by triggering a rerender of a certain part of UserContext after taking it out of the main function, but I'm not sure how to proceed with that.
After #samthecodingman's comment, I added another state for the user's database entry and have achieved the desired outcome with the following changes to UserProvider:
useEffect(() => {
if (user) {
const userDataRef = ref(db, `/users/${user.uid}`);
return onValue(userDataRef, async snapshot => {
await verifyUser(user);
setUserData(snapshot.val());
})
}
}, [user]);
return (
<UserContext.Provider
value={{
signIn,
signOut,
user,
userData
}}
>
{props.children}
</UserContext.Provider>
);

firebase: Cannot assign to read only property 'displayName' of object '#<Object>'

I'm using #reduxjs/toolkit: ^1.7.1 , firebase: 9.6.1 and firebase-tools: 10.0.1 on my ReactJS project.
I was trying to create a function where users can update their names and avatar photos.
For which I used updateProfile() function. But whenever I execute the update function it threw an error Cannot assign to read only property 'displayName' of object '#<Object>'.
There is an interesting thing I have noticed that is if I only update the photoURL property still it gives Cannot assign to read only property 'displayName' of object '#<Object>'
Project_Link_Github
Code: useFirebase.js:
import { getAuth, signInWithPopup, GoogleAuthProvider, createUserWithEmailAndPassword, signInWithEmailAndPassword, signOut, onAuthStateChanged, updateProfile } from 'firebase/auth';
import { getDownloadURL, getStorage, ref, uploadBytes } from 'firebase/storage';
import { useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { useLocation, useNavigate } from 'react-router-dom';
import { setIsLoading } from '../features/isloadingSlice';
import { login, logout } from '../features/userSlice';
import initAuth from '../Firebase/initAuth';
initAuth();
export const useFirebase = () => {
const auth = getAuth();
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch();
const [updateCount, setUpdateCount] = useState(0);
const storage = getStorage();
const Redirect = () => {
console.log(location);
const destination = location?.state?.from?.pathname || '/';
navigate(destination);
}
const uploadAvatar = async (file) => {
const fileRef = ref(storage, 'avatar/' + auth?.currentUser?.uid + '.png');
dispatch(setIsLoading(true));
const snapshot = await uploadBytes(fileRef, file);
const photoURL = await getDownloadURL(fileRef);
updateProfile(auth.currentUser, { photoURL }).then(() => {
setUpdateCount(updateCount + 1);
}).catch(e => console.log(e.message))
dispatch(setIsLoading(false));
console.log(snapshot);
}
const userRegister = (name, photoURL, email, password) => {
dispatch(setIsLoading(true));
createUserWithEmailAndPassword(auth, email, password)
.then(result => {
updateProfile(auth.currentUser, {
displayName: name, photoURL
}).then(() => { })
dispatch(login({ displayName: name, email, photoURL }));
Redirect();
}).catch(error => alert(error.message))
.finally(() => dispatch(setIsLoading(false)))
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (result) => {
if (result) {
dispatch(login({ ...result }))
}
else {
dispatch(login({}))
}
dispatch(setIsLoading(false));
})
return () => unsubscribe;
}, [updateCount, auth])
return {
logIn,
logOut,
Redirect,
uploadAvatar,
userRegister,
}
}
I don't know what's wrong with this displayName property but my previous project works fine.
Can anybody please help me with this?
I have 2 different projects using authentication of firebase, both using updateProfile() of firebase/auth. One of them works fine, but the other, which use #reduxjs/toolkit: ^1.8.4 got the same problem as yours, so i believe that it has something to do with reduxjs/toolkit. However, the function actually does work, the data has updated on server side (firebase). So in my case, i just ignore it.

Implement user present using Firestore db with Firebase Realtime db in React

I'm trying to set user presence in my React app using Firebase.
Everytime a user logs in the app I save it the a users collection and I display it as a list in my app.
I need to show the online status for each user, so I'm using Firebase Realtime db to update Firestore db, this is the whole AuthContext.js file :
import React, { useState, useEffect, useContext, createContext } from "react";
import { db, rtdb } from "../firebase/firebase-config";
import {
getAuth,
signInWithPopup,
GoogleAuthProvider,
onAuthStateChanged,
signOut,
} from "firebase/auth";
import {
addDoc,
collection,
serverTimestamp,
getDocs,
query,
where,
limit,
updateDoc,
doc,
} from "firebase/firestore";
import { Redirect } from "react-router-dom";
import { ref as rtdbRef, set, onValue } from "firebase/database";
const authContext = createContext();
export function ProvideAuth({ children }) {
const auth = useProvideAuth();
return <authContext.Provider value={auth}>{children}</authContext.Provider>;
}
export const useAuth = () => {
return useContext(authContext);
};
function useProvideAuth() {
const [user, setUser] = useState(null);
const provider = new GoogleAuthProvider();
const auth = getAuth();
const [userLoading, setUserLoading] = useState(true);
const getUserByUid = async (uid) => {
let exists = false;
const querySnapshot = await getDocs(
query(collection(db, "users"), where("uid", "==", uid), limit(1))
);
querySnapshot.forEach(() => (exists = true));
return exists;
};
const persistUser = async (user) => {
const exists = await getUserByUid(user.uid);
if (!exists) {
await addDoc(collection(db, "users"), {
uid: user.uid,
displayName: user.displayName,
email: user.email,
photoURL: user.photoURL,
createdAt: serverTimestamp(),
updatedAt: null,
});
}
};
const logIn = () =>
signInWithPopup(auth, provider)
.then((result) => {
setUser(result.user);
persistUser(result.user);
})
.catch((err) => {
console.log(err);
});
const updateFirestoreUser = (user, status) => {
if (user) {
getDocs(
query(collection(db, "users"), where("uid", "==", user.uid), limit(1))
).then((querySnapshot) => {
querySnapshot.forEach((u) => {
updateDoc(doc(db, "users", u.id), {
online: status,
updatedAt: serverTimestamp(),
});
});
});
}
};
const logOut = () =>
signOut(auth)
.then(() => {
console.log("SIGNOUT", user);
updateFirestoreUser(user, false);
setUser(false);
return <Redirect to="/login" />;
})
.catch((err) => {
console.log(err);
});
function checkAuthStatus() {
return new Promise((resolve, reject) => {
try {
onAuthStateChanged(auth, async (user) => {
resolve(user);
setUser(user);
const connectedRef = rtdbRef(rtdb, ".info/connected");
const myConnectionsRef = rtdbRef(rtdb, `status`);
onValue(connectedRef, (snap) => {
if (snap.val() === true) {
console.log("TRUE");
if (user) {
set(myConnectionsRef, "connected");
updateFirestoreUser(user, true);
}
} else {
console.log("FALSE");
updateFirestoreUser(user, false);
}
//onDisconnect(myConnectionsRef)
//.set("disconnected")
//.then(() => {
// updateFirestoreUser(user, false);
//});
});
});
} catch {
reject("api failed");
setUser(false);
}
});
}
const getUser = async () => {
setUserLoading(true);
await checkAuthStatus();
setUserLoading(false);
};
useEffect(() => {
getUser();
}, []);
return {
user,
logIn,
logOut,
userLoading,
};
}
The online field in users collection gets updated when I login & logout, but not when I close the browser tab, it kinda works when I uncomment this part :
onDisconnect(myConnectionsRef)
.set("disconnected")
.then(() => {
updateFirestoreUser(user, false);
});
But the changes reflect only in the Realtime db with no changes in Firestore db users collection, the string "disconnected" is added to the Realtime database but nothing changed in the user's entry I'm tying to update in the then() method.
Been stuck here for 2 days, I don't know what I'm doing wrong, I just need the online field to be true when the app is opened and the user is logged in, and false when the user is signed out or the app is closed in the browser.
By following the presence system in the documentation you linked, you will gets presence information in Realtime Database.
To get that information into Firestore, you'll need to implement a Cloud Function that triggers in the Realtime Database writes as shown in the section on updating presence information in Firestore in the documentation on implementing presence on Firestore. That's the only way to get the presence information in Firestore, as that database doesn't have any onDisconnect like functionality that is required to build a presence system.

Unable to call Context Provider in react

I am implementing aws cognito service in my react app.
I created a login page and applied the authentication method, it was working fine.
Now for state management, I wanted context.provider that will pass the authentication to all other pages.
When I am trying to useContext in my login page then it is giving error: failed to login Error: Username and Pool information are required.
My login.js page looks like this:
import React, { useState, useContext } from 'react';
import { AccountContext } from './Account';
const LoginPage = props => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [isLoggedIn, setIsLoggedIn] = useState(false);
const { authenticate } = useContext(AccountContext);
const onSubmit = event => {
event.preventDefault();
authenticate(username, password)
.then(data => {
console.log('logged in', data);
})
.catch(err => {
console.log('error is here');
console.error('failed to login', err);
});
};
It seems like it is not going inside authenticate function.
Can you please suggest me where am I going wrong?
My account.js(context provider) file:
import React, { createContext } from 'react';
import { CognitoUser, AuthenticationDetails } from 'amazon-cognito-identity-js';
import Pool from '../../UserPool';
const AccountContext = createContext();
const Account = props => {
const authenticate = async (username, password) => {
return await new Promise((resolve, reject) => {
const user = new CognitoUser({ username, Pool });
const authDetails = new AuthenticationDetails({ username, password });
user.authenticateUser(authDetails, {
onSuccess: data => {
console.log('onSuccess: ', data);
resolve(data);
// setIsLoggedIn(true);
},
onFailure: err => {
console.error('onFailure: ', err);
reject(err);
}
});
});
};
return (
<AccountContext.Provider value={{ authenticate }}>
{props.children}
</AccountContext.Provider>
);
};
export { Account, AccountContext };

Handling auth with React Hooks

I've been chasing my tail for hours now trying to figure out how to handle auth on my component using firebase and react hooks.
I've created a custom useAuth hook that is intended to handle all the auth behaviors. My thought was to put a useEffect on the root of my component tree that would trigger if the firebase.auth.onAuthStateChanged() ever changed (ie, user is now logged out / logged in.) But, at this point after making a million unsuccessful changes I really don't know what I'm doing anymore.
Here is the code that I have...
RootPage component
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [authStatus]);
...
}
my thought was ok, lets trigger authStatus on mount, but that ends up with me lying about my dependencies. So, in an effort to not lie about my deps, I added authStatus to the deps. Logging out and then logging in results in this:
useAuth hook
const useAuth = () => {
const { fetchUser, resetUser, userData } = useUser();
const { currentUser } = firebaseAuth;
const registerUser = async (username, email, password) => {
try {
const credentials = await firebaseAuth.createUserWithEmailAndPassword(
email,
password
);
const { uid } = credentials.user;
await firebaseFirestore
.collection('users')
.doc(credentials.user.uid)
.set({
username,
points: 0,
words: 0,
followers: 0,
following: 0,
created: firebase.firestore.FieldValue.serverTimestamp(),
});
fetchUser(uid);
console.log('user registered', credentials);
} catch (error) {
console.error(error);
}
};
const loginUser = async (email, password) => {
try {
// login to firebase
await firebaseAuth.signInWithEmailAndPassword(email, password);
// take the current users id
const { uid } = firebaseAuth.currentUser;
// update the user in redux
fetchUser(uid);
} catch (error) {
console.error(error);
}
};
const logoutUser = async () => {
try {
// logout from firebase
await firebaseAuth.signOut();
// reset user state in redux
resetUser();
return;
} catch (error) {
console.error(error);
}
};
const authStatus = () => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
return;
}
console.log('User logged out.');
});
};
return { currentUser, registerUser, loginUser, logoutUser, authStatus };
};
export default useAuth;
I'm relatively certain that react hooks are only meant for reusable pieces of logic, so if the purpose of your hook is to contact firebase in every single component you're using it, along with rerendering and refreshing state every time that component is updated, then it's fine, but you can't use hooks for storing global auth state, which is how auth should be stored.
You're looking for react context instead.
import React, {createContext, useContext, useState, useEffect, ReactNode} from 'react'
const getJwt = () => localStorage.getItem('jwt') || ''
const setJwt = (jwt: string) => localStorage.setItem('jwt', jwt)
const getUser = () => JSON.parse(localStorage.getItem('user') || 'null')
const setUser = (user: object) => localStorage.setItem('user', JSON.stringify(user))
const logout = () => localStorage.clear()
const AuthContext = createContext({
jwt: '',
setJwt: setJwt,
user: {},
setUser: setUser,
loading: false,
setLoading: (loading: boolean) => {},
authenticate: (jwt: string, user: object) => {},
logout: () => {},
})
export const useAuth = () => useContext(AuthContext)
const Auth = ({children}: {children: ReactNode}) => {
const auth = useAuth()
const [jwt, updateJwt] = useState(auth.jwt)
const [user, updateUser] = useState(auth.user)
const [loading, setLoading] = useState(false)
useEffect(() => {
updateJwt(getJwt())
updateUser(getUser())
}, [])
const value = {
jwt: jwt,
setJwt: (jwt: string) => {
setJwt(jwt)
updateJwt(jwt)
},
user: user,
setUser: (user: object) => {
setUser(user)
updateUser(user)
},
loading: loading,
setLoading: setLoading,
authenticate: (jwt: string, user: object) => {
setJwt(jwt)
updateJwt(jwt)
setUser(user)
updateUser(user)
},
logout: () => {
localStorage.removeItem('jwt')
localStorage.removeItem('user')
updateJwt('')
updateUser({})
setLoading(false)
},
}
return <AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
}
export default Auth
...
// app.tsx
import Auth from './auth'
...
<Auth>
<Router/>
</Auth>
// or something like that
...
import {useAuth} from './auth'
// in any component to pull auth from global context state
You can change that according to whatever you need.
I know the issue why its happening but don't know the solution...But i am not fully sure...Look how react works is if any parents re render it also cause re render the children..ok?Its mean if any reason your apps is re rendering and the useAuth keep firing...so for this there to much console log.But i am not sure that it will work or not..give me your repo i will try on my local computer
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus,currentUser } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [currentUser]);
//only fire when currentUser change
...
}
Update your useEffect hook like so:
useEffect(() => {
const unsub = firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
} else {
console.log('User logged out.');
}
});
return ()=> unsub;
},[])

Resources