Reputation: 3176
Objective : I want to redirect a logged in user to the home page if he/she tries to manually go to the /auth/signin.
Signin page/component :
const Signin = ({ currentUser }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { doRequest, errors } = useRequest({
url: '/api/users/signin',
method: 'post',
body: {
email, password
},
onSuccess: () => Router.push('/')
});
useEffect(() => {
const loggedUser = () => {
if (currentUser) {
Router.push('/');
}
};
loggedUser();
}, []);
Custom _app component:
const AppComponent = ({ Component, pageProps, currentUser }) => {
return (
<div>
<Header currentUser={currentUser} />
<Component {...pageProps} currentUser={currentUser} />
</div>
)
};
AppComponent.getInitialProps = async (appContext) => {
const client = buildClient(appContext.ctx);
const { data } = await client.get('/api/users/currentuser');
let pageProps = {};
if (appContext.Component.getInitialProps) {
pageProps = await appContext.Component.getInitialProps(appContext.ctx);
}
return {
pageProps,
...data
}
};
export default AppComponent;
Issue :
I tried this, but this causes a slight delay as it won't be server side rendered. By delay I mean: It shows the page I don't want to show for a second or so before redirecting.
I could use a loading sign or bunch of if else conditions, but that would be a work around, what would be the best approach/practice to handle this issue?
Another solution I came up with:
import Router from 'next/router';
export default (ctx, target) => {
if (ctx.res) {
// server
ctx.res.writeHead(303, { Location: target });
ctx.res.end();
} else {
// client
Router.push(target);
}
}
import React from 'react';
import redirect from './redirect';
const withAuth = (Component) => {
return class AuthComponent extends React.Component {
static async getInitialProps(ctx, { currentUser }) {
if (!currentUser) {
return redirect(ctx, "/");
}
}
render() {
return <Component {...this.props} />
}
}
}
export default withAuth;
export default withAuth(NewTicket);
Is there any better approach to handling this? Would really appreciate the help.
Upvotes: 19
Views: 45009
Reputation: 4539
Vercel recently introduced middleware for Next.js. Next.js middleware allows you to run code before an HTTP request is handled.
To add your middleware logic to your app, add a middleware.js
or middleware.ts
file in the root directory of your Next.js project.
export async function middleware(req: NextRequest) {
const token = req.headers.get('token') // get token from request header
const userIsAuthenticated = true // TODO: check if user is authenticated
if (!userIsAuthenticated) {
const signinUrl = new URL('/signin', req.url)
return NextResponse.redirect(signinUrl)
}
return NextResponse.next()
}
// Here you can specify all the paths for which this middleware function should run
// Supports both a single string value or an array of matchers
export const config = {
matcher: ['/api/auth/:path*'],
}
Upvotes: 1
Reputation: 1885
// Authentication.js
import { useRouter } from "next/router";
import React, { useEffect } from "react";
function Authentication(props) {
let userDetails;
const router = useRouter();
useEffect(() => {
if (typeof window !== undefined) {
userDetails=useSession
if (!userDetails) {
const path = router.pathname;
switch (path) {
case "/":
break;
case "/about":
break;
case "/contact-us":
break;
default:
router.push("/");
}
} else if (userDetails) {
if (router.pathname == "/") {
router.push("/home");
}
}
}
}, []);
return <>{props.children}</>;
}
export default Authentication;
Now add this code in your _app.js
<DefaultLayout>
<Authentication>
<Component {...pageProps} />
</Authentication>
</DefaultLayout>
Evrything should work now, if you want add loading.
Upvotes: 1
Reputation: 2536
I have faced the same problem and my client-side solution which does not flash the content is as follows: Please correct me if I have done it in the wrong way.
I use useRouter
//@/utils/ProtectedRoute
import { useRouter } from "next/router";
import { useState, useEffect } from "react";
export const ProtectedRoute = ({ user = false, children }) => {
const [login, setLogin] = useState(false);
const router = useRouter();
useEffect(() => {
login && router.push("/account/login");//or router.replace("/account/login");
}, [login]);
useEffect(() => {
!user && setLogin(true);
}, []);
return (
<>
{user ? (
children
) : (
<div>
<h4>
You are not Authorized.{" "}
<Link href="/account/login">
<a>Please log in</a>
</Link>
</h4>
</div>
)}
</>
};
)
When I want to protect a route I use this syntax:
import { ProtectedRoute } from "@/utils/ProtectedRoute";
const ProtectedPage = () => {
const user = false;
return (
<ProtectedRoute user={user}>
<h1>Protected Content</h1>
</ProtectedRoute>
);
};
export default ProtectedPage;
Upvotes: 1
Reputation: 108
export const getServerSideProps = wrapper.getServerSideProps(
(store) =>
async ({ req, params }) => {
const session = await getSession({ req });
if (!session) {
return {
redirect: {
destination: '/',
permanent: false,
},
};
}
}
);
Here in Next 9++ you can do like this just check the session, if there are none, we can return a redirect with destination to route the user to end point!
Upvotes: 3
Reputation: 1413
Just to expand on what @Nico was saying in his comment. Here is how I set it up:
Layout.tsx
file
// ...
import withAuth from "../utils/withAuth";
interface Props {
children?: ReactNode;
title?: string;
}
const Layout = ({
children,
title = "This is the default title",
}: Props): JSX.Element => (
<>
{children}
</>
);
export default withAuth(Layout);
And withAuth.js
file
import { getSession } from "next-auth/client";
export default function withAuth(Component) {
const withAuth = (props) => {
return <Component {...props} />;
};
withAuth.getServerSideProps = async (ctx) => {
return { session: await getSession(ctx) };
};
return withAuth;
}
Upvotes: 2
Reputation: 890
Here's an example using a custom "hook" with getServerSideProps.
I am using react-query, but you can use whatever data fetching tool.
// /pages/login.jsx
import SessionForm from '../components/SessionForm'
import { useSessionCondition } from '../hooks/useSessionCondition'
export const getServerSideProps = useSessionCondition(false, '/app')
const Login = () => {
return (
<SessionForm isLogin/>
)
}
export default Login
// /hooks/useSessionCondition.js
import { QueryClient } from "react-query";
import { dehydrate } from 'react-query/hydration'
import { refreshToken } from '../utils/user_auth';
export const useSessionCondition = (
sessionCondition = true, // whether the user should be logged in or not
redirect = '/' // where to redirect if the condition is not met
) => {
return async function ({ req, res }) {
const client = new QueryClient()
await client.prefetchQuery('session', () => refreshToken({ headers: req.headers }))
const data = client.getQueryData('session')
if (!data === sessionCondition) {
return {
redirect: {
destination: redirect,
permanent: false,
},
}
}
return {
props: {
dehydratedState: JSON.parse(JSON.stringify(dehydrate(client)))
},
}
}
}
Upvotes: 6
Reputation: 1443
I would really recommend looking at the examples to see how NextJS suggest handling this. The resources are really good!
https://github.com/vercel/next.js/tree/master/examples
For example you could use next-auth
which is an open-source auth option.
The example is here. https://github.com/vercel/next.js/tree/master/examples/with-next-auth
// _app.js
import { Provider } from 'next-auth/client'
import '../styles.css'
const App = ({ Component, pageProps }) => {
const { session } = pageProps
return (
<Provider options={{ site: process.env.SITE }} session={session}>
<Component {...pageProps} />
</Provider>
)
}
export default App
// /pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth'
import Providers from 'next-auth/providers'
const options = {
site: process.env.VERCEL_URL,
providers: [
Providers.Email({
// SMTP connection string or nodemailer configuration object https://nodemailer.com/
server: process.env.EMAIL_SERVER,
// Email services often only allow sending email from a valid/verified address
from: process.env.EMAIL_FROM,
}),
// When configuring oAuth providers make sure you enabling requesting
// permission to get the users email address (required to sign in)
Providers.Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
Providers.Facebook({
clientId: process.env.FACEBOOK_ID,
clientSecret: process.env.FACEBOOK_SECRET,
}),
Providers.Twitter({
clientId: process.env.TWITTER_ID,
clientSecret: process.env.TWITTER_SECRET,
}),
Providers.GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
],
// The 'database' option should be a connection string or TypeORM
// configuration object https://typeorm.io/#/connection-options
//
// Notes:
// * You need to install an appropriate node_module for your database!
// * The email sign in provider requires a database but OAuth providers do not
database: process.env.DATABASE_URL,
session: {
// Use JSON Web Tokens for session instead of database sessions.
// This option can be used with or without a database for users/accounts.
// Note: `jwt` is automatically set to `true` if no database is specified.
// jwt: false,
// Seconds - How long until an idle session expires and is no longer valid.
// maxAge: 30 * 24 * 60 * 60, // 30 days
// Seconds - Throttle how frequently to write to database to extend a session.
// Use it to limit write operations. Set to 0 to always update the database.
// Note: This option is ignored if using JSON Web Tokens
// updateAge: 24 * 60 * 60, // 24 hours
// Easily add custom properties to response from `/api/auth/session`.
// Note: This should not return any sensitive information.
/*
get: async (session) => {
session.customSessionProperty = "ABC123"
return session
}
*/
},
// JSON Web Token options
jwt: {
// secret: 'my-secret-123', // Recommended (but auto-generated if not specified)
// Custom encode/decode functions for signing + encryption can be specified.
// if you want to override what is in the JWT or how it is signed.
// encode: async ({ secret, key, token, maxAge }) => {},
// decode: async ({ secret, key, token, maxAge }) => {},
// Easily add custom to the JWT. It is updated every time it is accessed.
// This is encrypted and signed by default and may contain sensitive information
// as long as a reasonable secret is defined.
/*
set: async (token) => {
token.customJwtProperty = "ABC123"
return token
}
*/
},
// Control which users / accounts can sign in
// You can use this option in conjunction with OAuth and JWT to control which
// accounts can sign in without having to use a database.
allowSignin: async (user, account) => {
// Return true if user / account is allowed to sign in.
// Return false to display an access denied message.
return true
},
// You can define custom pages to override the built-in pages
// The routes shown here are the default URLs that will be used.
pages: {
// signin: '/api/auth/signin', // Displays signin buttons
// signout: '/api/auth/signout', // Displays form with sign out button
// error: '/api/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/api/auth/verify-request', // Used for check email page
// newUser: null // If set, new users will be directed here on first sign in
},
// Additional options
// secret: 'abcdef123456789' // Recommended (but auto-generated if not specified)
// debug: true, // Use this option to enable debug messages in the console
}
const Auth = (req, res) => NextAuth(req, res, options)
export default Auth
So the option above is defo a server-side rendered app since we're using /api paths for the auth. If you want a serverless solution you might have to pull everything from the /api path into a lambda (AWS Lambda) + a gateway api (AWS Api Gateway). All you'd need there is a custom hook that connects to that api. You can do this in different ways too of course.
Here is another auth example using firebase.
https://github.com/vercel/next.js/tree/master/examples/with-firebase-authentication
And another example using Passport.js
https://github.com/vercel/next.js/tree/master/examples/with-passport
Also you asked about a loading behaviour, well this example might help you there
https://github.com/vercel/next.js/tree/master/examples/with-loading
🙌
The custom _app component is usually a top level wrapper (not quite the very top _document would fit that description).
Realistically I'd create a Login component one step below the _app. Usually I'd achieve that pattern in a Layout component or like the examples above are doing it, using an api path or a utility function.
Upvotes: 13
Reputation: 984
Upgrade NextJs to 9.3+ and use getServerSideProps
instead of getInitialProps
. getServerSideProps
runs only and always server side unlike getInitialProps
. Redirect from getServerSideProps
if auth fails.
Upvotes: 3