How to add Nextjs cookie based secure authentication? - javascript

I have a small issue. I'm very very very new to Nextjs and I'm trying to learn by making a app. I have managed to make a Login system using next and I have few issues when securing routes. I have successfully added a cookie after successful login. Now I want to validate the cookie whenever user go to a protected route. I have followed below steps using this tutorial.
Made a Higher order component and checked the cookie validation using it.
Wrap the protected component using it.
Below is my HOD.
import { useRouter } from "next/router";
import Cookies from 'js-cookie';
const withAuth = (WrappedComponent) => {
return (props) => {
if (typeof window !== "undefined") {
const Router = useRouter();
const accessToken = Cookies.get('token');
if (!accessToken) {
Router.replace("/");
return null;
}
return <WrappedComponent {...props} />;
}
return null;
};
};
export default withAuth;
And then I have wrapped my component using above HOD.
import React, { Component } from 'react';
import withAuth from '../utils/withAuth';
class Home extends Component {
render() {
return (
<div>
HOME
</div>
);
}
}
export default withAuth(Home);
ISSUE #1
Above HOD is showing a console warning saying below.
Warning: Expected server HTML to contain a matching in .
div
Is their anyway I can fix this issue? As per some github answer I have found this can be solved using useEffect. SOURCE
Can anyone help me with this?
ISSUE #2
In this way, I have to wrap each and every protected component with my HOD. Is this the correct way of doing this or is there any other way to do this better than this?
Thank you so much or your support.

After spending some time. I was able to fix the issue by using below code. Now I just want to know the answer for 2nd issue mentioned above.
ISSUE #2 In this way, I have to wrap each and every protected component with my HOD. Is this the correct way of doing this or is there any other way to do this better than this?
Thank you so much or your support.
Code I use to fix the issue
import Router from 'next/router'
import Cookies from 'js-cookie';
import React, { useState, useEffect } from 'react';
const withAuth = (WrappedComponent) => {
return (props) => {
const [isLoggedIn, setLoginStatus] = useState(false);
useEffect(() => {
if (typeof window !== "undefined") {
const accessToken = Cookies.get('token');
if (accessToken) {
setLoginStatus(true)
}
else {
Router.push("/")
}
}
}, []);
if (isLoggedIn) {
return <WrappedComponent {...props} />;
} else {
return null;
}
}
};
export default withAuth;

Related

React-router-dom (v6) go back only within application

Is there a built-in way in react-router-dom v6, to go back to the previous page, BUT in case the previous page is out of the context of the application, to route to the root and to thus not out of the application.
Example: I surf to a www.thing.com/thingy from www.google.com, this page (www.thing.com/thingy) has a go back button on it => when I click on the go back button => I am redirected to www.google.com instead of the wanted behaviour a redirect to www.thing.com.
Mockup of an example page.
I have tried several implementations and searched through the documentation but couldn't find a built-in way to resolve this. As far as I can see there isn't a way. I can however make something custom to resolve my issue if its not.
import { useNavigate } from 'react-router-dom';
function YourApp() {
const navigate = useNavigate();
return (
<>
<button onClick={() => navigate(-1)}>go back</button>
</>
);
}
I solved it by keeping track of the history.
If a user had not yet been on the page, I redirect them to the homepage.
Else redirect them to the previous page.
import {
useEffect
} from 'react';
import {
createContext,
useMemo,
useState
} from 'react';
import {
useLocation
} from 'react-router-dom';
export const LocationHistoryContext = createContext({});
const LocationHistoryProvider = ({
children
}) => {
const [locationHistory, setLocationHistory] = useState(
new Set(),
);
const location = useLocation();
useEffect(() => {
// if pathname has changed, add it to the history
let path = location.pathname.match(/^\/([^/])*/)[0];
setLocationHistory((prev) => new Set([path, ...prev]));
}, [location.pathname, location]);
const context = useMemo(() => {
return {
/* if the user has visited more than one page */
hasHistory: locationHistory.size > 1,
};
}, [locationHistory]);
return ( <
LocationHistoryContext.Provider value = {context}>
{
children
}
</LocationHistoryContext.Provider>
);
};
export default LocationHistoryProvider;

How to get navbar to not display until a user logs in? [Done]

