Redirecting from server side in NextJS - reactjs

I'm developing a Next.JS app where a user should login to see the content.I want to redirect the user to '/' if their username and password are correct.However my implementation seems not working.
I searched on SO for questions regarding this,but all of them were talking about redirecting with getInitialProps but it doesn't help me since I want to redirect user from my custom express server.
Login.js
async handleSubmit(event) {
event.preventDefault()
const { username, password } = this.state
try {
const response = await fetch('/log_in', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
}
catch (error) {
console.log(error)
}
}
server.js
app.post('/log_in', (req,res) => {
console.log(`login form data`);
console.log(`Username : ${req.body.username}`);
console.log(`password : ${req.body.password}`);
if(req.body.username == "user" && req.body.password == "123"){
res.redirect('/')
}
})

This is now possible on Next.js 10+ without having to do any sort of response header manipulation directly. Use getServerSideProps or getStaticProps and return a redirect key as the only value from it if your conditions are met:
export async function getServerSideProps(context) {
if(req.body.username == "user" && req.body.password == "123"){
return {
redirect: {
permanent: false,
destination: "/"
}
}
}
}

It's a good idea for SEO to redirect server side if you can (not for login like the questions mention).
But for things like user language etc it's probably a good idea to use server side redirects like this.
----- REMOVED since it was bad code --------
Update, I ended up with this as I did have some problems with headers already sent. Please let me know how this can be fixed as it's not the most ideal (although Google does understand it according to this article https://www.seroundtable.com/google-meta-refresh-redirects-work-25335.html)
export default function HomePage({ language }) {
return (
<Head>
<title>-</title>
<meta httpEquiv="refresh" content={`0;url=/${language}/discover/0`} />
</Head>
);
}
Update
Nextjs is adding native support there is an RFC available: https://github.com/vercel/next.js/discussions/17078

With Next.JS, you need to redirect the client on the client side. Try to return a 200 status code if the login is successful and then use the client router on success to navigate to another page. This example should give you an idea on how you can programmatically push a new page to the router history:
import Router from 'next/router'
() => {
if(code==200){
Router.push('/')
}
}

Related

How to re-route a React-Admin url

I'm trying to configure the login page for my React-Admin app. I'm using authProvider and LoginPage components, as per the documentation and my login process is generally working.
I have the app running locally on port 3000. But when I go to http://localhost:3000, I'm automatically redirected to http://localhost:3000/#/login.
What specifically is driving that redirection? I don't specify that .../#/login url within the app itself.
I'm using an old version of React-Admin (2.9), which I understand uses Redux. Is that redirection to .../#/login a function of Redux? Or of React-Admin itself?
My understanding is I can maybe use HashHistory or BrowserHistory to prevent the # - but not sure if that's compatible with React-Admin.
The actual issue I'm having is that once I deploy the app to my domain, the login process behaves differently compared to when I run on localhost - which is making pre-deployment testing difficult.
That is, http://localhost:3000 and http://localhost:3000/#/login both allow me to login successfully. But when I deploy to my domain, http://www.example.com allows me to login, while http://www.example.com/#/login does not.
Any idea why this would be? And can I configure a React-Admin app to not re-route to http://www.example.com/#/login?
"If the promise is rejected, react-admin redirects by default to the /login page.
You can override where to redirect the user in checkAuth(), by rejecting an object with a redirectTo property:"
React-admuin 2.9:
https://marmelab.com/react-admin/doc/2.9/Authentication.html#checking-credentials-during-navigation
// in src/authProvider.js (React-admin 2.9)
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin';
export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
// ...
}
if (type === AUTH_ERROR) {
// ...
}
if (type === AUTH_CHECK) {
return localStorage.getItem('token') ? Promise.resolve() : Promise.reject({ redirectTo: '/no-access' });
}
return Promise.reject('Unknown method');
};
React-admin 4.3: https://marmelab.com/react-admin/AuthProviderWriting.html
// in src/authProvider.js
export default {
login: ({ username, password }) => { /* ... */ },
checkError: (error) => { /* ... */ },
checkAuth: () => localStorage.getItem('auth')
? Promise.resolve()
: Promise.reject({ redirectTo: '/no-access' }),
// ...
}

Protected Route by checking JWT saved in user's cookie

