Is it ok to use getSession inside getServerSideProps to redirect an already logged in users if they try to access a custom login page? - next.js

Please find below the code of my (very simple, for demonstration purposes) custom login page. I am using getSession inside getServerSideProps to determine wheter there is already a session with a user. If that is the case, I redirect to the "root page". If not, I "hydrate" my page with the currently available "providers" as "props".
Is my approach valid? Or is there anything "more best-practice" I could do? And, specifically, is it ok to use getSession inside getServerSideProps in this way?
import { getProviders, getSession, signIn } from 'next-auth/react';
import type { BuiltInProviderType } from 'next-auth/providers';
import type { ClientSafeProvider, LiteralUnion } from 'next-auth/react';
import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
import { Session } from 'next-auth';
interface Properties {
providers: Record<
LiteralUnion<BuiltInProviderType, string>,
ClientSafeProvider
> | null;
}
export default function SignIn({ providers }: Properties) {
return (
<>
{providers &&
Object.values(providers).map((provider) => (
<div key={provider.name}>
<button
onClick={() => signIn(provider.id, { callbackUrl: '/test' })}
>
Sign in with {provider.name}
</button>
</div>
))}
</>
);
}
export const getServerSideProps: GetServerSideProps = async (
context: GetServerSidePropsContext
) => {
const session: Session | null = await getSession({ req: context.req });
if (session && session.user) {
console.log(
'Since there is already an active session with a user you will be redirected!'
);
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
return { props: { providers: await getProviders() } };
};

Related

NextJS - useSWR with token from session

I'm working with NextJS, Next-auth and Django as backend. I'm using the credentials provider to authenticate users. Users are authenticated against the Django backend and the user info together with the accesstoken is stored in the session.
I'm trying to use useSWR now to fetch data from the backend. (no preloading for this page required, that's why I'm working with SWR) I need to send the access_token from the session in the fetcher method from useSWR. However I don't know how to use useSWR after the session is authenticated. Maybe I need another approach here.
I tried to wait for the session to be authenticated and then afterwards send the request with useSWR, but I get this error: **Error: Rendered more hooks than during the previous render.
**
Could anybody help with a better approach to handle this? What I basically need is to make sure an accesstoken, which I received from a custom backend is included in every request in the Authorization Header. I tried to find something in the documentation of NextJS, Next-Auth or SWR, but I only found ways to store a custom access_token in the session, but not how to include it in the Header of following backend requests.
This is the code of the component:
import { useSession } from "next-auth/react";
import useSWR from 'swr';
import axios from 'axios'
export default function Profile() {
const { data: session, status } = useSession();
// if session is authenticated then fetch data
if (status == "authenticated") {
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
}
if (status == "loading") {
return (
<>
<span>Loading...</span>
</>
)
} else {
return (
<>
{data.email}
</>
)
}
}
you don't need to check status every time. what you need to do is to add this function to your app.js file
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/sign-in");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
then add auth proprety to every page that requires a session
Page.auth = {};
finally update your const App like this
<SessionProvider session={pageProps.session}>
<Layout>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</Layout>
</SessionProvider>
so every page that has .auth will be wrapped with the auth component and this will do the work for it
now get rid of all those if statments checking if session is defined since you page will be rendered only if the session is here
Thanks to #Ahmed Sbai I was able to make it work. The component now looks like this:
import { useSession } from "next-auth/react";
import axios from "axios";
import useSWR from 'swr';
Profile.auth = {}
export default function Profile() {
const { data: session, status } = useSession();
// create config with access_token for fetcher method
const config = {
headers: { Authorization: `Bearer ${session.access_token}` }
};
const url = "http://mybackend.com/user/"
const fetcher = url => axios.get(url, config).then(res => res.data)
const { data, error } = useSWR(url, fetcher)
if (data) {
return (
<>
<span>{data.email}</span>
</>
)
} else {
return (
<>
Loading...
</>
)
}
}
App component and function:
function Auth({ children }) {
const router = useRouter();
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push("/api/auth/signin");
},
});
if (status === "loading") {
return (
<div> Loading... </div>
);
}
return children;
}
function MyApp({
Component,
pageProps: { session, ...pageProps },
}) {
return (
<SessionProvider session={pageProps.session}>
{Component.auth ? (
<Auth>
<Component {...pageProps} />
</Auth>
) : (
<Component {...pageProps} />
)}
</SessionProvider>
)
}

How to redirect using `getServerSideProps` with props in Next.js?

After a user signs in, I use router.push() to redirect the user to their profile page. I am using getServerSideProps() for authentication right now. When the redirect happens, the props don't seem to be fetched and I have to refresh the browser myself to call gSSR. Is this behavior normal or is there a way to fix it?
Demonstration - Updated
login.js
import {useRouter} from 'next/router';
export default function Login({user}) {
const router = useRouter();
// invoked on submission
async function submitLoginForm(email, password) {
const user = await signIn(email, password)
const username = await getUsernameFromDB(user);
router.push("/" + username);
}
return ( ... );
}
export async function getServerSideProps({req}) {
const user = await getUserFromCookie(req);
if(user === null) {
return {
props: {}
}
}
return {
redirect: {
destination: `/${user.username}`,
permanent: false
}
}
}
[username].js
export default function Profile({user, isUser}) {
// Use isUser to render different interface.
return ( ... );
}
export async function getServerSideProps({params, req}) {
// The username of the path.
const profileUsername = params.username
// Current user.
const user = await getUserFromCookie(req);
...
if(user !== null) {
return {
props: {
user: user,
isUser: user !== null && profileUsername === user.username
}
}
}
return {
notFound: true
}
}
The cookie is set in the _app.js using the Supabase auth sdk.
function MyApp({Component, pageProps}) {
supabase.auth.onAuthStateChange( ( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
} );
} );
return (
<Component {...pageProps} />
);
}
I would recommend that you update your _app.js like that:
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
// make sure to run this code only once per application lifetime
useEffect(() => {
// might return an unsubscribe handler
return supabase.auth.onAuthStateChange(( event, session ) => {
fetch( "/api/auth", {
method: "POST",
headers: new Headers( { "Content-Type": "application/json" } ),
credentials: "same-origin",
body: JSON.stringify( { event, session } ),
});
});
}, []);
return <Component {...pageProps} />;
}
Also, please make clear what is happening. E.g. my current expectation:
Not authenticated user opens the "/login" page
He does some login against a backend, that sets a cookie value with user information
Then router.push("/" + username); is called
But the problem now: On page "/foo" he sees now the Not-Found page instead of the user profile
Only after page reload, you see the profile page correctly
If the above is correct, then it is possible the following line is not correctly awaiting the cookie to be persisted before the navigation happens:
const user = await signIn(email, password)
It could be that some internal promise is not correctly chained/awaited.
As an recommendation, I would log to the console the current cookie value before calling the router.push to see if the cookie was already saved.

