Next js and firebase authentication login page problem - firebase-authentication

I'm using nextjs and having a problem with firebase authentication. When I log in it, I the session is stored at IndexedDB (I guess), and then I have a context that has an useEffect with the method onAuthStateChanged, which updates the user when it is changed.
Let's say I have a /login and a /dashboard (private page), when the login occurs, it should send me to /dashboard. That works fine. The problem comes when I try to go to /login (by typing the link in browser, thus refreshing) without logging off, which should send me back to /dashboard again. Instead of making the component be blank till the data is fetched, it loads the login page, only then renders the dashboard again.
const Dashboard = dynamic(() => import('../pages/dashboard'))
const router = useRouter()
const { signIn, user } = useAuth()
const { addToast } = useToast()
const formRef = useRef<FormHandles>(null)
useEffect(() => {
console.log(user)
if (!user) return
router.replace('/login', '/dashboard', { shallow: true })
}, [user])
I use dynamic to render the page conditionally
return (
<>
{user ? (
<Dashboard />
) : (
<>
<Head>
<title>Login</title>
</Head>
<Container>
<Content>
<img src={Logo} width={245} alt="Imobiliária Predial Primus" />
<Form ref={formRef} onSubmit={handleSubmit}>
<h1>Faça seu login</h1>
<Input name="email" placeholder="E-mail" />
<Input name="password" type="password" placeholder="Senha" />
<Button type="submit">Entrar</Button>
Esqueci minha senha
</Form>
</Content>
<Background />
</Container>
</>
)}
</>
)
As you can see, "user" is what conditionally renders the page, but since it comes null everytime the page loads, this problem occur.
This is the Auth.tsx context, which I wrap around the app.
import React, {
createContext,
useCallback,
useContext,
useState,
useEffect
} from 'react'
import { useRouter } from 'next/router'
// Firebase
import { auth } from '../config/firebase'
import { User } from '../interfaces'
interface SignInCredentials {
email: string
password: string
}
interface AuthContextData {
user: User
signIn(credentials: SignInCredentials): Promise<void>
signOut(): void
}
const AuthContext = createContext<AuthContextData>({} as AuthContextData)
const AuthProvider: React.FC = ({ children }) => {
const router = useRouter()
const [user, setUser] = useState(auth.currentUser)
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
console.log('user', user)
setUser(user)
if (!user) {
router.push('/login')
}
})
return () => {
unsubscribe()
}
}, [])
const signIn = useCallback(async (data: SignInCredentials) => {
await auth.signInWithEmailAndPassword(data.email, data.password)
}, [])
const signOut = useCallback(() => {
auth.signOut()
router.push('/login')
}, [])
return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{children}
</AuthContext.Provider>
)
}
function useAuth(): AuthContextData {
const context = useContext(AuthContext)
if (!context) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
export { AuthProvider, useAuth }

Fixed utilizing Cookies and NextPage, with getServerSideProps

Related

nextjs-ts-user-management, how to handle redirect to login page using #supabase/auth-helpers-nextjs?

TLDR:
I'm leveraging https://github.com/supabase/supabase/tree/master/examples/user-management/nextjs-ts-user-management
to build basically a TODO/hello world app and it's amazing. But you will notice that the way they handle loginpage/protected pages toggle in nextjs-ts-user-management is via session ? <Account/>: <Login/> (here) which is not compatible with using Next's file directory routing system and therefore doesn't allow me to use router etc. Is there an example somewhere that shows how to do this the correct way using urls?
Details:
I have successfully implemented a middleware.ts to redirect the user to login page if they're not authenticated per the instructions here which works great! https://supabase.com/docs/guides/auth/auth-helpers/nextjs#auth-with-nextjs-middleware
but it only works when user tries to navigate to a new page.
The middleware.ts does not redirect the user for the below situations:
user clicks a signOut button
user revisits the page after he is no longer authenticated
Is there some best practice way of addressing those 2 situations since it doesnt seem possible to do using middleware?
I've tried making "ProtectedRoutes" and stuff like that but it has 2 problems (1.no way to see if session is loading so login page flashes on page load, 2. it doesnt actually change the url, they are still on /profile but seeing the login page for example.)
After some digging I found this NextJS + Supabase - Blank Page Issue
but it still doesn't solve the "doesnt change the url" problem i listed above.
Any help or pointers is appreciated. I'm pretty new to Next and I love it, surely i am missing something simple here...
here is what I have currently:
"#supabase/auth-helpers-nextjs": "^0.5.2",
"#supabase/auth-helpers-react": "^0.3.1",
"#supabase/auth-ui-react": "^0.2.2",
"#supabase/supabase-js": "^2.0.4",
"next": "12.3.1",
"react": "18.2.0",
"react-dom": "18.2.0"
//_app.tsx
function MyApp({
Component,
pageProps,
}: AppProps<{
initialSession: Session
}>) {
const [supabaseClient] = useState(() => createBrowserSupabaseClient())
const queryClient = new QueryClient()
return (
<SessionContextProvider
supabaseClient={supabaseClient}
initialSession={pageProps.initialSession}
>
<QueryClientProvider client={queryClient}>
<RouteGuard>
<Component {...pageProps} />
</RouteGuard>
</QueryClientProvider>
</SessionContextProvider>
)
}
export default MyApp
const RouteGuard = ({ children }: { children: ReactElement }) => {
const session = useSession()
const { user, isAuthorizing } = useAuth()
if (isAuthorizing) {
return <div>Loading...</div>
}
if (!session) {
return <Login />
}
return <>{children}</>
}
//useAuth.js
import { useSupabaseClient } from '#supabase/auth-helpers-react'
import { useEffect, useState } from 'react'
export const useAuth = () => {
const [user, setUser] = useState(null)
const [isAuthorizing, setIsAuthorizing] = useState(true)
const supabase = useSupabaseClient()
useEffect(() => {
supabase.auth
.getUser()
.then((response) => {
setUser(response.data.user)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
setIsAuthorizing(false)
})
}, [])
return { user, isAuthorizing }
}
//login.tsx
import type { NextPage } from 'next'
import { Auth, ThemeSupa, ThemeMinimal } from '#supabase/auth-ui-react'
import { useSession, useSupabaseClient } from '#supabase/auth-helpers-react'
import { Layout } from '../components/Layout'
import { LayoutTailwind } from '../components/LayoutTailwind'
import { CodeBracketIcon, TvIcon } from '#heroicons/react/20/solid'
import { CurrencyDollarIcon, LifebuoyIcon } from '#heroicons/react/24/outline'
import { useRouter } from 'next/router'
export const Login: NextPage = () => {
const session = useSession()
const router = useRouter()
const supabase = useSupabaseClient()
if (session) {
router.push('/posts')
}
return (
<div className="mt-12">
<div className="flex min-h-full">
<div className="mx-auto w-full max-w-sm lg:w-96">
<div>
<CodeBracketIcon className="h-12" />
<h2 className="mt-6 text-3xl font-bold tracking-tight text-gray-900">Code Market</h2>
<div className="mt-2 text-sm text-gray-600 ">
<div className="flex items-center pb-1 gap-1">
<LifebuoyIcon className="h-4" />
Offer money for coding help
</div>
<div className="flex items-center gap-1">
<CurrencyDollarIcon className="h-4" />
Make money helping other coders
</div>
</div>
</div>
<div className="mt-8">
<Auth
providers={['github']}
supabaseClient={supabase}
appearance={{ theme: ThemeMinimal }}
/>
</div>
</div>
</div>
{/* <Footer /> */}
</div>
)
}
//middleware.ts
import { createMiddlewareSupabaseClient } from '#supabase/auth-helpers-nextjs'
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export async function middleware(req: NextRequest) {
// We need to create a response and hand it to the supabase client to be able to modify the response headers.
const res = NextResponse.next()
// Create authenticated Supabase Client.
const supabase = createMiddlewareSupabaseClient({ req, res })
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
// return res
// Check auth condition
if (session?.user.id) {
// Authentication successful, forward request to protected route.
return res
}
// Auth condition not met, redirect to home page.
const redirectUrl = req.nextUrl.clone()
redirectUrl.pathname = '/'
// redirectUrl.searchParams.set(`redirectedFrom`, req.nextUrl.pathname)
return NextResponse.redirect(redirectUrl)
}
export const config = {
matcher: ['/posts', '/createpost', '/profile'],
}
this ended up being the solution:
https://supabase.com/docs/guides/auth/auth-helpers/nextjs-server-components#supabase-listener
you should just look at the link above, but in case that goes away:
'use client'
import { useSupabaseClient } from '#supabase/auth-helpers-react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
export default function SupabaseListener({ accessToken }) {
const router = useRouter()
const supabase = useSupabaseClient()
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
if (session?.access_token !== accessToken) {
router.reload()
}
})
}, [accessToken])
return null
}
then you just put this somewhere high in your stack:
<SupabaseListener accessToken={session?.access_token} />
now whenever logged in status changes it refreshes the page which will then trigger your middleware.ts file to redirect you to login page