I just finished implementing Google social authentication in my NextJS + DjangoRest project following this blog post. I am trying to figure out how to make protected routes that will redirect users if they’re not logged in.
This is how I did it so far:
when user logs in, it saves the jwt_token in the cookie as httponly
uses axios with “withCredentials: true” to access the API endpoint which returns current user data(i.e. email)
saves the user data as a useContext(). When protected page loads, check if UserContext is empty or not and redirects to login page if it is empty.
The obvious problem is the UserContext is reset whenever user refreshes the page, even when the JWT token is still present in the cookies. And I have a feeling this isn’t the right way to implement this.
So how would I implement a similar feature in a non-hacky way? I cannot read jwt-token from cookies in the frontend as it is httponly. Is there a safe way to read user’s JWT token from cookies to test for authentication?
So if I am reading your question right then you can use getServerSide props on your page to detect if the user is authenticated with your api.
function Page({ isAuth }) {
return (
<>
<div>My secure page</div>
//if you return data from your token check api then you could do something like this
<div>Welcome back {isAuth.name}</div>
</>
)
}
export default Page
export async function getServerSideProps(context) {
const isAuth = await tokenChecker(context.cookies.jwt) // In your token checker function you can just return data or false.
if (!isAuth) { //if tokenChecker returns false then redirect the user to where you want them to go
return {
redirect: {
destination: `/login`,
}
};
}
//else return the page
return {
props: {
isAuth,
},
}
}
If this is not what you mean let me know and i can edit my answer.
I modified #Matt's answer slightly and typescript-friendly to solve my problem. It simply checks the user's cookies if they have a jwt_token value inside.
import cookies from 'cookies'
export const getServerSideProps = async ({
req,
}: {
req: { headers: { cookie: any } };
}) => {
function parseCookies(req: { headers: { cookie: any } }) {
var parsedCookie = cookie.parse(
req ? req.headers.cookie || '' : document.cookie
);
return parsedCookie.jwt_token;
}
const isAuth = parseCookies(req);
if (typeof isAuth === undefined) {
return {
redirect: {
destination: `/auth/sign_in`,
},
};
}
return {
props: {
isAuth,
},
};
};

React Next js app redirect to login is premature