check required auth in vue beforeEach method with firebase v9

i want to check if user exist before go to some pages in beforeEach method
i export the user state i use firebase v9
export const userAuthState = ()=>{
let currentUser = null;
onAuthStateChanged(auth, (user) => {
if (user) {
currentUser = user;
}
});
return currentUser;
}
here where i use it
import {userAuthState} from 'src/backend/firebase-config';
...
console.log("before route");
Router.beforeEach(async (to,from,next)=>{
if(await !userAuthState() && to.meta.requiresAuth){
next({path: 'login', query:{ redirect: to.fullPath }})
}else if(await userAuthState() && to.meta.requiresAuth == 'login'){
next({path:'/'})
}else{
next()
}
})
here the problem cant navigate to any page and print the console.log many times
how i can check the user before route in correct way
thank you.
I'll give you a simple example of how can you make some decision based on user authentication.
For this, I'll use Vuex as a central store since you'll commonly use user information across all your app. I'll assume that you're building an SPA with Vue and Firebase V9.
This is a simple store for users. Register this store with Vue (with .use()) in your main.js file (your entry point file).
import { createStore } from 'vuex'
const Store = createStore({
state() {
return {
user: {
uid: '',
email: ''
}
}
},
mutations: {
setUser (state, payload) {
if (payload) {
state.user.uid = payload.uid
state.user.email = payload.email
return
}
state.user.uid = ''
state.user.email = ''
}
}
})
export Store
Now, at your App.vue (or your root component) you simple call onAuthStateChanged and run commits depending on User's state:
<template>
<div>Your wonderful template...</div>
</template>
<script>
import { onAuthStateChanged } from "firebase/auth";
import { yourAuthService } from 'yourFirebaseInit'
export default {
name: 'App',
created () {
onAuthStateChanged(yourAuthService, (user) => {
if (user) {
this.$store.commit('setUser', { uid: user.uid, email: user.email })
} else {
this.$store.commit('setUser', null)
}
})
}
}
</script>
Finally, in your routes, you could do something like:
// we import the Store that we've created above.
import { Store } from 'your-store-path'
Router.beforeEach((to,from,next)=>{
if(to.meta.requiresAuth && Store.state.user.uid === ''){
next({path: 'login', query:{ redirect: to.fullPath }})
} else{
next()
}
})
This is a simple example of how can you implement an Authentication flow with Vue and Firebase V9.

Next.js: How to clear browser history with Next Router?

I created a wrapper for the pages which will bounce unauthenticated users to the login page.
PrivateRoute Wrapper:
import { useRouter } from 'next/router'
import { useUser } from '../../lib/hooks'
import Login from '../../pages/login'
const withAuth = Component => {
const Auth = (props) => {
const { user } = useUser();
const router = useRouter();
if (user === null && typeof window !== 'undefined') {
return (
<Login />
);
}
return (
<Component {...props} />
);
};
if (Component.getInitialProps) {
Auth.getInitialProps = Component.getInitialProps;
}
return Auth;
};
export default withAuth;
That works \o/, However I noticed a behavior when I log out, using Router.push('/',), to return the user to the homepage the back button contains the state of previous routes, I want the state to reset, as a user who is not authenticated should have an experience as if they're starting from scratch...
Thank you in advance!
You can always use Router.replace('/any-route') and the user will not be able to go back with back button

Protect pages from not logged in user in Nextjs

I am creating a login page and dashboard for the admin panel using NExtjS and react-redux. Below is the code I have tried. If I login using Id and password I can login and get all the values from the state and everything works fine.
The problem is if I tried to access the dashboard URL directly it says
Cannot read properties of null (reading 'name') how can I redirect the user to the login page instead of getting up to return statement ???
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import { useRouter } from 'next/router';
import dynamic from 'next/dynamic';
const Dashboard = () => {
const { auth } = useSelector((state) => state);
const router = useRouter();
console.log(auth)
// I can get all the objects from state and cookies are set as state for browser reload so everything is fine here.
useEffect(() => {
if (!auth.userInfo && auth.userInfo.role == 'user') {
router.push('/admin');
console.log('I am here');
}
}, []);
return <h1>{auth.userInfo.name}</h1>;
};
export default dynamic(() => Promise.resolve(Dashboard), { ssr: false });
Finally I find the correct way of solving this issue. The correct way was:
export const getServerSideProps = async (context) => {
const session = await getSession({ req: context.req });
if (session) {
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
return {
props: {
session,
},
};
};

Resources