Can't get cookie during _app initialization with getInitialProps - javascript

I'm using next-cookies to store auth credentials from user session, however, I can't get them during app initialization -let's say, the user refreshes the page or comes back later-, however, after app has been initialized -or user has loged in-, I get them navigating in the app.
This is important, because I want to fetch some initial data to be available in to the redux store from the beginning.
// pages/_app.js
import { useStore } from '../store/store';
import nextCookie from 'next-cookies';
function MyApp({ Component, pageProps }) {
const Layout = Component.layout || MainLayout;
const store = useStore(pageProps.initialReduxState); // custom useStore method to init store
const pageTitle = Component.title || pageProps.title || 'Título de la página';
return (
<Provider store={store}>
<Layout pageTitle={pageTitle}>
<Component {...pageProps} />
</Layout>
</Provider>
);
}
export default MyApp;
MyApp.getInitialProps = async (ctx) => {
const { token, user } = nextCookie(ctx);
/* TO DO
the idea is to get cookie from the server side
and pass it to the client side, if the cookie is
active, initial data will be triggered from the
initializers
*/
return { pageProps: { token, user } };
};
Check it out:
Is it a better way or native option to get the cookie without having to a different cookie dependency?
Btw, I already have a middleware that protects routes from non authenticated user which works fine:
// pages/_middleware.js
export function middleware(req) {
const activeSession = req.headers.get('cookie');
const url = req.nextUrl.clone();
if (activeSession) {
if (req.nextUrl.pathname === '/login') {
url.pathname = '/';
return NextResponse.redirect(url);
}
return NextResponse.next();
}
url.pathname = '/login';
return NextResponse.rewrite(url);
}

Related

Next js hydration error when implementing private route

I'm using nextjs 12.2.5 have created a higher order component in order to limit access to users that are logged in for certain pages but I keep getting hydration error.
Here is my code:
WithAuth.jsx
import { useRouter } from "next/router";
const withAuth = (WrappedComponent) => {
return (props) => {
if (typeof window !== "undefined") {
const Router = useRouter();
const token = localStorage.getItem("token");
if (!token) {
Router.replace("/login");
return null;
}
return <WrappedComponent token={token} {...props} />;
}
// If we are on server, return null
return null;
};
};
export default withAuth;
Home.jsx
const Home = () => {
return (
<Layout>
Home
</Layout>
);
};
export default withAuth(Home);
I have read the documentation on hydration but I still don't understand how I can implement my private route with Higher Order Component and avoiding the hydration error.
How am I supposed to implement private route with next js?

Prevent Router from Loading Page Briefly before Redirect

I have a session context for my NextJS application where anyone accessing /app/ directory pages have to go through an authorization check prior to allowing the user to access the page.
While my logic works in redirecting users without proper authentication, it is a bit glitchy because when someone navigate to the URL, /app/profile/ the page briefly loads before being redirected by Router.
I am wondering what is the best way to have this check happen prior to router loading the unauthorized page and redirecting them to the /login/ page.
Here are the steps in the authorization check:
Check is the user object has a property, authorized
Query the server for a session token
if the object from the server request comes back with authorized = false, then redirect user to /login/
Here is the code:
import React, { createContext, useContext, useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import axios from 'axios'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const router = useRouter()
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''})
useEffect(()=> {
// Check for populated user state if pages are accessed with the path /app/
if (routerPath.includes("/app/")){
if (user){
if(user.authenticated === undefined){
// Check if user session exists
axios.get('/api/auth/session/')
.then(res => {
const data = res.data;
// Update user state depending on the data returned
setUser(data)
// If user session does not exist, redirect to /login/
if (data.authenticated === false){
router.push('/login/')
}
})
.catch(err => {
console.log(err)
});
}
}
}
}, [])
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
Since user.authenticated isn't defined in the initial user state you can conditionally render null or some loading indicator while user.authenticated is undefined. Once user.authenticated is defined the code should either redirect to "/login" or render the SessionContext.Provider component.
Example:
const AppSession = ({ children }) => {
const router = useRouter();
const routerPath = router.pathname;
const [user, setUser] = useState({ user_id: '', user_email: '', user_avatar: ''});
...
if (user.authenticated === undefined) {
return null; // or loading indicator/spinner/etc
}
return (
<SessionContext.Provider value={{ user, setUser }}>
{children}
</SessionContext.Provider>
);
};
Check out getServerSideProps, redirects in getServerSideProps and this article.
In your client-side, if you export the NextJS function definition named getServerSideProps from a page, NextJS pre-renders the page on each request using the data returned by getServerSideProps.
In other words, you can use getServerSideProps to retrieve and check the user while pre-rendering the page and then choose to redirect instead of render if your condition is not met.
Here is an example.
function Page({ data }) {
// Render data...
}
export async function getServerSideProps(context) {
const { req, res } = context;
try {
// get your user
if (user.authenticated === undefined) {
return {
redirect: {
permanent: false,
destination: `/`,
},
};
}
return {
props: {
// any static props you want to deliver to the component
},
};
} catch (e) {
console.error("uh oh");
return;
}
}
Good luck!