Why is the name of the user is not being shown instead of the Login icon in the menu bar after a successful login?

So I have an issue where I set up the frontend so that when the user logs in, the Login button in the menu bar will change to display the user's name currently logged in with a drop-down list showing both the user profile and logout buttons. But every time I'm logging in with a user, the Login in the menu bar does not change, and even though in the console it says that the user has logged in, I can still go back to the login screen and log in again.
Below is the code:
Navbar
function Navbar() {
const [showLinks, setShowLinks] = useState(false);
const { state, dispatch: ctxDispatch } = useContext(Store);
const { cart, userInfo } = state;
const handleLogout = () => {
ctxDispatch({ type: 'USER_LOGOUT' });
localStorage.removeItem('userInfo');
};
console.log(userInfo);
<div className="rightSide">
<div className="linksTwo">
{/* {userInfo ? (
<NavDropdown id="nav-dropdown-light" title= {userInfo.name}>
<LinkContainer to="/profile">
<NavDropdown.Item> User Profile</NavDropdown.Item>
</LinkContainer>
<NavDropdown.Divider />
<Link
className="dropdown-item"
to="#logout"
onClick={handleLogout}
>
{' '}
Logout
</Link>
</NavDropdown>
) : ( */}
<Link to="/login">
Login <LoginIcon />
</Link>
{/* )} */}
</div>
</div>
Login
import Axios from 'axios';
import { useContext, useState } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import Container from 'react-bootstrap/Container';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import { Helmet } from 'react-helmet-async';
import { Store } from '../components/Store';
import { toast } from 'react-toastify';
import { getError } from '../utils';
export default function Login() {
const navigate = useNavigate();
const { search } = useLocation();
const redirectInUrl = new URLSearchParams(search).get('redirect');
const redirect = redirectInUrl ? redirectInUrl : '/profile';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { state, dispatch: ctxDispatch } = useContext(Store);
const { userInfo } = state;
function validateForm() {
return email.length > 0 && password.length > 0;
}
const handleSubmit = async (e) => {
e.preventDefault();
if (!password) {
toast.warn('Password is not correct');
return;
}
try {
const { data } = await Axios.post('/api/users/login', {
email,
password,
});
ctxDispatch({ type: 'USER_LOGIN', payload: data });
localStorage.setItem('userInfo', JSON.stringify(data));
navigate(redirect || '/profile');
toast.success(email + ' has logged in successfully');
} catch (err) {
toast.error(getError(err));
}
};
return (
<Container className="small-container">
<Helmet>
<title>Login</title>
</Helmet>
<h1 className="my-3">Login</h1>
<Form onSubmit={handleSubmit}>
<Form.Group className="mb-3" controlId="email">
<Form.Label>Email</Form.Label>
<Form.Control
type="email"
required
onChange={(e) => setEmail(e.target.value)}
/>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
required
onChange={(e) => setPassword(e.target.value)}
autoComplete="on"
/>
</Form.Group>
<div className="m-3">
<Button
type="submit"
className="btn btn-outline-success me-2"
disabled={!validateForm()}
>
Login
</Button>
</div>
<div className="m-3">
New customer? <Link to={`/register`}>Register a new account</Link>
</div>
</Form>
</Container>
);
}
In Login component you are doing:
const { state, dispatch: ctxDispatch } = useContext(Store);
In Navbar Component you are doing:
const { state, userInfo, dispatch: ctxDispatch } = useContext(Store);
I guess that you are just destructuring your context bad in Navbar, try:
const { state, dispatch: ctxDispatch } = useContext(Store);
const { userInfo } = state;
Or you could also abbreviate it in : const { state: { userInfo }, dispatch: ctxDispatch } = useContext(Store);

Next js Firebase Auth phone number invisible recaptcha

Nextjs Firebase Phone Auth
First attempt useEffect()
useEffect(() => {
window.recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha', {
'size': 'invisible',
'callback': (response) => {
console.log("This is not fired on loading", response)
}
})
}, [])
return (
<>
<div id="recaptcha"></div>
<button onClick={clicked}> Click me </button>
</>
)
This runs, however the recaptcha doesn't work... User is forced to pick fire hydrants.
Second attempt: React Component
Inspiration: https://stackoverflow.com/a/63860925/7451631
Import this to Login page
class Recap extends Component {
constructor(props) {
super(props);
this.signIn = this.signIn.bind(this);
}
componentDidMount() {
window.reCaptchaVerifier = new firebase.auth.RecaptchaVerifier(this.recaptcha, {
'size': 'invisible',
'callback': function (response) {
console.log("Magic", response)
}
})
}
signIn() {
firebase.auth().signInWithPhoneNumber(phoneNumber, window.reCaptchaVerifier).catch((error) => {
console.log(error)
})
}
render() {
return (
<>
<div ref={(ref) => this.recaptcha = ref} onClick={this.signIn}> Clik meeeee </div>
</>
)
}
}
Works! I got a ugly solution while typing up this question. If anyone knows how to make it nicer or can explain why the first attempt did not work that would be dope.
here is my solutions:
import { createFirebaseApp } from '#utils/firebase';
import { getAuth, PhoneAuthProvider, RecaptchaVerifier, signInWithCredential } from 'firebase/auth';
import { useState } from 'react';
export default function Example() {
const app = createFirebaseApp();
const auth = getAuth(app);
const [code, setCode] = useState('');
const [verificationId, setVerificationId] = useState('');
const signInWithPhone1 = async () => {
const applicationVerifier = new RecaptchaVerifier(
'sign-in-button',
{
size: 'invisible',
},
auth,
);
const provider = new PhoneAuthProvider(auth);
const vId = await provider.verifyPhoneNumber('+855012000001', applicationVerifier);
setVerificationId(vId);
};
const verify = async () => {
const authCredential = PhoneAuthProvider.credential(verificationId, code);
const userCredential = await signInWithCredential(auth, authCredential);
console.log('verify: ', userCredential);
};
return (
<>
<button id="sign-in-button" onClick={signInWithPhone1}>
SignIn With Phone1
</button>
<div>
<input type="text" value={code} onChange={(v) => setCode(v.target.value)} />
<button onClick={verify}>Verify</button>
</div>
</>
);
}