I am struggling with getting my NavBar to not display/show until a user has logged in (received a token). I know you can set it up using a ternary but I am not able to get one to function. If another option besides a ternary works I am okay with that.
import Auth from './Auth/Auth';
import Sitebar from './Home/Navbar';
import ReviewIndex from './Reviews/ReviewIndex';
import Navigation from './Home/Navigation'
import {
BrowserRouter as Router
} from 'react-router-dom';
function App() {
const [sessionToken, setSessionToken] = useState('');
useEffect(() => {
if (localStorage.getItem('token')){
setSessionToken(localStorage.getItem('token'));
}
}, [])
const updateToken = (newToken) => {
localStorage.setItem('token', newToken);
setSessionToken(newToken);
console.log(sessionToken)
}
const clearToken = () => {
localStorage.clear();
setSessionToken('');
window.location.href="/"
}
const protectedViews = () => {
return (sessionToken === localStorage.getItem('token') ? <ReviewIndex token={sessionToken}/>: <Auth updateToken={updateToken}/>)
}
return (
<div className="App">
<Router>
<Sitebar sessionToken={sessionToken} clickLogout={clearToken}/>
<Navigation sessionToken={sessionToken} />
{protectedViews()}
</Router>
</div>
);
}
export default App;
the way i would approach this is i would use the turnary operator to conditionally render it. so here you are setting the session token as soon as the component renders and you are only doing it once.
so heres what you could write to conditionally render this.
{sessionToken ? "Put Jsx here that you want to render if they are authenticated" : "Put Jsx here to render if they are not authenticated"}
// css file
.navigation {
visibility: 'hidden';
// or
display: 'none';
}
// component
<Navigation className={sessionToken ? 'navigation': ''} />
It really depends what you want to do. lizardcoder's solution won't mount the component at all until the user is logged in. My solution will hide it, but it will mount. If you don't want to initialize anything in Navigation until the user is logged in, lizardcoder's is the right way to go. If you just want to hide it, css is a good way to go.

Query values lost on page refresh in Next js? [Example given]