Keep state in react inside context after logged in?

I'm coding a web react app with sign in. In my server side I'm using express, jwt and sending a httpOnly cookie with the token when succesfully log in. When user logs in, I'm trying to keep state in the client (e.x, loggedIn = true) inside of a context, but every time that context is rendered it comes back to default state (undefined). How could i keep that state in memory?
My user route, that works as expected (backend):
users.post('/login',async (req, res) => {
try {
const {userName,userPass} = req.body
const u = await models.User.findOne({
userid: userName
})
if (!u) res.status(404).end()
if (bcrypt.compare(userPass,u.password)) {
// JWT TOKEN
const t = c_auth(u._id)
res.status(200).cookie("session",t,{
httpOnly:true
}).end()
} else {
res.status(404).end()
}
} catch (e) {
console.log({'ERROR':e})
res.status(500).end()
}
})
My user provider that returns true when request is ok (client):
get: async (user,pass) => {
try {
const req = await axios.post('/users/login',{
userName: user,
userPass: pass
})
if (req.status === 200) {
return true
} else {
return false
}
} catch (e) {
console.log({'ERROR':e})
return false
}
}
Login submit function (client):
import {useAuth} from '../../../../contexts/AuthContext.js'
const {setLoggedIn} = useAuth()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
// ^---> Trying to set loggedIn state to true in context
window.location.replace('/')
} else {
alert(`Incorrect.`)
}
}
Auth context:
const AuthContext = createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider ({children}) {
const [loggedIn,setLoggedIn] = useState()
console.log(loggedIn)
// ^---> Getting true after login,
// undefined (default useState) after re-render
const value = {
loggedIn,
setLoggedIn
}
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}
App.js:
import {AuthProvider} from './contexts/AuthContext'
function App() {
return (
<AuthProvider>
<div className="App">
<Navigation/>
<main>
<Router>
<Routes>
...Routes
</Routes>
</Router>
</main>
</div>
</AuthProvider>
)
}
I suppose that react memo could be the solution, but I don't understand quite well how it works. Also, is it correct not to use the setLoggedIn in the AuthContext itself? I tried to call the login or sign up function (second piece of code) from the AuthContext, but can't set state since its unmounted. Would need to do that inside a useEffect and that's not what I'm looking for since I wouldn`t be able to export that function. All help is appreciated.
EDIT: fixed
The problem solved after changing window.location.replace to the useNavigate hook from react-router-dom, causing a refresh:
import {useAuth} from '../../../../contexts/AuthContext.js'
import {useNavigate} from 'react-router-dom'
const {setLoggedIn} = useAuth()
const navigate = useNavigate()
const handleLogin = async (e) => {
if (await users.get(data.userName,data.userPass)) {
setLoggedIn(true)
navigate('/')
} else {
alert(`Incorrect.`)
}
}
Also in my navbar I was using <a href> tags instead of <Link to> from 'react-router-dom'. That fixes the problem when I go to a different page from the navbar, so it doesn't 'refresh'.

Can I use getInitialProps in _app.js and in pages?

I'm working on my first serious NextJS app. I have it set up to pull in JSON data for the left nav, rather than hardcoding them in the app somewhere. This way I don't have to rebuild every time there's a minor change to the site's navigation.
Since the navigation needs to be available on every page, I added getInitialProps to the _app.js file, which grabs the left nav and passes it to the left nav component. But now as I'm moving on to build the homepage, I see that the getInitialProps there does not run. It seems that the getInitialProps in _app.js takes precendence.
Is there a way to have both? Or some other workaround that accomplishes the goal (or just a better way to do this in general)?
Note that I'm using getInitialProps for two reasons:
getStaticProps is out because I don't plan to build the entire site at build time
getServerSideProps is usually out because I don't like that it ends up doing two http requests: first a request goes to the NextJS server, then the server sends a request to my API (which happens to live on a different server). If I'm just getting basic stuff like the navigation, there's no need for getServerSideProps to run on the NextJS server, I'd rather skip the middle man
Here's some some simplified code:
_app.js:
import { Provider } from "react-redux";
import axios from "axios";
import store from "../store";
import Header from "../components/Header";
import LeftNav from "../components/LeftNav";
function MyApp(props) {
const { Component, pageProps } = props;
return (
<Provider store={store}>
<Header />
<LeftNav leftnav={props.leftnav} />
<Component { ...pageProps } />
</Provider>
)
}
MyApp.getInitialProps = async (context) => {
let config = await import("../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getLeftNav`);
if (response) {
return {
leftnav: response.data.leftNav
};
} else {
return {
leftnav: null
};
}
};
export default MyApp;
Home.js:
import axios from "axios";
const Home = (props) => {
console.log("Home props", props);
return (
<div>home</div>
);
};
Home.getInitialProps = async(context) => {
// this only runs if the getInitialProps in _app.js is removed :(
let config = await import("../config/config");
let response = await axios.get( `${config.default.apiEndpoint}&cAction=getHome` );
if ( response ) {
return {
home: response.data.home
};
} else {
return {
home: null
}
}
};
export default Home;
You have to call App.getInitialProps(context) in your _app to call the current page's getInitialProps. You can then merge the page's props with the remaining props from _app.
import App from 'next/app'
// Remaining code...
MyApp.getInitialProps = async (context) => {
const pageProps = await App.getInitialProps(context); // Retrieves page's `getInitialProps`
let config = await import("../config/config");
let response = await axios.get(`${config.default.apiEndpoint}&cAction=getLeftNav`);
return {
...pageProps,
leftnav: response?.data?.leftNav ?? null
};
};
From the custom _app documentation:
When you add getInitialProps in your custom app, you must import App from "next/app", call App.getInitialProps(appContext) inside
getInitialProps and merge the returned object into the return value.