How to navigate in const function [react native]

I have 2 questions:
How do I add props to this function in order to navigate
const usersRef = firestore().collection('Users');
//const signIn = React.useContext(AuthContext);
const { signIn } = React.useContext(AuthContext);
const CreateUser = async (email, password) => {
try {
let response = await auth().createUserWithEmailAndPassword(email, password)
if (response) {
console.log( "?", response)
}
} catch (e) {
console.error(e.message)
}
usersRef.add({
// Name: this.state.Name,
Email: email
})
navigation.navigate("SignIn")
}
export function RegisterScreen({navigation}) {
const [email, setEmail] = React.useState('');
const [password, setPassword] = React.useState('');
const { register } = React.useContext(AuthContext);
const {container, txtInput} = styles;
return (
<View style={container}>
<Text>Reg</Text>
<TextInput
placeholder="email"
value={email}
onChangeText={setEmail}
style={txtInput}
/>
<TextInput
placeholder="Password"
value={password}
onChangeText={setPassword}
secureTextEntry
style={txtInput}
/>
<Button title="Register" onPress={() => {
// navigation.navigate('signIn')
//register(email,password)
CreateUser(email,password)
}} />
</View>
);
}
I want to make it logged in after the registration is complete, so far as my understanding I need to get it to SignIn function of my AuthContext
I have const signIn = React.useContext(AuthContext);
and I try to do signIn(email,password) after or before the navigation.
please try this, where did you put it CreateUser
const { signIn } = React.useContext(AuthContext);
const data = {
username: state.username,
password: state.password
}
signIn(data)
App.js
it will switch automatically
{userToken ?
<RootStack.Navigator>
<RootStack.screen />
</RootStack.Navigator> :
<RootStack.Navigator>
<RootStack.screen />
</RootStack.Navigator>}
You can change routes by doing this
const CreateUser = async (data,navigation) => {
try {
let response = await auth().createUserWithEmailAndPassword(data.email, data.password)
if (response) {
console.log("test")
}
} catch (e) {
console.error(e.message)
}
usersRef.add({
// Name: this.state.Name,
Email: email
})
navigation.navigate("SignIn")
}
So I fixed it by changing registration to class then I was able to use navigation and using constructor props.
The only problem is that the auth context doesnt seem to support class. so I navigate after registration to signIn that remained as function so there Im able to use the authContext SignIn
If someone want to help me and tell me if its possible to manage it on class instead of function it would be greatful.