After a lot of searching for several hours, I have the following code to redirect from a user profile page if not logged in.
NOTE: Simply showing a not authorized page is easy but its the redirect thats messing things up.
The code does the job of redirecting when user is not logged in.
const Dashboard = () => {
const [user, { mutate }] = useCurrentUser();
const router = useRouter();
useEffect(() => {
// redirect to login if user is not authenticated
if (!user) router.push('/login');
}, [user]);
...
The problem is when a user is logged in and directly goes to /user/dashboard route, for a split second, user is undefined may be so it redirects to login. When it gets to login, it finds that user is authenticated so redirects to home page because I am redirecting a logged in user to home page.
How to prevent that split second of "not a user" status when page is first loading?
I tried -
getInitialProps
getServerSideProps - Cant use router because next router can only be used on client side
componentDidMount - UseEffectI tried above is the equivalent correct?
Edit: Based on answer below, I tried this but still directly takes user to login first. I am using react cookies and I do see loggedIn cookie as true when user is logged in and its not set when user is not logged in.
Dashboard.getInitialProps = ({ req, res }) => {
console.log(req.headers.cookie)
var get_cookies = function(request) {
var cookies = {};
request.headers && request.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
return cookies;
};
//console.log(get_cookies(req)['loggedIn']);
if (get_cookies(req)['loggedIn'] == true) {
console.log("entered logged in")
return {loggedIn: true};
}
else {
console.log("entered not logged in")// can see this on server console log
// User is not logged in, redirect.
if (res) {
// We're on the server.
res.writeHead(301, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
}
}
You can implement redirect when not authenticated in getServerSideProps
Below example is based on JWT Authentication with cookies.
export const getServerSideProps = async (ctx) => {
const cookie = ctx.req.headers.cookie;
const config = {
headers: {
cookie: cookie ?? null
}
}
let res;
try {
// your isAuthenticated check
const res = await axios('url', config);
return { props: { user: res.data } };
} catch (err) {
console.error(err);
ctx.res.writeHead(302, {
Location: 'redirectUrl'
})
ctx.res.end();
return;
return { props: { user: null } };
}
}
You should be able to use getInitialProps to redirect. You just need to check whether you're on the server or the client and use the proper redirect method. You can't use hooks in getInitialProps so your useCurrentUser approach won't work and you'll need some other way to check whether the user is authed. I don't know anything about the structure of your application, but it's probably just some kind of request to wherever you're storing the session.
import Router from 'next/router';
const Dashboard = (props) => {
// props.user is guaranteed to be available here...
};
Dashboard.getInitialProps = async ({ res }) => {
// Check authentication.
// Await the response so that the redirect doesn't happen prematurely.
const user = await ...
// User is logged in, return the data you need for the page.
if (user) {
return { user };
}
// User is not logged in, redirect.
if (res) {
// We're on the server.
// Make the redirect temporary so it doesn't get cached.
res.writeHead(307, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
};
After many hours of struggle, there was one number that was breaking this.
Instead of
res.writeHead(301, { Location: '/login' });
I used
res.writeHead(307, { Location: '/login' });
and it worked.
301 is a permanent redirect so if we use that, when the user logs in, the browser still holds the redirect cache.
From next js docs
Next.js allows you to specify whether the redirect is permanent or not with the permanent field. This is required unless you need to specify the statusCode manually
When permanent is set to true we use a status code of 308 and also set a Refresh header for backwards compatibility with IE11.
When permanent is set to false we use a status code of 307 which is not cached by browsers and signifies the redirect is temporary.
Next.js permits the following status codes:
-301 Moved `Permanently`
-302 Found
-303 See Other
-307 `Temporary` Redirect
-308 Permanent Redirect

How to access react-router v3 prop in function?

I'm using refresh-fetch for authentication token refreshing. If the app receives not 200 http status code response I need to handle that by redirecting the user to logout page. How could I achieve this using react-router v3.
browserHistory.push('/logout');
I think this is not an option because I'm using basename.
const refreshToken = () => {
return fetchJSONWithToken(`${API_ROOT}user/login/refresh`, {
method: 'POST',
body: JSON.stringify({ refresh_token: retrieveToken() })
})
.then(({body}) => {
saveToken(body.access_token, body.refresh_token);
return body;
})
.catch(error => {
//TODO: redirect user to /logout
throw error;
});
};
Or maybe there is a better way of doing this?
You need to store your browserHistory instance and reuse it.
Example:
import { createHistory, useBasename } from 'history'
// Run our app under the /base URL.
const yourCustomHistoryWithBasename = useBasename(createHistory)({
basename: '/base'
})
// Re-use the same history, which includes the basename
yourCustomHistoryWithBasename.push('/logout') // push /base/logout
yourCustomHistoryWithBasename.replace('/logout') // replace current history entry with /base/logout
Source for this example

React Relay Modern redirecting to another page when receiving 401 error on network environment

I´m using JWT authentication inside my ReactJS RelayJS network environment. All the token retrieval and processing in server and client are fine. I´m using react router v4 for routing.
My problem is when I receive a Unauthorized message from server (status code 401). This happens if the user points to an application page after the token has expired, ie. What I need to do is to redirect to login page. This is the code I wish I could have:
import { Environment, Network, RecordSource, Store } from 'relay-runtime';
const SERVER = 'http://localhost:3000/graphql';
const source = new RecordSource();
const store = new Store(source);
function fetchQuery(operation, variables, cacheConfig, uploadables) {
const token = localStorage.getItem('jwtToken');
return fetch(SERVER, {
method: 'POST',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json',
'Content-type': 'application/json'
},
body: JSON.stringify({
query: operation.text, // GraphQL text from input
variables
})
})
.then(response => {
// If not authorized, then move to default route
if (response.status === 401)
this.props.history.push('/login') <<=== THIS IS NOT POSSIBLE AS THERE IS NO this.history.push CONTEXT AT THIS POINT
else return response.json();
})
.catch(error => {
throw new Error(
'(environment): Error while fetching server data. Error: ' + error
);
});
}
const network = Network.create(fetchQuery);
const handlerProvider = null;
const environment = new Environment({
handlerProvider, // Can omit.
network,
store
});
export default environment;
Naturally calling this.props.history.push is not possible as the network environment is not a ReactJS component and therefore has no properties associated.
I´ve tried to throw an error at this point, like:
if (response.status === 401)
throw new Error('Unauthorized');
but I saw the error on the browser console, and this cannot be treated properly in the code.
All I wanna do is to redirect to login page in case of 401 error received, but I can´t find a proper way of doing it.
I am not using relay but a render prop. I experienced kind of the same issue. I was able to solve it using the window object.
if (response.statusText === "Unauthorized") {
window.location = `${window.location.protocol}//${window.location.host}/login`;
} else {
return response.json();
}
You can go with useEnvironment custom hook.
export const useEnvironment = () => {
const history = useHistory(); // <--- Any hook using context works here
const fetchQuery = (operation, variables) => {
return fetch(".../graphql", {...})
.then(response => {
//...
// history.push('/login');
//...
})
.catch(...);
};
return new Environment({
network: Network.create(fetchQuery),
store: new Store(new RecordSource())
});
};
// ... later in the code
const environment = useEnvironment();
Or you can create HOC or render-prop component if you are using class-components.
btw: this way you can also avoid usage of the localStorage which is slowing down performance.

Resources