How to redirect to any url after login in redirect?

I want to redirect my user to any url that he types in after logging in
for example; my user types in the browser, http://localhost:3000/login/tickets,
If he has not logged in I would need the program to load the login page, and after login, the program redirects to this page, I can do it with a single page but I wanted it to be dynamic, something like this.
isAuthenticated()
? (
<Component {...props} />
) : <Redirect to=`/login?next=${this.props.location.search}` />
)}
soon this redirect would load the login page with the tag next
My solution was to do essentially what you describe. I made a HOC to wrap my route's component in if it requires that the user be logged in:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter, Redirect } from 'react-router-dom';
/**
* Higher-order component (HOC) to wrap restricted pages
*/
export default function LoggedInOnly(BaseComponent) {
class Restricted extends Component {
state = {};
static getDerivedStateFromProps(nextProps) {
const { history, location } = nextProps;
if (!nextProps.isLoggedIn) {
history.replace({ pathname: '/signin', search: `dest=${encodeURIComponent(location.pathname)}` });
}
return null;
}
render() {
const { location, staticContext } = this.props;
if (this.props.isLoggedIn) return <BaseComponent {...this.props} />;
const destinationURL = `/signin?dest=${encodeURIComponent(location.pathname)}`;
if (staticContext) staticContext.url = destinationURL;
return <Redirect to={destinationURL} />;
}
}
const mapStateToProps = state => ({
isLoggedIn: !!state.globalUserState.loggedInUserEmail,
});
return withRouter(connect(mapStateToProps)(Restricted));
}
I also set the url on the static context in my case so I can handle redirects appropriately in server side rendering. If you're not doing the same you can ignore that part.
For using it, though, I redirect after my SSR render function, like so:
if (context.url) {
console.log(`Redirecting to URL ${context.url}`);
return res.redirect(context.url);
}
A route using this would look like:
<Route path="/preferences" component={LoggedInOnly(SettingsView)} />
On my login page I grab the url parameters to see if there's a destination. If there is, I redirect there on login success.
I do this using query-string and the search component of the location:
const { destination } = queryString.parse(props.location.search);
The above assumes you're using withRouter to get the location info in the props.
On authentication success in the client I simply redirect to destination if it exists:
window.location.href = this.props.destination;
You could also use history.push or similar to accomplish the above.
In my case, as you can see, I'm using redux to track the logged in user state.
you need to update state by taking a variable and apply check wether it has been changed or not, if yes then redirect to desired page ,if not revert back. Since you have not posted your whole code .You can refer to this video for wider and clear perspective :
https://www.youtube.com/watch?v=zSt5G3s3OJI
You can accomplish what you need by doing some thing like this:
if(isAuthenticated)
this.props.history.push('/login', {lastPage: this.props.location.match})
and after user gets logged in you cant redirect him to passed param lastPage!
Another way is to store lastPage in redux and access it after user get logged in.
I had the same problem, i made a HOC to solve it.
import React from "react";
import { connect } from "react-redux";
import Login from "../../Auth/Login";
import { withRouter } from "react-router-dom";
import qs from "querystring";
const signInPath = "/signin";
const signUpPath = "/signup";
const forgotPassPath = "/forgot";
const resetPassPath = "/resetpassword";
const returlUrlPath = "returnUrl";
const allowedPaths = pathname =>
pathname === signInPath ||
pathname === signUpPath ||
pathname === forgotPassPath ||
pathname === resetPassPath;
const homePath = "/";
export default Component => {
class AuthComponent extends React.Component {
componentDidMount() {
this.checkAuthentication();
}
componentDidUpdate(nextProps) {
if (
nextProps.location.pathname !== this.props.location.pathname ||
this.props.loggedIn !== nextProps.loggedIn
) {
this.checkAuthentication();
}
}
checkAuthentication() {
const {
loggedIn,
history,
location: { pathname, search }
} = this.props;
if (!loggedIn) {
if (!allowedPaths(pathname)) {
const returlUrl =
pathname.length > 1
? `${returlUrlPath}=${pathname.replace("/", "")}`
: undefined;
history.replace({ pathname: signInPath, search: returlUrl });
}
} else if (search) {
const parsedSearch = qs.parse(search.replace("?", ""));
if (parsedSearch.returnUrl) {
history.replace({ pathname: parsedSearch.returnUrl });
} else {
history.replace({ pathname: homePath });
}
} else if (
history.location.pathname === signInPath ||
history.location.pathname === signUpPath
) {
history.replace({ pathname: homePath });
}
}
shouldRedirectToLogin() {
const {
location: { pathname }
} = this.props;
return (
!this.props.loggedIn &&
pathname !== signUpPath &&
pathname !== forgotPassPath &&
pathname !== resetPassPath
);
}
render() {
return this.shouldRedirectToLogin() ? (
<Login></Login>
) : (
<Component {...this.props}></Component>
);
}
}
return withRouter(
connect(({ user: { loggedIn } }) => {
return {
loggedIn
};
})(AuthComponent)
);
};
Thanks for all, after a lot of research i get only:
const params = (this.props.children.props.computedMatch.url);
return <Redirect to={`/login/?next=${params}`} />;

Categories