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

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

Related

PWA + Keycloak - Authentication doesn't work when refresh page

I'm implementing a keycloak authentication on a PWA with CRA.
That the user could authenticate to the app when connected to the internet, and if disconnecting to the internet and the user refreshes the page, Keycloak should make use of the stored tokens, but it happen that Keycloak tries to initialize when the page is refreshed and throws an error
“Timeout waiting for 3rd party verification iframe message.”
I'm using:
Keycloak server 12.0.4, dependencies:
"#react-keycloak/web": "^3.4.0",
"keycloak-js": "^19.0.1",
What I'm doing wrong?
Any comments are appreciated!
*Keycloak.js
import Keycloak from 'keycloak-js';
const config = {
url: process.env.REACT_APP_IAM_URL,
realm: process.env.REACT_APP_IAM_REALM,
clientId: process.env.REACT_APP_IAM_CLIENTID,
};
const keycloak = new Keycloak(config);
export default keycloak;
App.js
import { ReactKeycloakProvider } from '#react-keycloak/web';
import keycloakConf from './keycloak';
import Routes from './routes/Routes';
import Interceptor from 'services/Interceptor';
import AppProvider from './context';
import LazyLoader from './LazyLoader';
function App() {
const token = localStorage.getItem('#token'),
idToken = localStorage.getItem('#idToken'),
refreshToken = localStorage.getItem('#refreshToken');
const tokens = {
...(token !== 'undefined' ? { token } : {}),
...(idToken !== 'undefined' ? { idToken } : {}),
...(refreshToken !== 'undefined' ? { refreshToken } : {}),
};
return (
<ReactKeycloakProvider authClient={keycloakConf} initOptions={{
onLoad: 'check-sso'
enableLogging: true,
checkLoginIframe: false,
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
silentCheckSsoFallback: false,
...tokens,
}}>
<AppProvider>
<BrowserRouter>
<Interceptor />
<Routes />
</BrowserRouter>
</AppProvider>
</ReactKeycloakProvider>
);
}
export default App;
silent.check-sso.html
<html>
<body>
<script>
parent.postMessage(location.href, location.origin)
</script>
</body>
</html>
Routes.js
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom';
import Admin from 'layouts/Admin';
import { useKeycloak } from '#react-keycloak/web';
export default function Routes() {
const { keycloak, initialized } = useKeycloak();
if (!initialized) {
return <Spinner />;
}
if (!keycloak.authenticated) {
keycloak.login({
scope: 'offline_access',
redirectUri: 'http://localhost:3006/',
});
}
return (
<Switch>
<Route path="/admin" component={Admin} />
<Redirect from="/" exact to="/admin/page" />
</Switch>
);
}

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);

PDFtron & react Error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer

I am trying to show in the app that I built in React a PDF file using PDFtron and encounter the following error: Two instances of WebViewer were created on the same HTML element. Please create a new element for each instance of WebViewer.
my code is:
import { url } from "../../../../config.json";
import React, { useState, useEffect, useRef } from "react";
import { getProject } from "../../../../services/projectService";
import { useParams } from "react-router-dom";
import WebViewer from "#pdftron/webviewer";
import { getCurrentUser } from "../../../../services/userService";
import { Link, Redirect } from "react-router-dom";
import { deleteImage } from "../../../../services/projectService";
const MyContracts = () => {
const [project, setProject] = useState({});
const [counter, setCounter] = useState(0);
const [files, setFiles] = useState([]);
const { id } = useParams();
// const viewerDiv = useRef();
const user = getCurrentUser();
const [viewerUrl, setViewerUrl] = useState(`${url}/files/testing.pdf`);
const viewer = document.getElementById("viewer");
useEffect(() => {
getProject(id)
.then(res => {
setProject(res.data);
setFiles(res.data.files.contracts);
})
.catch(error => console.log(error.message));
}, []);
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
if (!user) return <Redirect to="/private-area/sign-in" />;
if (user && user.isAdmin | (user._id === project.userID))
return (
<div className="container">
</div>
{/********** PDF VIEWER ************/}
<div className="web-viewer" id="viewer"></div>
{/* <div className="web-viewer" ref={viewerDiv} id="viewer"></div> */}
{/********** PDF Gallery ************/}
{files !== undefined && (
<>
<h2 className="text-rtl h3Title mt-2">בחר קובץ</h2>
<select
id="select"
className="col-12 text-rtl px-0"
onChange={e => setViewerUrl(e.target.value)}>
{files.map((file, index) => (
<option value={`${url}${file.url}`} key={index}>
{file.name}
</option>
))}
</select>
</>
)}
</div>
);
};
export default MyContracts;
What am I doing wrong and how can I fix it?
I see that you are trying to load multiple instances of WebViewer:
useEffect(() => {
if (files.length > 0) {
WebViewer(
{
path: `${url}/lib`,
initialDoc: `${url}/files/testing.pdf`,
fullAPI: true,
},
viewer
).then(async instance => {
const { docViewer } = instance;
docViewer.getDocument(viewerUrl);
});
}
}, [files, viewerUrl]);
Webviewer cannot be instantiated more than once in the same HTML element. If you need a completely different instance, you can hide or remove the HTML element and create a new one to hold the new instance.
That being said, if you just need to load another document, I would recommend using the loadDocument API. You can read more about it here as well.
​

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>
</>
);
}

Next js and firebase authentication login page problem

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