React with Rails User Authentication
A comprehensive full-stack User authorization guide
Architecting a User authentication service on the Rails backend is very straightforward, but it can be a bit challenging to implement this system on the React client/front-end side of the application.
In this article, I show you how to build an authentication system for a React with Rails application using HTTP Cookies and the Rails Session Data. Now, you may be wondering about JWT or JSON Web Tokens, and although that approach seems to be the most popular, I think using session data is more practical and secure. For example, If a User password is hacked, with JWT we would have to wait for the token to expire before we could 100% remove access to the User account. With session data, we can simply delete all browser cookies and all the server’s session data associated with that User and immediately block access to the account with the hacked credentials. However, there are some benefits with using JWT over Session data. You should do some research and decide which method works best for you.
Ok, let’s start building!
Pt. 1 — Rails Setup
We’ll be working with Rails 5, but these techniques should also work on Rails 6.
First, let’s create a new rails project. On your terminal, navigate to your working directory and run the rails new
command:
rails new your-app-name-api --database=postgresql
Notice we are using postgresql
for the database. This will make your life much easier when it comes time to deploy your app. I wrote another article on deploying a React + Rails application here: https://medium.com/how-i-get-it/rails-react-js-heroku-deployment-43d7469e122e
Also notice we didn’t use the --api
flag. We don’t want to initiate an API only application. This would prevent us from using the Rails Session without installing more dependencies. Below we’ll go over how to configure the app to behave like a real API server.
Once the rails new
command is finished building the file structure, we need to install two essential gems: bcryt
and rack-cors
. Add these to your gemfile
and run bundle
in your terminal to install them:
#Gemfilegem 'bcrypt'gem 'rack-cors'
CORS (Cross Origin Resource Sharing) will allow you to whitelist the origin of requests that can communicate with our Rails backend. Since we’ll be fetching server data originating from the front-end client, we need to configure CORS to allow the front-end to access and post data on the backend, and since we are dealing with authentication, we also want to prevent any other origins from communicating with the server.
In the config/initializers
folder, create a new file called cors.rb
and write the following:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head],
credentials: true
end
end
By default, your React client will run on http://localhost:3000. This code allows requests originating from that address to have access to all server resources ('*')
and utilize all HTTP methods: :get
, :post
, :put
, :patch
, :delete
, :options
, :head
. This configuration is ideal while you are working on the development environment. Once your application is ready for production, you will need to change the origin to your deployed front-end client’s domain address.
We now need to configure the way our server will handle the HTTP cookies. To do so, in the same config/initializers
folder, create a new file called session_store.rb
. The server needs to know two things about the cookie; its key
and domain
. Note, the domain
is only needed when the app is in production or deployed:
if Rails.env === 'production'
Rails.application.config.session_store :cookie_store, key: '_your-app-name', domain: 'your-frontend-domain'
else
Rails.application.config.session_store :cookie_store, key: '_your-app-name'
end
By convention, the key name is your application’s name and should start with an underscore.
Another thing we should change, is the default port for our Rails server. Rails 5 uses Puma
to run the server, so in the config/puma.rb
file, change the default port to 3001: port ENV.fetch(“PORT”) { 3001 }
This will prevent our back-end and front-end applications from attempting to run on the same port.
With this, and the two files above, we’ve now configured the Rails backend server to behave like a secure API.
Bcrypt and the User Model
Now that the server is configured, we can build a User model and migrate it to the database. In the terminal:
rails g model User username email password_digest
Note we used password_digest
instead of password
. By doing this we are instructing the bcrypt
gem to encrypt the User’s password. bcrypt
uses a process called ‘salt’ to hash and encrypt passwords. You can learn more about it here: https://auth0.com/blog/hashing-in-action-understanding-bcrypt/. For now, all you need to know is that bcrypt
will secure our passwords.
We can now create and migrate our database with the User model. Make sure your postgres db client is running, otherwise you’ll run onto errors. In the terminal:
rake db:create && rake db:migrate
Check that your schema and database migrations were done correctly. If so, there is one more bcrypt
configuration to do. bcrypt
uses an attribute called has_secure_password
We need to include this in our User model in order for bcrypt
to work. While we are at it, let’s also add some validations. In app/model/user.rb
:
class User < ApplicationRecord
has_secure_passwordvalidates :username, presence: true
validates :username, uniqueness: true
validates :username, length: { minimum: 4 }
validates :email, presence: true
validates :email, uniqueness: truevalidates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
Our validations prevent Users from entering invalid or empty data, and return errors that we can present to Users. With this setup, our User model is complete. Let’s add its corresponding routes. I think we should only allow Users to create a new User, see that User and all other Users. They shouldn’t be allowed to edit or delete the user information, so we will allow the following routes in config/routes.rb
Rails.application.routes.draw do resources :users, only: [:create, :show, :index]end
Let’s now connect the Users Controller. Inside app/controllers
create the users_controller.rb
file. Please note I’m assuming you are familiar with these methods as they are basic Rails knowledge…
class UsersController < ApplicationControllerdef index
@users = User.all
if @users
render json: {
users: @users
}
else
render json: {
status: 500,
errors: ['no users found']
}
endenddef show
@user = User.find(params[:id])
if @user
render json: {
user: @user
}
else
render json: {
status: 500,
errors: ['user not found']
}
end
end
def create
@user = User.new(user_params)
if @user.save
login!
render json: {
status: :created,
user: @user
}
else
render json: {
status: 500,
errors: @user.errors.full_messages
}
end
endprivate
def user_params
params.require(:user).permit(:username, :email, :password, :password_confirmation)
endend
A couple of things to note here, one, I’m only rendering JSON objects because we won’t be using our Rails views. We want to treat our server as an API only app, so we pass the JSON objects to our React client for it to handle displaying them. Two, I’m handling errors in a very basic and semantic way. How you chose to handle errors is up to you. And three, I’m using a helper method login!
to create a User. This method inherits from the Application Controller, where I have additional helper methods that will be useful later on. Let’s add them to application_controller.rb
:
class ApplicationController < ActionController::Base skip_before_action :verify_authenticity_token helper_method :login!, :logged_in?, :current_user, :authorized_user?, :logout!def login!
session[:user_id] = @user.id
enddef logged_in?
!!session[:user_id]
enddef current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
enddef authorized_user?
@user == current_user
enddef logout!
session.clear
endend
There are a lot of important things going on here. Let’s start from the top: the skip_before_action :verify_authenticity_token
command prevents Rails from using its authenticity token. This is a security token that Rails generates from our session data and adds to the parameters sent from a Rails form to a controller action to prevent cross-site request forgery (CSRF) attacks. Since we are treating our back-end as an API, we should disable this so that we don’t receive ‘forbidden’ parameters that will prevent our controller actions from executing without errors.
Next, we have: helper_method :login!, :logged_in?, :current_user, :authorized_user?, :logout!
All this is doing is making sure the methods we define below will be passed to all other controllers in the app.
The login!, logged_in?, current_user, authorized_user?, logout!
methods are pretty self explanatory; we are creating a session or deleting it, or we are authorizing a User based on the session data. These methods will be helpful when dealing with the sessions controller. Let’s write that next.
The Sessions Controller
Create a sessions_controller.rb
file under app/controllers
and write the following:
class SessionsController < ApplicationControllerdef create
@user = User.find_by(email: session_params[:email])
if @user && @user.authenticate(session_params[:password])
login!
render json: {
logged_in: true,
user: @user
}
else
render json: {
status: 401,
errors: ['no such user', 'verify credentials and try again or signup']
}
end
enddef is_logged_in?
if logged_in? && current_user
render json: {
logged_in: true,
user: current_user
}
else
render json: {
logged_in: false,
message: 'no such user'
}
end
enddef destroy
logout!
render json: {
status: 200,
logged_out: true
}
endprivatedef session_params
params.require(:user).permit(:username, :email, :password)
endend
This controller’s purpose is to deal with the User’s Log In and Log Out functionality. Again, I only render JSON objects and handle errors in a very basic way. The .authenticate
method works hand in hand with the User model’s has_secure_password
method to authenticate the password.
These controller actions need their routes, so let’s write those:
Rails.application.routes.draw do post '/login', to: 'sessions#create'
delete '/logout', to: 'sessions#destroy'
get '/logged_in', to: 'sessions#is_logged_in?'
resources :users, only: [:create, :show, :index]end
The /logged_in
route and corresponding action will come to play later when we design the User registration system in the React front-end. For now, just know that it will render a boolean to confirm that the User is authenticated and logged in and if so, it responds with the user
object too.
Pt. 2— React Setup
A few notes before we begin the React part of this article:
- I’m assuming you are familiar with React basics like create-react-app, class vs functional components, props and state basics.
- I’m assuming you have
npm
oryarn
installed in your system. We’ll useyarn
in this article. - We’ll use the ‘axios’ dependency for out HTTP requests to the server, and ‘react-router’ and ‘react-router-dom’ to manage our routes.
- We are going to keep the app’s style to a minimum as it’s really not relevant to this topic.
- We’ll use four components:
App.js
,Home.js
,Login.js
,Signup.js
. OurApp.js
component will handle most of the apps logic and routing.
Let’s start by creating our React app. In your terminal:
npx-create-react-app your-app-name-client && cd your-app-name-client
I tend to clean up and remove all the unnecessary files this scaffold comes with, i.e. App.css,
App.test.js,
logo.svg
, serviceWorker.js
This is my preference, but you decide how to initiate your app. Also I’m removing the serviceWorker
because we won’t need it at all here.
I then also clean the App.js
file to initiate like this:
import React from 'react';function App() {
return (
<div>
</div>
);
}export default App;
And in index.js
, we still pass App.js
to the root element and clean the file as such:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';ReactDOM.render(<App />, document.getElementById('root'));
You may also want to make some changes to your public folder, like changing the app’s name on index.html
and manifest.json
, and remove the unused logos.
Now let’s add the dependencies we need for our app. In your terminal:
yarn add axios && yarn add react-router && yarn add react-router-dom
Alright, so now that the app is setup, let’s start developing the authorization system.
Initial App Logic
We are going to establish the application’s logic in the App.js
component. We will need to change to a class component to manage state, and we will need to import our dependencies axios
plus {BrowserRouter, Switch, Route}
from react-router-dom.
The initial setup for our App.js
component should now look like this:
import React, { Component } from 'react';
import axios from 'axios'
import {BrowserRouter, Switch, Route} from 'react-router-dom'class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user: {}
};
} render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route exact path='/' component={}/>
<Route exact path='/login' component={}/>
<Route exact path='/signup' component={}/>
</Switch>
</BrowserRouter>
</div>
);
}
}export default App;
Our App.js
component is not going to render itself to the DOM, instead, it will serve as our router to render all other components. It will also manage the application’s state and authentication status; we use the component’s state to maintain the logged in status of a User, and to store the User data when we request it from the server. You may have noticed that the Routes aren’t rendering any components yet. We’ll build those and include them here later on.
Let’s add more functionality to the component. Let’s write two methods to interact with the app’s state:
handleLogin = (data) => {
this.setState({
isLoggedIn: true,
user: data.user
})
}handleLogout = () => {
this.setState({
isLoggedIn: false,
user: {}
})
}
With these methods we now control the login status of the application and the User data. As you can see in the handleLogin()
method, we are passing a data
argument. This is the response data we receive from the server. So you can think of these methods as being dependent of other functions and other return values. So how do we get this server response data? Let’s add another method to our component:
loginStatus = () => {
axios.get('http://localhost:3001/logged_in',
{withCredentials: true}) .then(response => {
if (response.data.logged_in) {
this.handleLogin(response)
} else {
this.handleLogout()
}
})
.catch(error => console.log('api errors:', error))
}
This method is very important! It is the backbone of our front-end authorization system. You’ll notice it’s a very straightforward fetch ‘GET’ request using axios
, but you can see that we are also passing another argument {withCredentials: true}
; this is the key here! This allows our Rails server to set and read the cookie on the front-end’s browser. ALWAYS pass this argument! Also notice the http address we are requesting. Remember our Rails /logged_in
route and its corresponding controller action?:
def is_logged_in?
if logged_in? && current_user
render json: {
logged_in: true,
user: current_user
}
else
render json: {
logged_in: false,
message: 'no such user'
}
end
end
Our React App.js
component communicates with Rails through this route and controller action. If the User is verified in the Rails server, then a logged_in
boolean is returned, along with the user
object. App.js
uses this response data to maintain the logged in status in the front-end. We need to ‘automate’ this request though. App.js
should keep track of this status and request this information every time it’s mounted. We can use the componentDidMount()
lifecycle method to call on loginStatus()
and achieve this:
componentDidMount() {
this.loginStatus()
}
With this, our App.js
component should look like this:
import React, { Component } from 'react';
import axios from 'axios'
import {BrowserRouter, Switch, Route} from 'react-router-dom'class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user: {}
};
}componentDidMount() {
this.loginStatus()
} loginStatus = () => {
axios.get('http://localhost:3001/logged_in',
{withCredentials: true}) .then(response => {
if (response.data.logged_in) {
this.handleLogin(response)
} else {
this.handleLogout()
}
})
.catch(error => console.log('api errors:', error))
}handleLogin = (data) => {
this.setState({
isLoggedIn: true,
user: data.user
})
}handleLogout = () => {
this.setState({
isLoggedIn: false,
user: {}
})
}
render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route exact path='/' component={}/>
<Route exact path='/login' component={}/>
<Route exact path='/signup' component={}/>
</Switch>
</BrowserRouter>
</div>
);
}
}export default App;
We now have the main authentication logic for the app, however, all this is useless without a way for the user to Sign Up, Log In, and Log Out. For this, inside src/components
, let’s make a Home.js
file. Also inside, src/components
, let’s create a new folder called registrations
, and inside of it, let’s add Signup.js
and Login.js
files.
The Home Component
This will be a very basic functional component:
import React from 'react';
import {Link} from 'react-router-dom'const Home = () => {
return (
<div>
<Link to='/login'>Log In</Link>
<br></br>
<Link to='/signup'>Sign Up</Link>
</div>
);
};export default Home;
Very simple. We just render two links using the Link
component from react-router-dom
. These links point to the /signup
and /login
routes which will render their respective components. Let’s build those components:
Registration System
The Signup.js
and Login.js
components will handle the User signup and login functionality through the use of controlled forms. Users are presented with a form to enter their credentials, and then these are sent to the server for validation.
A quick note here. These two components are almost identical. To keep the code ‘DRY’, we could use a Higher Order Component, or use Styled Components to pass repeating elements, but this is beyond the scope of this article and as such, we’ll just use two separate components.
Login.js
:
import React, { Component } from 'react';
import axios from 'axios'
import {Link} from 'react-router-dom'class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
errors: ''
};
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
};render() {
const {username, email, password} = this.statereturn (
<div>
<h1>Log In</h1> <form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <button placeholder="submit" type="submit">
Log In
</button> <div>
or <Link to='/signup'>sign up</Link>
</div>
</form>
</div>
);
}
}export default Login;
Signup.js
:
import React, { Component } from 'react';
import axios from 'axios'class Signup extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
password_confirmation: '',
errors: ''
};
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
};render() {
const {username, email, password, password_confirmation} = this.statereturn (
<div>
<h1>Sign Up</h1> <form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <input
placeholder="password confirmation"
type="password"
name="password_confirmation"
value={password_confirmation}
onChange={this.handleChange}
/>
<button placeholder="submit" type="submit">
Sign Up
</button>
</form>
</div>
);
}
}export default Signup;
Both containers are pretty much the same. They are a basic controlled form, where Signup.js
has and additional input
field, password_confirmation
. This is common when using bcrypt
on Rails and it’s good practice in general to have Users confirm their passwords.
Other things to note about the forms; we are adding an errors
attribute to state so we can handle response errors from the server. We are also destructuring
the component’s state and the form’s inputs. On handleChange
, we use this technique to avoid hardcoding each of the state’s properties while setting state on the input’s change. Also, we are using event.preventDefault()
on handleSubmit()
to prevent the browser from submitting the form in the usual way. We are going to add some additional logic to this method — and to other parts of the components, so we need to prevent the form from submitting.
Before we can add this logic to our registrations components, we must expand the App.js
component and the router functionality. This component is actively checking the logged in status of the User. What we really want to do, is pass this status to components that require User authentication. The idea is to only render or display authorized data when the User is logged in, so our components will need this information, and with it, we can conditionally display information to the Users. The content you decide to render to your Users based on their logged in status is up to you. Below we expand App.js
to be able to pass this status — along with our other App.js
methods to our components. We also fully expand the router
logic:
import React, { Component } from 'react';
import axios from 'axios'
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import Home from './components/Home'
import Login from './components/registrations/Login'
import Signup from './components/registrations/Signup'class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user: {}
};
}componentDidMount() {
this.loginStatus()
} loginStatus = () => {
axios.get('http://localhost:3001/logged_in', {withCredentials: true})
.then(response => {
if (response.data.logged_in) {
this.handleLogin(response)
} else {
this.handleLogout()
}
})
.catch(error => console.log('api errors:', error))
}handleLogin = (data) => {
this.setState({
isLoggedIn: true,
user: data.user
})
}handleLogout = () => {
this.setState({
isLoggedIn: false,
user: {}
})
}render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route
exact path='/'
render={props => (
<Home {...props} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
<Route
exact path='/login'
render={props => (
<Login {...props} handleLogin={this.handleLogin} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
<Route
exact path='/signup'
render={props => (
<Signup {...props} handleLogin={this.handleLogin} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
}export default App;
We imported our Home.js
, Login.js
andSignup.js
components. We are also now rendering these components using render=props
. This allows us to pass props to the components to be rendered, and in that way, we can pass isLoggedIn
state status, handleLogin()
, and handleLogout()
, to our components as props!
Note: you can also pass the User object from state down to the necessary components. Which of your components will need that data is up to you, I’m not passing user
at this time.
Back to the Registration System
Now that we have access to App.js
's state and methods, we can expand the functionality of Login.js
and Signup.js
:
Login.js
:
import React, { Component } from 'react';
import axios from 'axios'
import {Link} from 'react-router-dom'class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
errors: ''
};
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
const {username, email, password} = this.statelet user = {
username: username,
email: email,
password: password
}
axios.post('http://localhost:3001/login', {user}, {withCredentials: true})
.then(response => {
if (response.data.logged_in) {
this.props.handleLogin(response.data)
this.redirect()
} else {
this.setState({
errors: response.data.errors
})
}
})
.catch(error => console.log('api errors:', error))
};redirect = () => {
this.props.history.push('/')
}handleErrors = () => {
return (
<div>
<ul>
{this.state.errors.map(error => {
return <li key={error}>{error}</li>
})}
</ul>
</div>
)
}render() {
const {username, email, password} = this.statereturn (
<div>
<h1>Log In</h1>
<form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <button placeholder="submit" type="submit">
Log In
</button> <div>
or <Link to='/signup'>sign up</Link>
</div>
</form>
<div>
{
this.state.errors ? this.handleErrors() : null
}
</div>
</div>
);
}
}export default Login;
Signup.js
:
import React, { Component } from 'react';
import axios from 'axios'class Signup extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
password_confirmation: '',
errors: ''
};
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
const {username, email, password, password_confirmation} = this.state
let user = {
username: username,
email: email,
password: password,
password_confirmation: password_confirmation
}axios.post('http://localhost:3001/users', {user}, {withCredentials: true})
.then(response => {
if (response.data.status === 'created') {
this.props.handleLogin(response.data)
this.redirect()
} else {
this.setState({
errors: response.data.errors
})
}
})
.catch(error => console.log('api errors:', error))
};redirect = () => {
this.props.history.push('/')
}handleErrors = () => {
return (
<div>
<ul>{this.state.errors.map((error) => {
return key={error}>{error}</li>
})}
</ul>
</div>
)
}render() {
const {username, email, password, password_confirmation} = this.statereturn (
<div>
<h1>Sign Up</h1> <form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <input
placeholder="password confirmation"
type="password"
name="password_confirmation"
value={password_confirmation}
onChange={this.handleChange}
/>
<button placeholder="submit" type="submit">
Sign Up
</button>
</form>
<div>
{
this.state.errors ? this.handleErrors() : null
}
</div>
</div>
);
}
}export default Signup;
We expanded the same functionality to both Login.js
and Signup.js
. Let’s break down what we added:
On handleSubmit()
, we are creating a user
object based on the component’s state. This is the data argument that axios
will POST to the Rails server for authentication. Again, note we are also passing {withCredentials: true}
. ALWAYS pass this in the header as it’s what allows Rails to set the cookie! We then check the server response and if it’s valid, we can call App.js
's handleLogin()
method, which will change the app’s isLogged_in
status. Else, the server responds with errors and we set the state’s errors
attribute. Since this attribute evaluates to true
, the handleErrors()
method is called in the component’s render()
method and displays these errors to the user:
handleErrors = () => {
return (
<div>
<ul>
{this.state.errors.map(error => {
return <li key={error}>{error}</li>
})
}
</ul>
</div>
)
}.
.
.render()
...<div>
{
this.state.errors ? this.handleErrors() : null
}
</div>
We are also utilizing redirect()
:
redirect = () => {
this.props.history.push('/')
}
We built this method so we can redirect the User if the server responds with a valid authentication. We have access to the history
props passed to us from react-router-dom
when we use render=props
on App.js
.
We now have a working authorization system on our React front-end. But how do we really implement it? This is really up to you. Now that we have access to App.js
's isLoggedIn
state, we can render or prevent rendering of content. For example, if a User is already logged in, allowing navigation to the /login
route, wouldn’t make sense. Let’s prevent that:
componentWillMount() {
return this.props.loggedInStatus ? this.redirect() : null
}
With this, we prevent a logged in User from navigating to /login
. You can see how the logged in status can be used in several ways; allowing or preventing displaying the User page, rendering items on a NavBar, loading User preferences, or rendering a Log Out link for example. Let’s create the Log Out functionality:
We are going to add a ‘Log Out’ link to the home page in Home.js
. Normally though, I would add this link on a NavBar so that is available to Users at anytime.
import React from 'react';
import {Link} from 'react-router-dom'const Home = (props) => {const handleClick = () => {
// we'll do handle the logout functionality here
}return (
<div>
<Link to='/login'>Log In</Link>
<br></br>
<Link to='/signup'>Sign Up</Link>
<br></br>
{
props.loggedInStatus ?
<Link to='/logout' onClick={handleClick}>Log Out</Link> :
null
}
</div>
);
};export default Home;
Here, we are displaying the link only if the User is logged in, and we are adding an onClick
event that triggers the handleClick
function which will handle the logout functionality. This will have to do two things, 1) update the isLoggedIn
state in our front-end to false and remove the User data, 2) logout the User on the back-end server.
App.js
already has a handleLogout()
method that we can use to update the isLoggedIn
state and remove the User. We need to pass this method as a prop to Home.js
. To do so, we update the route in App.js
:
<Route
exact path='/'
ender={props => (
<Home {...props} handleLogout={this.handleLogout} //pass prop loggedInStatus={this.state.isLoggedIn}/>)}
/>
And now to logout the User on the back-end, we make a request to the server's /logout
route, which triggers the sessions_controller
destroy
action. We also keep the User on the home page by redirecting to the root route. As ALWAYS, remember to pass {withCredentials: true}
:
axios.delete('http://localhost:3001/logout', {withCredentials: true})
.then(response => {
props.handleLogout()
props.history.push('/')
})
.catch(error => console.log(error))
We put it all together and our Home.js
component should now look like this:
import React from 'react';
import axios from 'axios'
import {Link} from 'react-router-dom'const Home = (props) => {const handleClick = () => {
axios.delete('http://localhost:3001/logout', {withCredentials: true})
.then(response => {
props.handleLogout()
props.history.push('/')
})
.catch(error => console.log(error))
}return (
<div>
<Link to='/login'>Log In</Link>
<br></br>
<Link to='/signup'>Sign Up</Link>
<br></br>
{
props.loggedInStatus ?
<Link to='/logout' onClick={handleClick}>Log Out</Link> :
null
}
</div>
);
};export default Home;
And the other components should look like this:
App.js
:
import React, { Component } from 'react';
import axios from 'axios'
import {BrowserRouter, Switch, Route} from 'react-router-dom'
import Home from './components/Home'
import Login from './components/registrations/Login'
import Signup from './components/registrations/Signup'class App extends Component {
constructor(props) {
super(props);
this.state = {
isLoggedIn: false,
user: {}
};
}componentDidMount() {
this.loginStatus()
}loginStatus = () => {
axios.get('http://localhost:3001/logged_in', {withCredentials: true})
.then(response => {
if (response.data.logged_in) {
this.handleLogin(response)
} else {
this.handleLogout()
}
})
.catch(error => console.log('api errors:', error))
}handleLogin = (data) => {
this.setState({
isLoggedIn: true,
user: data.user
})
}handleLogout = () => {
this.setState({
isLoggedIn: false,
user: {}
})
}render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route
exact path='/'
render={props => (
<Home {...props} handleLogout={this.handleLogout} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
<Route
exact path='/login'
render={props => (
<Login {...props} handleLogin={this.handleLogin} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
<Route
exact path='/signup'
render={props => (
<Signup {...props} handleLogin={this.handleLogin} loggedInStatus={this.state.isLoggedIn}/>
)}
/>
</Switch>
</BrowserRouter>
</div>
);
}
}export default App;
Login.js
:
import React, { Component } from 'react';
import axios from 'axios'
import {Link} from 'react-router-dom'class Login extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
errors: ''
};
}
componentWillMount() {
return this.props.loggedInStatus ? this.redirect() : null
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
const {username, email, password} = this.statelet user = {
username: username,
email: email,
password: password
}
axios.post('http://localhost:3001/login', {user}, {withCredentials: true})
.then(response => {
if (response.data.logged_in) {
this.props.handleLogin(response.data)
this.redirect()
} else {
this.setState({
errors: response.data.errors
})
}
})
.catch(error => console.log('api errors:', error))
};redirect = () => {
this.props.history.push('/')
}handleErrors = () => {
return (
<div>
<ul>
{this.state.errors.map(error => {
return <li key={error}>{error}</li>
})
}
</ul>
</div>
)
}render() {
const {username, email, password} = this.statereturn (
<div>
<h1>Log In</h1>
<form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <button placeholder="submit" type="submit">
Log In
</button> <div>
or <Link to='/signup'>sign up</Link>
</div>
</form>
<div>
{
this.state.errors ? this.handleErrors() : null
}
</div>
</div>
);
}
}export default Login;
Signup.js
import React, { Component } from 'react';
import axios from 'axios'class Signup extends Component {
constructor(props) {
super(props);
this.state = {
username: '',
email: '',
password: '',
password_confirmation: '',
errors: ''
};
}handleChange = (event) => {
const {name, value} = event.target
this.setState({
[name]: value
})
};handleSubmit = (event) => {
event.preventDefault()
const {username, email, password, password_confirmation} = this.state
let user = {
username: username,
email: email,
password: password,
password_confirmation: password_confirmation
}axios.post('http://localhost:3001/users', {user}, {withCredentials: true})
.then(response => {
if (response.data.status === 'created') {
this.props.handleLogin(response.data)
this.redirect()
} else {
this.setState({
errors: response.data.errors
})
}
})
.catch(error => console.log('api errors:', error))
};redirect = () => {
this.props.history.push('/')
}handleErrors = () => {
return (
<div>
<ul>{this.state.errors.map((error) => {
return <li key={error}>{error}</li>
})}</ul>
</div>
)
}render() {
const {username, email, password, password_confirmation} = this.statereturn (
<div>
<h1>Sign Up</h1> <form onSubmit={this.handleSubmit}>
<input
placeholder="username"
type="text"
name="username"
value={username}
onChange={this.handleChange}
/>
<input
placeholder="email"
type="text"
name="email"
value={email}
onChange={this.handleChange}
/>
<input
placeholder="password"
type="password"
name="password"
value={password}
onChange={this.handleChange}
/> <input
placeholder="password confirmation"
type="password"
name="password_confirmation"
value={password_confirmation}
onChange={this.handleChange}
/>
<button placeholder="submit" type="submit">
Sign Up
</button>
</form>
<div>
{
this.state.errors ? this.handleErrors() : null
}
</div>
</div>
);
}
}export default Signup;
And that’s it! You now have a working React and Rails User authorization system. I know it’s a long one, but this is a good guide to keep.
Sources:
Jordan Hudgens CTO at devCamp