How to configure between pages role control automation in Next.js? - reactjs

enter image description here
every time the route information changes, it should get the user's role information and then check it before going to the page he wants to go to.
If the permissions of the page and the user role match, it should load the page and allow access.
If there is no access permission, it should redirect the user to the error page without loading the page.
I need an algorithm that will capture all pages in "user_app" in "Next.js" and manage access by providing control.
1- When the user wants to enter a page, I got the path information.
2- I got the data where the page permissions are defined in the process.env with the path information.
3- I got the user role information from the cookie/token.
4- I compared the role information with the page permissions. If the user has permission, they can enter the page, if not, they will be redirected to the 404 page.
Problem: This control structure works when the user enters the page he wants to go to. and it makes that page accessible for a very short time. (while the control function is running)... as I added in the picture, the new page should be checked before loading and after the permission is granted, the page data should be loaded and run.
note: this function is triggered in app_js every time the page changes.
import React from 'react';
import axios from 'axios';
import UserLogout from '../UserLogout';
import decrypted from '../crypto/decrypted';
import { useRouter } from 'next/router';
export default async function RolePageCheck() {
try {
const router = useRouter();
//kullanıcı hangi sayfada onu alıyoruz.
const path = router.pathname;
//public erişebilir sayfaları aldık
//sadece izinle girilebilen sayfaları aldık
const pageRoles = process.env.pageLinks[path];
const res = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/token/${"GET"}`).then((value) => {
//cookie var ise kullanıcı bilgilerini alıyoruz.
if(value.data.Acoount !== null && value.data.Acoount !== undefined && value.data.success === true){
const decryptedValue = decrypted(value.data.Acoount);
const userRole = decryptedValue.role;
if(pageRoles.includes(userRole) && ( !pageRoles.includes(0) || path==="/")){
return true;
}
else{
return router.replace("/404");
}
}
else if(pageRoles.includes(0)){
return true;
}
else{
return router.replace("/404");
}
});
} catch (error) {
return { success: false, message: error.message };
}
}

you can achieve this with router.events
useEffect(() => {
// on initial load - run auth check
authCheck(router.asPath);
// on route change start - hide page content by setting authorized to false
const hideContent = () => setAuthorized(false);
const logAction = () => {
console.log('routeChangeStart');
};
router.events.on('routeChangeStart', logAction);
router.events.on('routeChangeStart', hideContent);
// on route change complete - run auth check
router.events.on('routeChangeComplete', authCheck);
// unsubscribe from events in useEffect return function
return () => {
router.events.off('routeChangeStart', logAction);
router.events.off('routeChangeStart', hideContent);
router.events.off('routeChangeComplete', authCheck);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Related

how to execute a component before another one in next.js?

I've been struggling with this problem for a while. I have an Auth component inside which I try to access to local storage to see if there is a token in there and send it to server to validate that token.
if token is valid the user gets logged-in automatically.
./components/Auth.tsx
const Auth: React.FC<Props> = ({ children }) => {
const dispatch = useDispatch(); // I'm using redux-toolkit to mange the app-wide state
useEffect(() => {
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
const userId = localStorage.getItem("userId");
if (userId) {
axios
.post("/api/get-user-data", { userId, token })
.then((res) => {
dispatch(userActions.login(res.data.user)); // the user gets logged-in
})
.catch((error) => {
localStorage.clear();
console.log(error);
});
}
}
}, [dispatch]);
return <Fragment>{children}</Fragment>;
};
export default Auth;
then I wrap every page components with Auth.tsx in _app.tsx file in order to manage the authentication state globally.
./pages/_app.tsx
<Provider store={store}>
<Auth>
<Component {...pageProps} />
</Auth>
</Provider>
I have a user-profile page in which user can see all his/her information.
in this page first of all I check if the user is authenticated to access this page or not.
if not I redirect him to login page
./pages/user-profile.tsx
useEffect(() => {
if (isAuthenticated) {
// some code
} else {
router.push("/sign-in");
}
}, [isAuthenticated]);
The problem is when the user is in user-profile page and reloads . then the user always gets redirected to login-page even if the user is authenticated.
It's because the code in user-profile useEffect gets executed before the code in Auth component.
(user-profile page is a child to Auth component)
How should i run the code in Auth component before the code in user-profile page ?
I wanna get the user redirected only when he's not authenticated and run all the authentication-related codes before any other code.
Are you sure that the problem is that user-profile's useEffect is executed before Auth's useEffect? I would assume that the outermost useEffect is fired first.
What most probably happens in your case is that the code that you run in the Auth useEffect is asynchronous. You send a request to your API with Axios, then the useEffect method continues to run without waiting for the result. Normally, this is a good situation, but in your profile, you assume that you already have the result of this call.
You would probably have to implement an async function and await the result of both the axios.post method and dispatch method. You would need something like this:
useEffect(() => {
async () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem("token")
const userId = localStorage.getItem("userId")
if (userId) {
try {
const resp = await axios.post("/api/get-user-data", {userId, token})
await dispatch(userActions.login(res.data.user)) // the user gets logged-in
} catch(error) {
localStorage.clear()
console.log(error)
}
}
}
}()
}, [dispatch])
I think this should work, but it would cause your components to wait for the response before anything is rendered.

Unable to set state in .then block after FireBase login with Facebook

I can't seem to figure out what I'm doing incorrectly. Whenever I'm clicking on my Sign In button, my UserContext state does not get set accordingly.
I'm trying to get it to set, and then navigate to the next page.
Any insight on how I can best approach this and why this is happening?
import { UserContext } from "../Components/UserContext";
import { signInWithPopup, FacebookAuthProvider } from "firebase/auth";
[...]
let navigate = useNavigate();
const { user, setUser } = useContext(UserContext);
// Login from Firebase
const signInWithFacebook = () => {
const provider = new FacebookAuthProvider()
signInWithPopup(authentication, provider)
.then((result) => {
const credential = FacebookAuthProvider.credentialFromResult(result);
token = credential.accessToken;
userB = result.user;
setUser(userB)
console.log(user) // this equals null
navigate("../home") // because user state = null, "Home" does not exist in my router, then goes to my 404 page.
})
}
[.......]
return (
[...]
<button onClick={signInWithFacebook}> Sign In </button>
[...]
)
I've tried await in some portions but it never assigned itself accordingly. Along with that I've also tried to set the setUser in a different function, and called that function. That still didn't work for me.

How to redirect back to private route after login in Next.js?

I am building a Next.js project where I want to implement private route similar to react-private route. In React this can be done by using react-router, but in Next.js this cannot be done. next/auth has the option to create private route I guess, but I am not using next/auth.
I have created a HOC for checking if the user is logged-in or not, but I'm not able to redirect the user to the private he/she wants to go after successfully logging in. How to achieve this functionality in Next.js? Can anybody help me in this?
This is the HOC, I used the code from a blog about private routing.
import React from 'react';
import Router from 'next/router';
const login = '/login'; // Define your login route address.
/**
* Check user authentication and authorization
* It depends on you and your auth service provider.
* #returns {{auth: null}}
*/
const checkUserAuthentication = () => {
const token = typeof window !== "undefined" && localStorage.getItem('test_token');
if(!token) {
return { auth: null };
} else return {auth:true};
// change null to { isAdmin: true } for test it.
};
export default WrappedComponent => {
const hocComponent = ({ ...props }) => <WrappedComponent {...props} />;
hocComponent.getInitialProps = async (context) => {
const userAuth = await checkUserAuthentication();
// Are you an authorized user or not?
if (!userAuth?.auth) {
// Handle server-side and client-side rendering.
if (context.res) {
context.res?.writeHead(302, {
Location: login,
});
context.res?.end();
} else {
Router.replace(login);
}
} else if (WrappedComponent.getInitialProps) {
const wrappedProps = await WrappedComponent.getInitialProps({...context, auth: userAuth});
return { ...wrappedProps, userAuth };
}
return { userAuth };
};
return hocComponent;
};
This is my private route code:
import withPrivateRoute from "../../components/withPrivateRoute";
// import WrappedComponent from "../../components/WrappedComponent";
const profilePage = () => {
return (
<div>
<h1>This is private route</h1>
</div>
)
}
export default withPrivateRoute(profilePage);
To redirect back to the protected route the user was trying to access, you can pass a query parameter with the current path (protected route path) when redirecting to the login page.
// Authentication HOC
const loginPath = `/login?from=${encodeURIComponent(context.asPath)}`;
if (context.res) {
context.res.writeHead(302, {
Location: loginPath
});
context.res.end();
} else {
Router.replace(loginPath);
}
In the login page, once the login is complete, you can access the query parameter from router.query.from and use that to redirect the user back.
// Login page
const login = () => {
// Your login logic
// If login is successful, redirect back to `router.query.from`
// or fallback to `/homepage` if login page was accessed directly
router.push(router.query.from && decodeURIComponent(router.query.from) ?? '/homepage');
}
Note that encodeURIComponent/decodeURIComponent is used because the asPath property can contain query string parameters. These need to be encoded when passed to the login URL, and then decoded back when the URL is used to redirect back.

How can I stay the user in the same page?

Every time I reload the my account page, it will go to the log in page for a while and will directed to the Logged in Homepage. How can I stay on the same even after refreshing the page?
I'm just practicing reactjs and I think this is the code that's causing this redirecting to log-in then to home
//if the currentUser is signed in in the application
export const getCurrentUser = () => {
return new Promise((resolve, reject) => {
const unsubscribe = auth.onAuthStateChanged(userAuth => {
unsubscribe();
resolve(userAuth); //this tell us if the user is signed in with the application or not
}, reject);
})
};
.....
import {useEffect} from 'react';
import { useSelector } from 'react-redux';
const mapState = ({ user }) => ({
currentUser: user.currentUser
});
//custom hook
const useAuth = props => {
//get that value, if the current user is null, meaning the user is not logged in
// if they want to access the page, they need to be redirected in a way to log in
const { currentUser } = useSelector(mapState);
useEffect(() => {
//checks if the current user is null
if(!currentUser){
//redirect the user to the log in page
//we have access to history because of withRoute in withAuth.js
props.history.push('/login');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},[currentUser]); //whenever currentUser changes, it will run this code
return currentUser;
};
export default useAuth;
You can make use of local storage as previously mentioned in the comments:
When user logs in
localStorage.setItem('currentUserLogged', true);
And before if(!currentUser)
var currentUser = localStorage.getItem('currentUserLogged');
Please have a look into the following example
Otherwise I recommend you to take a look into Redux Subscribers where you can persist states like so:
store.subscribe(() => {
// store state
})
There are two ways through which you can authenticate your application by using local storage.
The first one is :
set a token value in local storage at the time of logging into your application
localStorage.setItem("auth_token", "any random generated token or any value");
you can use the componentDidMount() method. This method runs on the mounting of any component. you can check here if the value stored in local storage is present or not if it is present it means the user is logged in and can access your page and if not you can redirect the user to the login page.
componentDidMount = () => { if(!localStorage.getItem("auth_token")){ // redirect to login page } }
The second option to authenticate your application by making guards. You can create auth-guards and integrate those guards on your routes. Those guards will check the requirement before rendering each route. It will make your code clean and you do not need to put auth check on every component like the first option.
There are many other ways too for eg. if you are using redux you can use Persist storage or redux store for storing the value but more secure and easy to use.

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

Resources