I'm running ReactRouter on top of a Koa server.
My Koa server is configured so that all requests point to 'index.html' (using koa-connect-history-api-fallback), which in turn forwards the requests to the ReactRouter. This all works great, except I'm having trouble figuring out how to do user authentication.
I want to protect my Routes so that users must be logged in to access any of the Routes. The problem is that my login page is one of the Routes, which means a user has to be logged in to access the login page!!
Is there a good way to get around this? For example, in my Koa server could I somehow protect all routes except for the '/login' route? I've read this example, which takes care of the authentication within the ReactRouter, but it seems sketchy to me to have your authentication on the client side. I could be off base though.
In case your curious, I'm working off react-redux-starter-kit
You could authenticate your top level components using a decorator, e.g.
// auth.js
import React, {Component} from 'react';
import { connect } from 'react-redux';
export default function(ComposedComponent) {
class Auth extends Component {
static contextTypes = {
router: React.PropTypes.object
}
componentWillMount() {
if (!this.props.authenticated) {
this.context.router.push('/');
}
}
componentWillUpdate(nextProps) {
if (!nextProps.authenticated) {
this.context.router.push('/');
}
}
render() {
return <ComposedComponent {...this.props} />
}
}
function mapStateToProps(state) {
return {authenticated: state.auth.authenticated};
}
return connect(mapStateToProps)(Auth);
}
and:
#auth
...your component...
Or you could do it in a less "reduxy" way if you had an auth module:
function requireAuth(nextState, replace) {
if (!auth.loggedIn()) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
}
render((
<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="login" component={Login} />
<Route path="logout" component={Logout} />
<Route path="about" component={About} />
<Route path="dashboard" component={Dashboard} onEnter={requireAuth} />
</Route>
</Router>
), document.getElementById('example'))
In this instance I would leave the responsibility of protecting routes with ReactRouter, and use match on the server to figure out what route you actually want to render/redirect to when any request comes in.
Importantly, when a request comes to the Koa server, you run it through some authentication middleware that is able to tell you if the user is authenticated and their role. You then want this information reflected in the Redux store. You could either generate the store with an initial state something like this:
{
user: {
authenticated: true,
role: 'admin'
}
}
, or even better you could dispatch an action on the store where a reducer does this for you.
Now, when you create your routes on the server (passing in your store) React Router will know exactly what is okay and what is not. That is, if you have protected Routes with onEnter by checking user.authenticated, then calling match on the server will honour that and return a redirect to something like /login. An example onEnter might look like this:
const ensureLoggedIn = (nextState, replace) => {
if (!store.getState().user.authenticated)) {
replace('/login');
}
};
You can capture that redirect in redirectLocation which is an argument in the callback to match. Read all about match here. Then you can just use the servers res.redirect with the new location.
Of course, this type of route protection is just convenience, you will want to legitimately protect your API endpoints that contain sensitive information. But this method is invaluable because it uses the same logic for routing on the client and server with pretty much no effort.
Related
Is it not possible to route to the same component with a wildcard path?
If in React I have something like:
<Router>
<Switch>
<Route path="/path/:id" children={<Component />} />
</Switch>
</Router>
all the requests:
/path/123
/path/123/p
/path/123/p/1
will route to the same /path/123
How can I tell Gatsby to do the same?
createPage({
path: `/path/123/*`,
component,
context
})
Or what is the solution to this problem, a redirect engine of some sorts?
I think you are looking for client-only routes. Given a page (or template if it's created from gatsby-node.js) you can:
import React from "react"
import { Router } from "#reach/router"
import Layout from "../components/Layout"
import SomeComponent from "../components/SomeComponent"
const App = () => {
return (
<Layout>
<Router basepath="/app">
<SomeComponent path="/path" />
</Router>
</Layout>
)
}
export default App
Note: assuming a src/pages/app/[...].js page (File System Route API structure).
When a page loads, Reach Router looks at the path prop of each component nested under <Router />, and chooses one to render that best matches window.location (you can learn more about how routing works from the #reach/router documentation).
Alternatively, you can use an automated approach (plugin: gatsby-plugin-create-client-paths) by:
{
resolve: `gatsby-plugin-create-client-paths`,
options: { prefixes: [`/path/*`] },
},
Which will validate all routes under /path.
Or for a more customizable approach, in your gatsby-node.js:
exports.onCreatePage = async ({ page, actions }) => {
const { createPage } = actions
// page.matchPath is a special key that's used for matching pages
// only on the client.
if (page.path.match(/^\/path/)) {
page.matchPath = "/path/*"
// Update the page.
createPage(page)
}
}
Disclaimer: These routes will exist on the client only and will not correspond to index.html files in an app’s built assets. If you’d like site users to be able to visit client routes directly, you’ll need to set up your server to handle those routes appropriately.
I have normal and social media login options on my page, and I have created a private Route as in https://reacttraining.com/react-router/web/example/auth-workflow.
The problem is that after redirecting to for example google and back, the state of the Router history is gone.
I have created a workaround in which I store the previous URL in local storage, when the user visits the login screen.
Do you guys have any better idea on how to save History state?
I think the main issue is that you're not persisting the data.
Start off by creating a service that checks for the current user. You can persist this data in localstorage, and upon logout it should clear out the credentials.
I did an implementation like this in my previous side project.
https://github.com/EliHood/fullstacktypescript
I have something like ...
export function* getAutoLoginStatus(action) {
try {
const login = yield call(api.user.currentUser);
const token = login.token;
if (login.user.googleId !== null) {
localStorage.setItem("googleId", login.user.googleId);
}
setAuthToken(token);
sessionData.setUserLoggedIn(token);
yield put(actionTypes.getUserSuccess(login));
} catch (error) {
localStorage.clear();
yield put(actionTypes.getUserFailure(error.response.data.message));
}
}
This saga is getting called on nav component, this checks to see if user is logged in or not.
componentDidMount() {
this.props.getUser();
}
You should consider using react-router-dom, you install it by going to your project folder on command line and put:
npm i react-router-dom
Go to your App.js file and import the following:
import { BrowserRouter as Router, Route} from 'react-router-dom';
Then at your App.js file, you should render something like:
render(){
return (
<div className="App">
<Router>
<Route path="/" exact component={() => <[YourHomeJSXComponent]/>} />
<Route path="/anotherPage" exact component={()=> <[AnotherJSXComponent]/>}/>
</Router>
</div>
);
}
Then, when you want to redirect to another page, you simply put the following command:
window.location.assign("/anotherPage");
And voila, every location.assign will register an entry on your navigation history.
I've been scouring the inet trying to find anywhere that defines how to handle authentication in meteor and react router 4 .
Basically, I want certain routes to only be available to authenticated users. Is there any documentation on it?
Aseel
Meteor has a very well developed User Accounts system. It provides ready libraries for OAuth authentication with Twitter, Facebook, etc. as well as a basic but useful UI packages. Check Meteor's official guide here first.
For implementing routing you need to track Meteor.userId() and change route via Meteor's reactive system called Tracker. Meteor.userId() returns a userId if currently connected user is logged in, and null otherwise. I provide an example code where React Router is used for routing, below. Notice that you'll will also need the historypackage to be installed and imported while working with React Router v4.
In your client/main.js;
import { Meteor } from 'meteor/meteor';
import React from 'react';
import ReactDOM from 'react-dom';
import { Tracker } from 'meteor/tracker'
import {onAuthChange, routes} from "../imports/routes/routes";
Tracker.autorun(function(){
const authenticated = !! Meteor.userId();
onAuthChange(authenticated);
});
Meteor.startup(() => {
ReactDOM.render(routes, document.getElementById('app'));
});
And in your routes.js file;
import { Meteor } from 'meteor/meteor';
import React from 'react';
import { Router, Route, Switch } from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory'
import Home from './../ui/components/Home';
import Login from './../ui/components/Login';
import NotFound from './../ui/components/NotFound';
import Signup from './../ui/components/Signup';
const history = createBrowserHistory();
const unauthenticatedPages = ['/', '/signup'];
const authenticatedPages = ['/link'];
const publicPage = function () {
if (Meteor.userId()) {
history.replace('/link');
}
};
const privatePage = function () {
if(! Meteor.userId()) {
history.replace('/');
}
};
export const routes = (
<Router history = {history}>
<Switch>
<Route exact path='/:id' component= {Login} onEnter={publicPage}/>
<Route exact path='/signup' component={Signup} onEnter={publicPage}/>
<Route exact path='/link' render={ () => <Home greet='User'/> } onEnter={privatePage} />
<Route component={NotFound}/>
</Switch>
</Router>
);
export const onAuthChange = function (authenticated) {
console.log("isAuthenticated: ", authenticated);
const path = history.location.pathname;
const isUnauthenticatedPage = unauthenticatedPages.includes(path);
const isAuthenticatedPage = authenticatedPages.includes(path);
if (authenticated && isUnauthenticatedPage) { // pages: /signup and /
console.log(`Authenticated user routed to the path /link`);
history.replace('/link');
} else if (!authenticated && isAuthenticatedPage) {
console.log(`Unauthenticated user routed to the path /`);
history.replace('/');
}
};
Here's a neat way to have public routes and authenticated routes:
https://gist.github.com/lucnat/643988451c783a8428a2811dbea3d168
public components are visible by everyone, they use the PublicLayout
authenticated components are visible by authenticated users only -
they use the AuthenticatedLayout
We could have an arbitrary number of layouts. In the example above, there are two layouts - each with it's own navbar.
I've been trying to get a more updated method using functional components.
I've tried implementing a conditional check similar to the documentation of React-router.
This was working after giving history.push to a desired route after waiting for Meteor.loginWithPassword to complete.
But refreshing the browser ended up rendering login page again.
Meteor is having an intermediate state of Meteor.loggingIn().
Handling this state in the Authentication check fixed this issue.
Feel free to give feedback.
I've created a gist with an implementation for authentication of routes in Meteor - React-router stack with functional components and hooks.
Check this gist with basic structure of the implementation.
https://gist.github.com/rinturj84/0ef61005bf3a4ca5fb665dfc5f77e3d1
Actually, the best idea to do that is to create Multiple separated routers because you can take benefits from using Meteor Reactive-var.
This is a sample :
export default function App() {
if (Meteor.user()) {
return (
<React.StrictMode>
<Global_Router Client={null} About={About} HomeNavbar={HomePageNav} HomePage_Home={HomePage_Home}
HomeFooter={Footer} Homepage_Contacts={Homepage_Contacts}/>
</React.StrictMode>
);
}
else {
return(
<RouterClient/>
);
}
Router client
export const RouterClient = () => {
return (
<Router className="container-fluid">
<Switch >
<Route exact path="/Client" >
<HomeCLient/>
</Route>
<Route path="*">
<Redirect to="/Client" />
</Route>
</Switch>
</Router>
)
};
Respectively,you can create a router for the admin too. In general, that is the most efficient way to do that.
After a user successfully logs in, I want to push the state of my application to the '/home' route, which exists and can be accessed by changing the url. The problem is I am getting an error that says
"Cannot read property of push state of undefined"
I am trying to use this.props.history.pushState(null, '/home') to accomplish this reroute to the home page, but obviously this.props.history doesn't exist. Can someone help me use the router correctly?
login.jsx
var Login = React.createClass({
getInitialState: function() {
return {
email: null,
password: null
};
},
handleLogin: function(event) {
event.preventDefault();
var props = {
email: this.state.email,
password: this.state.password
};
axios.post('/login', props).then(() => {
console.log(this.props);
this.props.history.pushState(null, '/home'); //Cannot read property of push state of undefined
});
},
app.jsx contains the route definitions
ReactDOM.render(
<Router history={hashHistory}>
<Route path="/" component={Main}>
<Route path="about" component={About} />
<Route path="home" component={Home} />
<IndexRoute component={Signup} />
</Route>
</Router>,
document.getElementById('masterContainer')
);
It seems that you are not passing the history object as a prop to the Login component - that's where the error comes from (but you probably already know that). So you have to either pass it or use the context. The context seems to be more natural and easy way, but the way it can be used depends on the version of react-router.
If you are using the 2.x version, you will have to add this bit to the component class:
contextTypes: {
router: React.PropTypes.object
},
Then you will be able to use this.context.router.push in your component.
See docs for some info about the methods you can use and this example for more insight.
Edit:
Since the 2.4.0 version you can also use the withRouter component wrapper. In this case you will be able to access router through the props (this.props.router) instead of the context. More info about that here.
For version 1.x you can use the History mixin. Also you should have the history prop on the component that you passed to the router - but since this is not the case, maybe you could just pass it from the parent of the Login component.
The context option is also available (this is how the History mixin works), but this time you will have to pass
contextTypes: {
history: React.PropTypes.object
},
Then you will have all the mixin methods in the this.context.history object.
Hello StackOverflow community !
My application background is a bit complex but I feel like my problem isn't.
So I have a debut of application which consist of a Sign Up / Log In / Private component. My routes.js contains the following
// routes.js
function requireAuth(nextState, transition) {
if (!LoginStore.isLoggedIn()) {
transition.to('/login', null, { nextPathname: nextState.location.pathname });
}
}
<Route component={require('./components/App')} >
<Route path="/" component={require('./components/Home')} />
<Route path="/login" component={require('./components/LogIn')} />
<Route path="/signup" component={require('./components/SignUp')} />
<Route path="/private" component={require('./components/Private')} onEnter={requireAuth}/>
</Route>
Log In component retrieve from an API a JWT, store it in a LoginStore component (Flux design) and in a Cookie in order to have access to the Private component without re-login later on.
The whole is working correctly as when I'm logged, I have access to the Private component and I can access it when refreshing (thanks to the cookie).
My problem comes from the fact that I am also rendering this solution on the server and if I try to access directly to /private (by directly I mean my first call on the application is /private), I am redirected to /login and then I can access to /private. I would like to fall on /private at the first call.
// server.js
var location = new Location(req.path, req.query);
Router.run(routes, location, (error, initialState, transition) => {
console.log(initialState);
if (transition.isCancelled) {
return res.redirect(302, transition.redirectInfo.pathname);
} else {
var html = React.renderToString(<Router {...initialState}/>);
res.send(renderFullPage(html));
}
});
My LoginStore should retrieve the cookie and allow the access to the Private component but he doesn't succeed because my cookie cannot be find yet.
GET http://localhost/private [HTTP/1.1 302 Moved Temporarily 30ms]
GET http://localhost/login [HTTP/1.1 200 OK 51ms]
GET http://localhost/bundle.js [HTTP/1.1 200 OK 30ms]
I feel like I should send my cookie to the Router in the server.js so the LoginStore can be set but I don't know much about how to do it and it may not be the best solution. I would really appreciate help on this problem.
Thanks you in advance.
Your solution is similar to mine. Use react-cookie for manipulating cookies and patch save/delete method on the server on each request before calling Router.run. Also make sure your routes are sync.
import Iso from 'iso';
import React from 'react';
import ReactDomServer from 'react-dom/server';
import Router from 'react-router';
import Location from 'react-router/lib/Location';
import cookie from 'react-cookie';
import routes from '../../app/routes';
import Alt from '../../app/lib/Alt';
import AltBootstrap from '../lib/AltBootstrap';
export default {render};
function render(req, res, next) {
cookie.setRawCookie(req.headers.cookie); // THIS
cookie.save = res.cookie.bind(res); // THIS
let location = new Location(req.path, req.query);
Router.run(routes, location, (error, state, transition) => {
if (error) return next(error);
if (transition.isCancelled) return res.redirect(transition.redirectInfo.pathname);
AltBootstrap.run(state, req).then(snapshot => {
Alt.bootstrap(snapshot);
let markup = ReactDomServer.renderToString(<Router {...state}/>);
let html = Iso.render(markup, Alt.flush());
res.render('index', {html});
}).catch(next);
});
}
A complete source code is available here: isomorphic-react-flux-boilerplate.