I have a <Login> component/form that uses <Formik> to keep track of my forms state. There's a specific value props.values.userType that I want to pass to a context provider so I can pass this prop down to my routing component.
My goal is to redirect users that aren't logged in as admin and if they are indeed an admin proceed to render the route as normal.
So I created an AuthContext.
const AuthContext = React.createContext();
In my <Login> component I have the <Formik> component below. Where should I use AuthContext.Provider and how should I pass values.props.userType to that provider? Should props.values.userType be initialized in state of the class component this <Formik> component lives in ?
Or should I create an object store in state that keeps track of the userType? Something like this
export const AuthContext = createContext({
user: null,
isAuthenticated: null
});
I have a codesandbox here.
class FormikLoginForm extends Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
const {} = this.state;
return (
<Formik
initialValues={{
username: "",
password: "",
userType: "",
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 500);
}}
validationSchema={Yup.object().shape({
userType: Yup.string().required("User type is required"),
username: Yup.string().required(
"Required -- select a user type role"
),
password: Yup.string().required("Password is required"),
})}
>
{props => {
const {
values,
touched,
errors,
dirty,
isSubmitting,
handleChange,
handleBlur,
handleSubmit,
handleReset
} = props;
return (
<>
<Grid>
<Grid.Column>
<Header as="h2" color="teal" textAlign="center">
Log-in to your account
</Header>
<Form size="large" onSubmit={handleSubmit}>
<Segment stacked>
<RadioButtonGroup
id="userType"
label="User Type"
value={values.userType}
error={errors.userType}
touched={touched.userType}
>
Then in my index.js file, where I render all my routes, I have my AdminRoute that uses the logic I described above
const AdminRoute = props => {
const { userType, ...routeProps } = props;
if (userType !== "admin") return <Redirect to="/login" />;
return <Route {...routeProps} />;
};
const Routes = () => (
<Router>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/login" component={FormikLoginForm} />
<Route exact path="/admin" component={AdminPage} />
/>
<Route path="/admin/change-password" component={ChangePassword} />
</Switch>
</Router>
);
As you are using React Router I would recommend following the example in their docs for authentication flow and create a PrivateRoute component and use that in place of the regular Route component.
Formik itself wouldn't be the solution to this, it's simply a way to make it easier to interact with forms and perform form validation. Letting the user pass their own type that's later used for authorization does not seem to be a good idea unless that's also somehow required for the authentication flow.
As for the userType it seems to me it's something you should get as a claim from a successful login via an API endpoint, or from whatever login backend you are using. And yes, you could store that in your AuthContext and use it like so (assuming you use React 16.8+ in your project setup):
function AdminPrivateRoute({ component: Component, ...rest }) {
const auth = React.useContext(AuthContext);
return (
<Route
{...rest}
render={props =>
auth.isAuthenticated && auth.userType === 'admin' ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
);
}
The AuthContext.Provider component should be used close to, if not at, the top of the component tree. In your code sample I'd say in the Routes component. You probably also want to implement a way to interact with the context as it will need to be dynamic. Based on the React documentation it could look something like this:
// may want to pass initial auth state as a prop
function AuthProvider({ children }) {
const [state, setState] = React.useState({
isAuthenticated: false,
userType: null,
// other data related to auth/user
});
// may or may not have use for React.useMemo here
const value = {
...state,
// login() does not validate user credentials
// it simply sets isAuthenticated and userType
login: (user) => setState({ isAuthenticated: true, userType: user.type }),
// logout() only clears isAuthenticated, will also need to clear auth cookies
logout: () => setState({ isAuthenticated: false, userType: null }),
};
return (
<AuthContext.Provider value={value}>{children}</AuthContext.Provider>
);
}
Related
I created a react app, I added role based mechanism, the idea is that after the athentication, directly I send a request to an api to get the roles of the user, because the token contains only the username and I can not add any information on the payload.
so while getting thr roles from the api, I added a loder component that will block the user from using the app until the roles are loaded, at this point everything is working well, but when I reloaded the page, the app redirect me to the default route everytime, because the routes are not created yet, I would like to know how to block the app until the routes also are loaded? :
App.tsx :
const App: React.FC = () => {
const useRoles = useRoleBased(); // hook to get roles
return (
<>
{useRoles?.loading(
<Loader open backgroundColor="#ffffff" circleColor="primary" />
)}
<Box className={`mainSideBar ${openSideBar && 'openSideBar'}`}>
<Router />
</Box>
</>
);
};
export default App;
Router.tsx :
const routes = [
{ path: '/logout', element: <ConfirmLogout /> },
{
path: '/dashboard-page',
element: <DashboardPage />,
allowedRoles: [Roles.Director, Roles.RequestFullAccess],
},
{
path: '/profil',
element: <RequestPage />,
allowedRoles: [Roles.Manager],
},
];
const Router: React.FC = () => {
return <RolesAuthRoute routes={routes}></RolesAuthRoute>;
};
export default Router;
RolesAuthRoute.tsx :
export function RolesAuthRoute({ routes }: { routes: IRoutes[] }) {
const userRoles = useSelector((state: any) => state?.roles?.roles);
const isAllowed = (
allowedRoles: Roles[] | undefined,
userRoles: string[]) =>
process.env.REACT_APP_ACTIVATE_ROLE_BASED_AUTH === 'false' ||
!allowedRoles ||
allowedRoles?.some((allowedRole) => userRoles?.includes(allowedRole)
);
return (
<Routes>
{routes.map((route) => {
if (isAllowed(route?.allowedRoles, userRoles))
return (
<Route
path={route?.path}
element={route?.element}
key={route?.path}
/>
);
else return null;
})}
<Route path="*" element={<Navigate to="/" replace />} /> //this route is created in all cases
</Routes>
);
}
You could return early (conditional rendering) to stop the router from rendering prematurely. You'll need to modify the hook to return the loading state as boolean instead of rendering the component as it seems to be currently implemented.
const App: React.FC = () => {
const useRoles = useRoleBased(); // hook to get roles
if(useRoles.isLoading){
return <Loader open backgroundColor="#ffffff" circleColor="primary" />
};
return (
<>
<Box className={`mainSideBar ${openSideBar && 'openSideBar'}`}>
<Router />
</Box>
</>
);
};
export default App;
I have an context where i save the user data, and i have another component when verify the context user is null, if the context user is null my component should redirect the user to the login page, if not should render the component. My routers is inside my Authprovider, but still losing the user data when reload the router. I found another posts with the same issue, and the instruction is to keep the routers inside the useauthprovider, but doesn't work with my app.
My code
function App() {
let header = window.location.pathname === '/login' || '/cadastro' ? <Header /> : null;
let footer = window.location.pathname === '/login' || '/cadastro' ? <Footer /> : null;
return (
<UseAuthProvider> // My use AuthProvider
<Router>
<div className='app-container' >
<Switch>
<Cart>
<Header />
<NavbarMenu />
<div className='app-body'>
<UseCampanhaProvider>
<PublicRoute exact path='/' component={Home} />
<PrivateRoute exact path='/cupom/:campaignId' component={CupomScreen} />
<PrivateRoute exact path='/carrinho' component={CartScreen} />
</UseCampanhaProvider>
<PublicRoute exact path='/login' restricted={true} component={Login} />
<PublicRoute path='/cadastro' restricted={true} component={Cadastro} />
</div>
<AuthModal />
{footer}
</Cart>
</Switch>
</div>
</Router >
</UseAuthProvider>
);
}
export default App;
My component where i verify the user context
const PrivateRoute = ({ component: Component, ...rest }) => {
const { user } = useAuth();
return (
<Route {...rest} render={props => (
!user ?
<Redirect to='/login' />
:
<Component {...props} />
)} />
);
};
export default PrivateRoute;
My context where i load the user
const UseAuthProvider = ({ children }) => {
const [user, setUser] = useState();
const [open, setOpen] = useState(false)
useEffect(() => {
verifyUser(); //here i call the function when verify the localstorage
}, [])
const verifyUser = async () => {
let tokenHeader = authHeader();
if (tokenHeader) {
await Api.post('/cliente/index', {}, {
headers: {
...tokenHeader
}
}).then((response) => {
setUser(response.data.cliente)
})
}
}
const handleModal = () => {
setOpen((state) => !state)
}
const Logout = async () => {
localStorage.clear('acessToken-bolao')
setUser(null)
}
return (
<useAuthContext.Provider value={{ Auth, verifyUser, user, Register, Logout, open, handleModal }}>
{children}
</useAuthContext.Provider>
)
}
I tried to debug my application and when i redirect my user to another router, before the component render my user return undefined, and after my component is rendered the context load the user data.
It sounds like your entire application is unmounting and remounting.
In this case the state will be lost as it is not simply a re-render.
By what mechanism are you navigating to the new page?
If I remember React-Router correctly you need to use
If you try navigating the url itself with window.location or href then you are reloading the entire page (not using the router in the SPA)
If routed correctly I would expect that only data inside the Switch would be re-loaded.
trying to use the handle submit function to change the isAuthinticated to true from false thats on the main app.js file thats using react router.. all of the redux examples i look at are all using an app js file that has the onclick function on it not react router. thank you if you can help
function App () {
return (
<Router>
<div>
<Switch>
<Route exact path="/" component={Login} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
<PrivateRoute
exact path="/mainpage"
component={MainPage}
isAuthenticated={false}
/>
</Switch>
<Footer />
</div>
</Router>
);
}
export default App;
my login.js that has the click event
const Login = () => {
const history = useHistory()
const [state, setState] = useState({
email: '',
password: ''
});
const [validate, setValid] = useState({
validateEmail: '',
validatePassword: '',
})
const handleInputChange = event => setState({
...state,
[event.target.name]: event.target.value,
})
const handleSubmit = user => {
if(state.password === ''){
setValid({validatePassword:true})
}
if(state.email === '' ){
setValid({validateEmail:true})
}
axios.post(`auth/login`, state )
.then(res => {
console.log(res);
console.log(res.data);
if (res.status === 200 ) {
history.push('/mainpage');
}
})
.catch(function (error) {
console.log(error);
alert('Wrong username or password')
window.location.reload();
});
}
// console.log('state', state)
const {email, password } = state
const [popoverOpen, setPopoverOpen] = useState(false);
const toggle = () => setPopoverOpen(!popoverOpen);
return (
<>
<Container>
<Row>
<Col>
<img src={logo} alt="Logo" id="logo" />
<Button id="Popover1" type="button">
About Crypto-Tracker
</Button>
<Popover placement="bottom" isOpen={popoverOpen} target="Popover1" toggle={toggle}>
<PopoverHeader>About</PopoverHeader>
<PopoverBody>Your personalized finance app to track all potential cryptocurrency investments</PopoverBody>
</Popover>
</Col>
<Col sm="2" id="home" style={{height: 500}}>
<Card body className="login-card">
<Form className="login-form">
<h2 className="text-center">Welcome</h2>
<h3 className="text-center">____________</h3>
<FormGroup>
<Label for="exampleEmail">Email</Label>
<Input invalid={validate.validateEmail} onChange = {handleInputChange} value = {email} type="email" required name="email" placeholder="email" />
<FormFeedback>Please enter email</FormFeedback>
</FormGroup>
<FormGroup>
<Label for="examplePassword">Password</Label>
<Input invalid={validate.validatePassword} onChange = {handleInputChange} value = {password} type="password" required name="password" placeholder="password"/>
<FormFeedback>Please enter password</FormFeedback>
</FormGroup>
<Button onClick={()=> handleSubmit(state)} className="but-lg btn-dark btn-block">Login</Button>
<div className="text-center pt-3"> or sign in with Google account</div>
<Loginbutton />
<div className="text-center">
Sign up
<span className="p-2">|</span>
Forgot password
</div>
</Form>
</Card>
</Col>
</Row>
</Container>
</>
);
}
export default Login;
authslice.js using asyncthunk
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { userAPI } from '../../../server/routes/authRoutes'
const fetchAuth = createAsyncThunk(
'auth/login',
async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
const authSlice = createSlice({
name: 'isAuthenticated',
initialState: {
value: false,
},
reducers: {
// Authenticated: state => {
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
// state.value = true;
// },
},
extraReducers: {
[fetchAuth.fulfilled]: state => {
// Add user to the state array
state.value = true
},
[fetchAuth.rejected]: state => {
// Add user to the state array
state.value = false
}
}
});
dispatch(fetchUserById(123))
// export const { increment, decrement, incrementByAmount } = counterSlice.actions;
This would be easy to achieve if you use some kind of global store to store such values. Typical options include Redux, Mobx, etc.
Define your isAuthenticated variable as a global store variable. mutate its value using corresponding package methods like Redux dispatch events or similar from your functions anywhere else in the app.
You can try it using Redux, creating a store to the login variables, and sharing it with the component App.js.
I am building a MERN app with a login/register system and I am stuck on being able to redirect a user to a confirmation page which then prompts them to login.
It seems like I could use the useHistory hook in react-router-dom and do history.push() within my axios request which is within my register function:
function handleRegister(e) {
let history = useHistory();
e.preventDefault();
// Grab state
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
// Redirect user to the /thankyouForRegistering page which prompts them to login.
history.push("/thankyouForRegistering");
})
.catch((err) => {
console.log(err);
});
}
But this does not work. I get an error back saying:
React Hook "useHistory" is called in function "handleRegister" which is neither a React function component or a custom React Hook function
Upon further research, it seems that in order to use the useHistory hook, it has to be within <Router>(possibly?) or directly on an onClick handler.
So something like this:
<Button onClick={() => history.push()}></button>
I can't really do that though, because I am not using onClick for my register button, I am using onSubmit and my own register function.
I also looked into using <Redirect />, so I tried making a new state called authorized, set the authorize state to true in my axios request, and then tried this:
<Route
path="/thankyouForRegistering"
render={() => (
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
))
}
/>
But this is not working either, and it also does not give me any kind of error.
Does anyone know the best way to redirect a user to a new page upon registering/logging in? I've been struggling with this for going on two weeks.
Thanks!!
EDIT: Here is the entire component - it's a bit messy but if anyone needs any explanations please let me know.
import React, { useState } from "react";
import {
BrowserRouter as Router,
Switch,
Route,
Redirect,
useHistory,
} from "react-router-dom";
let navMenu;
function App() {
let history = useHistory();
const [navMenuOpen, setNavMenuOpen] = useState(false);
const [loggedIn, setLoggedIn] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [token, setToken] = useState("");
const [authorized, setAuthorized] = useState(false);
const initialState = {
username: "",
email: "",
password: "",
password2: "",
};
const [formState, setFormState] = useState(initialState);
const { username, email, password, password2 } = formState;
const handleChange = (e) => {
setFormState({ ...formState, [e.target.name]: e.target.value });
};
function handleRegister(e) {
//const history = useHistory();
e.preventDefault();
// Grab setState
const user = {
username: formState.username,
email: formState.email,
password: formState.password,
password2: formState.password2,
};
// Post request to backend
axios
.post("http://localhost:4000/register", user)
.then((res) => {
console.log(user);
history.push("/thankyouForRegistering");
setAuthorized(true);
// Redirect user to the /thankyouForRegistering page which prompts them to login.
})
.catch((err) => {
console.log(err);
});
// Once a user has registered, clear the registration form and redirect the user to a page that says thank you for registering, please login.
}
const handleLogin = (e) => {
e.preventDefault();
// Grab setState
const userData = {
email: formState.email,
password: formState.password,
};
axios
.post("http://localhost:4000/login", userData)
.then((res) => {
// Get token from local storage if there is a token
localStorage.setItem("token", res.data.token);
// If there is a token, redirect user to their profile and give them access to
// their recipeList and shoppingList
setLoggedIn(true);
//props.history.push("/profile");
})
.catch((err) => console.log(err));
};
const navMenuToggle = () => {
console.log("toggle");
setNavMenuOpen(!navMenuOpen);
};
const navMenuClose = () => {
setNavMenuOpen(false);
};
const logoutFromNavMenu = () => {
setLoggedIn(false);
navMenuClose();
};
return (
<Router>
<div>
<Navbar
loggedIn={loggedIn}
navMenuToggle={navMenuToggle}
/>
<NavMenu
loggedIn={loggedIn}
show={navMenuOpen}
navMenuClose={navMenuClose}
logoutFromNavMenu={logoutFromNavMenu}
/>
<Switch>
<Route
path="/login"
render={(props) => (
<Login
handleLogin={handleLogin}
handleChange={handleChange}
email={email}
password={password}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/register"
render={(props) => (
<Register
handleRegister={handleRegister}
handleChange={handleChange}
email={email}
username={username}
password={password}
password2={password2}
errorMsg={errorMsg}
/>
)}
/>
<Route
path="/profile"
render={() => (loggedIn ? <Profile /> : <Redirect to="/login" />)}
/>
<Route
path="/thankyouForRegistering"
render={() =>
authorized ? (
<ThankyouForRegistering />
) : (
<Redirect to="/register" />
)
}
/>
<Route
path="/recipes"
render={(props) =>
loggedIn ? <RecipeBook /> : <Redirect to="/login" />
}
/>
<Route
path="/list"
render={(props) =>
loggedIn ? <ShoppingList /> : <Redirect to="/login" />
}
/>
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
<Route
path="/accountSettings"
render={(props) =>
loggedIn ? <AccountSettings /> : <Redirect to="/login" />
}
/>
<Route
exact
path="/"
component={() => <Home isLoggedIn={loggedIn} />}
/>
</Switch>
</div>
</Router>
);
}
export default App;
I figured out what the problem was. It wasn't enough to be using useHistory, I had to be using withRouter to get access to the history prop.
So I imported withRouter to my App component:
import { withRouter } from 'react-router-dom';
Then added this at the very bottom of the page, right below the App function:
const AppWithRouter = withRouter(App);
export default AppWithRouter;
So the App now must be exported as an argument of withRouter to get access to the history prop (as well as location and params I think?)
But this still wasn't enough. It was giving me an error saying that withRouter can only be used within <Router />(or <BrowserRouter />, whichever one you are using). So I wrapped my main <App /> in index.js with <BrowserRouter />
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
And imported it of course.
This still wasn't enough, though. After adding
history.push("/thankYouForRegistering);
to my axios request, it added the route to the URL, but it did not re render the view. It was still stuck on the register page. Then I realized I had <BrowserRouter /> in two different places now. In my App component, I still had it wrapped around all of my routes, so after removing that and just returning a div with <Switch /> wrapped around all of my routes, that made it work like expected.
I hope this helps somebody who is facing the same issue!
i am developping a Reactjs-nodejs application. I would like to make a JWT authentification. when we log in, i give a unique token to the user. Then, thanks to this token, if it is valid, i allow the user to navigate through my router. my private route component is like :
PrivateRoute
My function getId is like that:
async function getId(){
let res = await axios('_/api/users/me',{config}).catch(err => { console.log(err)});
return res+1;
}
Finally the config component is the token stored in the localStorage :
const config = {
headers: { Authorization: ${window.localStorage.getItem("token")} }
};
GetId() returns the id of the user if logged in, else it is null.
The problem now is that my privateRoute always redirect to "/" path. I think it is because of the axios(promise) that gives me the userId too late. please tell me if you understand well and if you have a solution.
Thanks you
You can create private-route like this .
const PrivateRoute = ({ component: Component, ...props }) => {
return (
<Route
{...props}
render={innerProps =>
localStorage.getItem("Token") ? (
<Component {...innerProps} />
) : (
<Redirect
to={{
pathname: "/",
state: { from: props.location }
}}
/>
)
}
/>
);
};
then you can use
<PrivateRoute path="/" component={FIlname} />
if you are using redux, you can make the following:
in reduer:
case LOGIN_ADMIN_SUCCESS:
localStorage.setItem("token", action.payload.token);
return {
...state,
user: action.payload,
isSignedIn: true,
loadingSignIn: false,
token: action.payload.token,
};
then in your private router you have to check these values, like
const PrivateRoute = ({ component: Component, admin, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (admin.loadingSignIn) {
return <h3>Loading...</h3>;
} else if (!admin.isSignedIn) {
return <Redirect to="/login" />;
} else {
return <Component {...props} />;
}
}}
/>
);