I have created Firebase Authentication with ReactJS.
Everything works fine until The page refreshes. I'm using onAuthStateChanged listener, however, after I'm refreshing the page, it navigates me back to the Login form.
UserAuthContext.js
const userAuthContext = createContext();
export function UserAuthContextProvider({ children }) {
const [user, setUser] = useState("");
function signUp(email, password, username) {
return createUserWithEmailAndPassword(auth, email, password);
};
function logIn(email, password) {
return signInWithEmailAndPassword(auth, email, password);
};
function logout() {
return signOut(auth);
}
useEffect(() => {
const subscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return subscribe;
}, []);
return (
<userAuthContext.Provider value={{ user, signUp, logIn, logout }}>
{children}
</userAuthContext.Provider>
);
}
export function useUserAuth() {
return useContext(userAuthContext);
}
By the way, I'm using siteground hosting, so is there any way to store it?
You are saving the the user to a react state hook. React state does not persist data after page reload. A common way people persist data is by saving it into local storage or a cookie. However, these methods are prone to attacks like XSS and CSRF.
Related
This question already has answers here:
Firebase kicks out current user
(19 answers)
How to avoid being signed in automatically when a new user is created using firebase [duplicate]
(1 answer)
How to create user auth without closing the current firebase session [duplicate]
(1 answer)
Closed 5 months ago.
so im adding users to firestore as well as in authentication but doing this actually replaces the current user with that email which i dont want this as in the image after adding teacher the logged in account is replaced by added account i think its the useEffect which is doing this.here is dashboard where the useEffect is showing the current account logged in
Add user.js
const [data, setData] = useState({});
const handleinput = e =>{
const id = e.target.id;
const value = e.target.value;
setData({...data, [id]:value})
}
console.log(data)
const handleadd = async (e) =>{
e.preventDefault();
try{
const res = await createUserWithEmailAndPassword(
auth,
data.email,
data.password
)
setDoc(doc(db, "Teachers", res.user.uid), {
...data,
timestamp: serverTimestamp(),
});
} catch (e){
alert("Faculty added succesfully")
console.log(e.message)
}
}
authcontext
import { createUserWithEmailAndPassword, onAuthStateChanged, signOut, signInWithEmailAndPassword, signInWithPopup } from 'firebase/auth'
import React, {
createContext,
useContext,
useState,
useEffect} from 'react'
import { auth } from '../../firebase'
const AuthContext = createContext();
export const AuthProvider = ({children}) => {
const userIn = ( email, password) =>{
return signInWithEmailAndPassword(auth, email, password)
}
//sign up context
const [user, setUser] = useState({})
const addusers = (email, password) => {
return createUserWithEmailAndPassword(auth, email, password);
}
useEffect(()=>{
const unsubscribe = onAuthStateChanged(auth, (currentUser) =>{
console.log(currentUser);
setUser(currentUser)
})
return () =>{
unsubscribe()
}
},[])
//logout
const logout =() =>{
return signOut(auth)
}
return (
<AuthContext.Provider value={{addusers, user, logout, userIn }}>
{children}
</AuthContext.Provider>
)
}
export const UserAuth =()=>{
return useContext(AuthContext)
}
By using the JS SDK createUserWithEmailAndPassword() method the newly created user is automatically signed in when the method call is successful. You cannot prevent that.
The classical approach in your case is to use a Cloud Function to create the user, because Cloud Functions use the Admin SDK which allows creating users in the back-end without signing them in from the front-end.
You’ll find an example of this approach in the following article: How to create an Admin module for managing Firebase users access and roles.
I have react native app and i am implementing auth flow using Context API
the nature of the app is when user open it won't request to login or signup and user can explore the app screens however when user add items to cart and about to checkout then will be requested to sign-in/sign-up to continue.
let's assume these are the screens
Home-> shop -> cart
so when user in the cart will be asked to login, after login user value in context provider will be updated and they continue from same screen the user logged in (which in this case is cart screen)
However, when provider value updated all screens re-render again and redirect to initial route which home screen.
how can i handle this scenario which not re-render all screen again.
In my app i am passing the navigation stuck as a children to context provider as follow:
<AuthProvider>
<Routes />
</AuthProvider>
and here is the code of context provider:
authProvider.js:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, setUser }),
[user]);
return (
<AuthContext.Provider
value={{
value,
login: async (email, password) => {
//here i am validating the login details and update user state
// if login successful
setUser(/*some data related to user */)
//when i set user data here it re-render all screens again
// if login failed
setUser(null)
}
}}>
{children}
</AuthContext.Provider >
and the following the Routes.js which i am passing it as children to the above code:
const Routes = () => {
const { value } = useContext(AuthContext);
// validate token with backend
useEffect(() => {
setTimeout(async () => {
const Token = await AsyncStorage.getItem('Token');
if (Token === null) {
value.setUser(null)
} else {
await axios.get(URL, {
headers: { 'x-auth-token': Token },
}).then(async (res) => {
const {data} = res;
value.setUser(data)
}).catch(async (err) => {
if (err.response) {
await AsyncStorage.removeItem('Token');
value.setUser(null)
}
});
}
}, 1000)
}, []);
return (
<>
<NavigationContainer>
//this is the stuck where i have the three screen mentioned above (home, shop, cart)
<AppStuckScreen />
</NavigationContainer>
</>
)
}
export default Routes;
In cart screen i am checking the user values stored in context provider if null then open the login form.
I hope someone can help me on how to handle this situation where not to re-render all component again when updating user value in Context provider.
Why don't you keep the current screen in the Context? So after a successful login, you just navigate to that screen.
The situation is as follows. In my application, I use a router, and pass information to it whether the user is identified or not, similar to the role of the administrator. The data is stored in the auth context, put there auth hook, and used in App.js. The entire code is below. The problem is that when I reload the page, I get redirected from any tab to the home page. This happens because of a momentary change in App.js when constructing the App component, the variables in the useRoutes(isAuthenticated, admin) function change to false and true(true - after identification) when the page is reloaded. I'm relatively new to React, and don't really understand how to solve this issue. All I want to achieve is to make sure that the variables don't change their values in the App by simply refreshing the page.
App.js
function App() {
const { token, login, logout, admin } = useAuth()
const isAuthenticated = !!token
const routes = useRoutes(isAuthenticated, admin)
return (
<AuthContext.Provider value={{
token, login, logout, isAuthenticated, admin
}}>
<Router>
<div className="app-container">
{routes}
</div>
</Router>
</AuthContext.Provider>
);
}
AuthContext.js
function noop() { }
export const AuthContext = createContext({
token: null,
login: noop,
logout: noop,
isAuthenticated: false,
admin: false,
})
Auth.hook.js
const storageName = 'userData'
export const useAuth = () => {
const [token, setToken] = useState(null)
const [admin, setAdmin] = useState(false)
const login = useCallback((jwtToken, isAdmin) => {
setToken(jwtToken)
setAdmin(isAdmin)
localStorage.setItem(storageName, JSON.stringify({
token: jwtToken,
isAdmin: isAdmin,
}))
}, [])
const logout = useCallback(() => {
setToken(null)
setAdmin(false)
localStorage.removeItem(storageName)
}, [])
useEffect(() => {
const data = JSON.parse(localStorage.getItem(storageName))
if (data && data.token) {
login(data.token, data.isAdmin) // <-- There some problem
}
}, [login])
return { login, logout, token, admin }
}
I solved this problem with a simple solution :)
All you need to do is just add 1 more variable ready in the context with a value of false . Then set it in auth.hook.js to true after login. And export it to App.js and use it like if(ready){return "page"} else return <>Loading</>.
I cant figure out why but when I use cognito with my own custom user context everything works just fine but as soon as I use withAuthenticator higher order component it breaks my user context and I cant for the life of me figure out why, or even how to fix it. Ill post my user context file below for reference and tell you where it breaks.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
//TODO must redo cognito from scratch and will probably be able to keep this user context untouched
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser().then(x=>setUser(x)).catch((err)=>setUser(null))
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false)
Auth.signIn(username, password)
.then( x => {
setUser(x)
console.log('Welcome: ' + x.challengeParam.userAttributes.email)
setUserEmail(x.challengeParam.userAttributes.email)
setSignInError(false)
})
.catch((err)=>{
console.log(err.code)
if(err.code === 'UserNotFoundException' || 'NotAuthorizedException'){
err.message = 'Invalid username or password'
setSignInError(true)
console.log(err.message)
}
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
return x
})
}
const signup = (username, email, password) => {
Auth.signUp({ username, password, attributes: { email } })
.then( x => {
setUser(x)
return x
})
.catch((err)=>{
if(err.code){
err.message = 'Your Username or Password was incorrect'
}
throw err
})
}
const vals = useMemo( () => ({user, login, logout, signup, handleSignInError, userEmail, signInError}), [user, userEmail, signInError])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
Under the login function it now returns user not found after I wrap a component and npm i aws-amplify-react. The funny thing is when I uninstall it I still get the same error and cant go back without fully removing amplify and going through a complete amplify init again. Even more confusing, My app is hosted on vercel and that breaks after I attempt to do this on my local machine. If im not missing something there and my app does break in the cloud even though I dont push my modified code then im guessing cognito is getting something in the cloud when I attempt this on my local machine and then screwing up my untouched copy on vercel????? Since then Ive also tried using next-auth which makes me think I should just stick to front end work or find a better solution? any help would be appreciated. Ill revert to my old setup and rebuild my cognito and amplify from scratch just to get it going again.
You need to call Cognito configure prior to calling your auth provider. Place it before you define your auth provider or context.
Auth.configure({...your_config})
const UserContext = () => {};
I also use a auth hook with my context that removes the need for a HOC.
import { useContext } from 'react';
export const useAuth = () => useContext(UserContext);
// use it in components and pages
const user = useAuth();
Ensure that your configuration is using all of the proper types. If you don't, it sometimes fails silently. For example ENV files are always passed as strings so some options must be cast to the proper type like cookie expires
{
authenticationFlowType: 'USER_SRP_AUTH',
cookieStorage: {
...other settings
expires: Number(process.env.NEXT_PUBLIC_COGNITO_COOKIE_EXPIRES),
}
};
You will also need to call Auth.configure on every page that you need access to Congito auth inside of getStaticPaths, getStaticProps, and getServerSideProps. This is because they are independently called from your app during build or on a server.
Auth.configure({...your_config})
const getStaticProps = () => {};
const getStaticPaths = () => {};
const getServerSideProps = () => {};
If you can use it, their hosted UI is pretty good.
Lastly, AWS has a few libraries for Amplify and I use #aws-amplify/auth - I don't know if this makes a difference.
I added the config file to my _app.js and set ssr: true for ssr authentication
import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({...config, ssr: true})
Here is my working user context. I removed the signup function and will add it later once i work on it and test it.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
const [sub, setSub] = useState(null)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser()
.then(x=>{
setUser(x.username)
setUserEmail(x.attributes.email)
setSub(x.attributes.sub)
})
.catch((err)=>{
console.log(err)
setUser(null)
})
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false);
Auth.signIn(username, password)
.then((x) => {
setUser(x.username)
setSignInError(false)
console.log(x)
})
.catch((err)=>{
console.log(err)
setSignInError(true)
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
setSub(null)
})
}
}
const vals = useMemo( () => ({user, sub, login, logout, handleSignInError, userEmail, signInError}), [user, userEmail, signInError, sub])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
I have all my api functions in a special folder. When I use them, I sometimes have to update the context, which stores the user's informations.
However, the app crashes when I try to access its value directly from within these functions. So I always have to import the context in the component, and then pass the function setUser as a parameter to the api function. It is a bit messy. How to properly access and update the context's state in my api service?
Here is a classic function:
export async function useUpdatePassword(
email: string,
password: string,
token
) {
const { setUser } = useUser();
try {
const res = await ax.post(process.env.SERVER_URL + "/update-password", {
email,
password,
token,
});
localStorage.setItem("myapp", res.data.token);
setUser(jwt_decode(res.data.token));
return "update password done!!"
} catch (err) {
return "error"
}
}
And my context:
const UserContext = createContext(null);
export default function UserProvider({ children }) {
const [user, setUser] = useState<User>();
useEffect(() => {
fetchUser(setUser); // fetch user infos in my db
}, []);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
If there is no way to do it with Context, is it possible to use Redux, Jotai or any other library to do so?