Async request is not going to Firebase through action creators

I am trying to create a LoginForm in which I am placing the the UI using react-native, but the backend logic is through redux framework. I have integrated with the firebase libraries and am trying to make an async call to the firebase using the action creators and reducers through redux-thunk.
App.js
.........
.........
render()
{
const store = createStore(reducers, {}, applyMiddleware(ReduxThunk));
return(
<Provider store={ store } >
<LoginForm />
</Provider>
);
}
LoginForm.js
class LoginForm extends Component
{
.........
.........
onButtonPress () {
const { email, password } = this.props;
this.props.loginUser({ email, password });
}
render()
{
<CardSection>
<Button onPress={this.onButtonPress.bind(this)} >
Login
</Button>
</CardSection>
}
const mapStateToProps = ( state ) => {
return {
email: state.auth.email,
password: state.auth.password
};
};
export default connect (mapStateToProps, { emailChanged, passwordChanged, loginUser })(LoginForm);
Actions
index.js
export const loginUser = ({ email, password }) => {
console.log("Shikher1");
return (dispatch) => {
firebase.auth().signInWithEmailAndPassword( email, password ).then( user => {
dispatch ({ type: 'LOGIN_USER_SUCCESS' , payload: user });
});
};
};
Nothing is mentioned in the Reducer as such, I just wanted to make sure that the action gets triggered and the Async call is made. But nothing gets happened here. As I printed from the console.logs I can see that the callback function is getting executed and it calls the action creator too, but the firebase statement is not getting executed, as after its execution, it will return an object. Why is the firebase statement is not getting executed?
Where am I going wrong here?
in in your LoginForm.js, try to add these lines
const mapStateToProps = ( state ) => {
return {
email: state.auth.email,
password: state.auth.password
};
};
const mapDispatchToProps = dispatch => ({
emailChanged: payload => dispatch(emailChanged(payload)),
passwordChanged: payload => dispatch(passwordChanged(payload)),
loginUser : payload => dispatch(loginUser (payload))
})
export default connect (mapStateToProps ,mapDispatchToProps )(LoginForm);