I am making a simple Next Js application which has only two pages..
index.tsx:
import React from "react";
import Link from "next/link";
export default function Index() {
return (
<div>
<Link
href={{
pathname: "/about",
query: { candidateId: 8432 }
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
As per the above code, on click Go to the about page it goes to about page and using query I also receive the passed query values in about page.
about.tsx
import React from "react";
import Router, { withRouter } from "next/router";
function About({ router: { query } }: any) {
return (
<div>
Candidate Id: <b> {query.candidateId} </b>
</div>
);
}
export default withRouter(About);
This displays the value but on page refresh while we are in /about page, the candidateId received gets disappeared.
Requirement: Kindly help me to retain the query value passed down from one page to another page even on page refresh.
Note: As per my requirement I should not display the canidateId on url while navigating and hence I am using as approach.. I know I can achieve it if I remove as but I cannot remove that here in index page while navigating.. Reason is this will lead to displaying candidateId in the url which is not intended..
Tried this solution: https://stackoverflow.com/a/62974489/7785337 but this gives empty query object on refresh of page.
Stuck for very long time with this please kindly help me.
If you do not want to use the query parameter you may need to create a "store" that saves your variable that persist throughout your pages.
Sample code as follows.
//candidatestore.js
export const CandidateStoreContext = createContext()
export const useCandidateStore = () => {
const context = useContext(CandidateStoreContext)
if (!context) {
throw new Error(`useStore must be used within a CandidateStoreContext`)
}
return context
}
export const CandidateStoreProvider = ({ children }) => {
const [candidateId, setCandidateId] = useState(null);
return (
<CandidateStoreContext.Provider value={{ candidateId, setCandidateId }}>
{children}
</CandidateStoreContext.Provider >
)
}
Then you need to wrap the Provider around your app like
<CandidateStoreProvider><App /></CandidateStoreProvider>
This way you can use anywhere as follows both in your index page and your about page.
const { candidateId, setCandidateId } = useCandidateStore()
UseContext
In your codes, it should probably look something like that.
import React from "react";
import Link from "next/link";
import { useCandidateStore } from './candidatestore'
export default function Index() {
const { candidateId, setCandidateId } = useCandidateStore()
useEffect(() => {
setCandidateId(thecandidateId)
})
return (
<div>
<Link
href={{
pathname: "/about",
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
function About({ router: { query } }: any) {
const { candidateId, setCandidateId } = useCandidateStore()
return (
<div>
Candidate Id: <b> {candidateId} </b>
</div>
);
}
Update to Next.JS 10. It comes with Automatic Resolving of href which fixes your problem.
Try to delete the as="about" and then navigate again to the "about" page, the issue should be gone.
Codesandbox
My best bet would be to store the candidateId in an encrypted session on the client side. You could read/verify cookies in getServerSideProps() and pass their contents to the page component. If this sounds feasible, I'd recommend checking out the next-iron-session.
Another approach would be to check if candidateId exists in the query object in getServerSideProps(). If it does then pass it straight to the page component. If not, either get it elsewhere, redirect, or pass some default value. Append the following starter code to your about.tsx:
/* ... */
export function getServerSideProps({ query }: any) {
// if query object was received, return it as a router prop:
if (query.candidateId) {
return { props: { router: { query } } };
}
// obtain candidateId elsewhere, redirect or fallback to some default value:
/* ... */
return { props: { router: { query: { candidateId: 8432 } } } };
}
index.tsx file
Keep the code same as it is.
import React from "react";
import Link from "next/link";
export default function Index() {
return (
<div>
<Link
href={{
pathname: "/about",
query: { candidateId: 8432 }
}}
as="about"
>
Go to the about page
</Link>
</div>
);
}
AboutUs.tsx
Code starts from here
Adding router as a dependency in the useEffect the issue should get solved.
import Router, { useRouter } from "next/router";
import React, { useState, useEffect } from 'react';
function About({ router: { query } }: any) {
const route = userRouter();
const [candidateId, setCandidateid] = useState();
useEffect(() => {
const {candidateId} = router.query
if(candidateId) {
setCandidateid(candidateid)
}},[router]) //Here goes the dependency
return (
<div>
Candidate Id: <b> {candidateId} </b>
</div>
);
}
export default (About);

Next.js - understanding getInitialProps

I have an app that uses next.js along with Apollo/ Graphql and i'm trying to fully understand how the getInitialProps lifecycle hook works.
The lifecycle getInitialProps in my understanding is used to set some initial props that will render server side for when the app first loads which can be used prefetch data from a database in order to help SEO or simply to enhance page load time.
My question is this:
Every time I have a query component that fetches some data in my
components across my app, do I have to use getInitialProps to be
sure that data will be rendered server side?
My understanding is also that getInitialProps will only work in the page index components (as well as in _app.js), this would mean that any component lower down in the component tree would not have access to this lifecycle and would need to get some initial props from way up at the page level and then have them passed down the component tree. (would be great if someone could confirm this assumption)
Here is my code:
_app.js (in /pages folder)
import App, { Container } from 'next/app';
import { ApolloProvider } from 'react-apollo';
class AppComponent extends App {
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
render() {
const { Component, apollo, pageProps } = this.props;
return (
<Container>
<ApolloProvider client={apollo}>
<Component client={client} {...pageProps} />
</ApolloProvider>
</Container>
);
}
}
export default AppComponent;
Index.js (in /pages/users folder)
import React, { PureComponent } from 'react';
import { Query } from 'react-apollo';
import gql from 'graphql-tag';
const USERS_QUERY = gql`
query USERS_QUERY {
users {
id
firstName
}
}
`;
class Index extends PureComponent {
render() {
return (
<Query query={USERS_QUERY}>
{({data}) => {
return data.map(user => <div>{user.firstName}</div>);
}}
</Query>
);
}
}
export default Index;
The answer is NO
If you use Apollo with Next JS you will not have to use getInitialProps on each page to get some initial data rendered server side. The following configuration for getInitialProps is enough for all the components to render out with their respective queries if they have <Query> components in them
static async getInitialProps({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
// this exposes the query to the user
pageProps.query = ctx.query;
return { pageProps };
}
My issue and why I wasnt seeing any server side rendering is that Heroku or Now wouldnt perform SSR with a public URL ie my-app.heroku.com. To resolve this I purchased and applied a custom URL in Heroku and it worked. Along with a custom URL I had the following configuration in my Apollo config
const request = (operation) => {
operation.setContext({
fetchOptions: {
credentials: 'include'
},
headers: { cookie: headers.cookie }
});
};
This completely resolved it and now I have SSR without the pain of having to manually set getInitialProps on each page
Hope this helps someone

React-navigation get current navigation state in screens

I need to be able to get the current navigation state from my registered screen components. I expected to find a routes object inside the navigation.state object but alas its not there. I have managed to get this working by setting up my root component in the following way, however this seems convoluted and i cant help but think there must be a cleaner way to achieve this.
App.js
import React, {Component} from 'react'
import {Tabs} from './components/Routes'
import {NavigationActions} from 'react-navigation'
export default class App extends React.Component {
state = {
navState: null
}
componentDidMount(){
const initState = Tabs.router.getStateForAction(NavigationActions.init())
this.getState(null, initState);
}
getState = (prevState, newState) => {
let activeIndex = newState.index
let navState = newState.routes[activeIndex]
this.setState({navState})
}
render() {
return (
<Tabs
onNavigationStateChange={this.getState}
screenProps={{navState: this.state.navState}}/>
)
}
}
<NavigationName
onNavigationStateChange={(prevState, newState) => {
this._getCurrentRouteName(newState)
}}
/>
_getCurrentRouteName(navState) {
if (navState.hasOwnProperty('index')) {
this._getCurrentRouteName(navState.routes[navState.index])
} else {
console.log("Current Route Name:", navState.routeName)
this.setState({navState: setCurrentRouteName(navState.routeName)})
}
}

Categories