Protect pages from not logged in user in Nextjs - redux

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,
},
};
};

Related

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

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

Get supabase `user` server side in next.js

I am attempting to get the current logged in supabase user while server side.
I have attempted to use const user = supabase.auth.user(); but I always get a null response.
I have also attempted const user = supabase.auth.getUserByCookie(req) but it also returns null. I think because I am not sending a cookie to the api when calling it from the hook.
I have tried passing the user.id from the hook to the api but the api is not receiving the parameters.
I also attempted this approach but the token is never fetched. It seems to not exist in req.cookies.
let supabase = createClient(supabaseUrl, supabaseKey);
let token = req.cookies['sb:token'];
if (!token) {
return
}
let authRequestResult = await fetch(`${supabaseUrl}/auth/v1/user`, {
headers: {
'Authorization': `Bearer ${token}`,
'APIKey': supabaseKey
}
});
`
Does anyone know how to get the current logged in user in server side code?
If you need to get the user in server-side, you need to set the Auth Cookie in the server using the given Next.js API.
// pages/api/auth.js
import { supabase } from "../path/to/supabaseClient/definition";
export default function handler(req, res) {
if (req.method === "POST") {
supabase.auth.api.setAuthCookie(req, res);
} else {
res.setHeader("Allow", ["POST"]);
res.status(405).json({
message: `Method ${req.method} not allowed`,
});
}
}
This endpoint needs to be called every time the state of the user is changed, i.e. the events SIGNED_IN and SIGNED_OUT
You can set up a useEffect in _app.js or probably in a User Context file.
// _app.js
import "../styles/globals.css";
import { supabase } from '../path/to/supabaseClient/def'
function MyApp({ Component, pageProps }) {
useEffect(() => {
const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
handleAuthChange(event, session)
if (event === 'SIGNED_IN') {
// TODO: Actions to Perform on Sign In
}
if (event === 'SIGNED_OUT') {
// TODO: Actions to Perform on Logout
}
})
checkUser()
return () => {
authListener.unsubscribe()
}
}, [])
return <Component {...pageProps} />;
}
async function handleAuthChange(event, session) {
await fetch('/api/auth', {
method: 'POST',
headers: new Headers({ 'Content-Type': 'application/json' }),
credentials: 'same-origin',
body: JSON.stringify({ event, session }),
})
}
export default MyApp;
You can now handle this user with a state and pass it to the app or whichever way you'd like to.
You can get the user in the server-side in any Next.js Page
// pages/user_route.js
import { supabase } from '../path/to/supabaseClient/def'
export default function UserPage ({ user }) {
return (
<h1>Email: {user.email}</h1>
)
}
export async function getServerSideProps({ req }) {
const { user } = await supabase.auth.api.getUserByCookie(req)
if (!user) {
return { props: {}, redirect: { destination: '/sign-in' } }
}
return { props: { user } }
}
Here's a YouTube Tutorial from Nader Dabit - https://www.youtube.com/watch?v=oXWImFqsQF4
And his GitHub Repository - https://github.com/dabit3/supabase-nextjs-auth
supabase have a library of helpers for managing auth for both client- and server-side auth and fetching in a couple of frameworks including Next.js: https://github.com/supabase/auth-helpers and appears to be the recommended solution for similar problems based on this thread: https://github.com/supabase/supabase/issues/3783
This is how I'm using it in an API handler, but provided you have access to req, you can access the user object this way:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies["sb-access-token"]);
Note that you will need to use the helper library supabaseClient and supabaseServerClient on the client and server side respectively for this to work as intended.
I was following a tutorial today and was having a similar issue and the below is how i managed to fix it.
I've got this package installed github.com/jshttp/cookie which is why i'm calling cookie.parse.
Supabase Instance:
`//../../../utils/supabase`
import { createClient } from "#supabase/supabase-js";
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.NEXT_PUBLIC_SUPABASE_KEY
);
In my case this was my API page:
import { supabase } from "../../../utils/supabase";
import cookie from "cookie";
import initStripe from "stripe";
const handler = async (req, res) => {
const { user } = await supabase.auth.api.getUserByCookie(req);
if (!user) {
return res.status(401).send("Unathorized");
}
const token = cookie.parse(req.headers.cookie)["sb-access-token"];
supabase.auth.session = () => ({
access_token: token,
});`
const {
data: { stripe_customer },
} = await supabase
.from("profile")
.select("stripe_customer")
.eq("id", user.id)
.single();
For anyone who tries to figure out how to get the user server side with the new #supabase/auth-helpers-nextjs, Michele gave the answer.
Just a note: If you're trying to get the user on nextJs's Middleware, instead of:
... req.cookies["sb-access-token"]
You have to use: req.cookies.get('sb-access-token')
For example:
import { supabaseServerClient } from '#supabase/auth-helpers-nextjs';
const { user } = await supabaseServerClient({ req, res }).auth.api.getUser(req.cookies.get('sb-access-token'))
UPDATE: 2023. Available now on Supabase Docs here
import { createServerSupabaseClient } from '#supabase/auth-helpers-nextjs'
export default function Profile({ user }) {
return <div>Hello {user.name}</div>
}
export const getServerSideProps = async (ctx) => {
// Create authenticated Supabase Client
const supabase = createServerSupabaseClient(ctx)
// Check if we have a session
const {
data: { session },
} = await supabase.auth.getSession()
if (!session)
return {
redirect: {
destination: '/',
permanent: false,
},
}
return {
props: {
initialSession: session,
user: session.user,
},
}
}

Vue + Pinia + Firebase Authentication: Fetch currentUser before Route Guard

Recently I started to use Pinia as a global store for my Vue 3 Project. I use Firebase for the user authentication and am trying to load the current user before Vue is initialized. Ideally everything auth related should be in a single file with a Pinia Store. Unfortunately (unlike Vuex) the Pinia instance needs to be passed to the Vue instance before I can use any action and I believe that is the problem. On first load the user object in the store is empty for a short moment.
This is the store action that is binding the user (using the new Firebase Web v9 Beta) in auth.js
import { defineStore } from "pinia";
import { firebaseApp } from "#/services/firebase";
import {
getAuth,
onAuthStateChanged,
getIdTokenResult,
} from "firebase/auth";
const auth = getAuth(firebaseApp);
export const useAuth = defineStore({
id: "auth",
state() {
return {
user: {},
token: {},
};
},
actions: {
bindUser() {
return new Promise((resolve, reject) => {
onAuthStateChanged(
auth,
async (user) => {
this.user = user;
if (user) this.token = await getIdTokenResult(user);
resolve();
},
reject()
);
});
},
// ...
}})
and this is my main.js file
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import { useAuth } from "#/store/auth";
(async () => {
const app = createApp(App).use(router).use(createPinia());
const auth = useAuth();
auth.bindUser();
app.mount("#app");
})();
How can I set the user before anything else happens?
I figured it out. Had to register the router after the async stuff
//main.js
(async () => {
const app = createApp(App);
app.use(createPinia());
const { bindUser } = useAuth();
await bindUser();
app.use(router);
app.mount("#app");
})();

Trying to redirect from getStaticProps returns error during build

I'm currently trying to redirect inside of getStaticProps based on an API call which checks for a cookie. If the cookie is present, user is authenticated, which means no redirect, but if missing, redirect.
import React from "react";
import { GetStaticProps } from "next";
const Chat: React.FC = () => {
return null;
};
export const getStaticProps: GetStaticProps = async ({ locale, defaultLocale }) => {
const { authenticated } = await fetch("http://localhost:4000/api/auth").then(res => res.json());
if (!authenticated) {
return {
redirect: {
permanent: false,
destination: "/user/login",
},
};
}
return {
props: {
defaultLocale,
locale,
},
};
};
export default Chat;
This works fine during runtime, but on build, I get the following error:
Error: redirect can not be returned from getStaticProps during prerendering (/chat)
How so? The official next.js doc even shows how to redirect from getStaticProps. I even added the redirect to next.config.js, but somehow it still fails.
module.exports = {
async redirects() {
return [
{
source: "/chat",
destination: "/user/login",
permanent: false,
},
];
},
...
}
FYI the application uses i18n to danle translations, locales etc.

How to store firebase token in Vuex store

I'm trying to store the accessToken that comes back from Firebase into the Vuex store in my Vue project when I run the code I get the following error:
Uncaught TypeError: Cannot read property '$store' of undefined
It looks like it's not picking up store.js in the code below, any ideas why that might be?
Thanks
Login.vue
mounted: function() {
Firebase.auth.onAuthStateChanged( user => {
if (user) {
user.getIdToken().then(function(idToken) {
this.$store.commit('setStoreToken', idToken)
return idToken
});
}
else {
.
.
.
}
store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
loggedIn: false,
accessToken: '',
},
mutations: {
loggedIn () {
this.state.loggedIn = true
},
loggedOut () {
this.state.loggedIn = false
},
setStoreToken(state, accessToken) {
state.accessToken = accessToken
},
},
getters: {
getAccessToken: state => {
return state.accessToken
}
},
})
main.js
import store from './store.js'
new Vue({
store,
router,
render: h => h(App),
}).$mount('#app')
My guess:
user.getIdToken().then(function(idToken) {
this.$store.commit('setStoreToken', idToken)
return idToken
});
You dont use an arrow function, that means that this is binded to the function itself you can either use an arrow function or you create a variable infront of it like this const self = this
user.getIdToken().then((idToken)=>{
this.$store.commit('setStoreToken', idToken)
return idToken
});
Or you do it like this:
const self = this;
user.getIdToken().then(function(idToken) {
self.$store.commit('setStoreToken', idToken)
return idToken
});

Resources