DEV Community

Cover image for Rails API User Authentication in React A detailed tutorial on Rails/React user authentication
diiiiana99
diiiiana99

Posted on

Rails API User Authentication in React A detailed tutorial on Rails/React user authentication

API for Rails
Let's begin by establishing the API that will be used to authenticate our users in our react app.

Prerequisites
Please make sure the following are installed on your computer before beginning this portion of the course. This will make it easier for you to follow along without any problems.

Rails >= 6.0 should be installed.
Install PostgreSQL and set it up. Installers for Ubuntu may be found here.
Install your preferred text editor.
Rails new auth-api —api -d=postgresql creates a new Rails app. This script builds a new API Rails app called auth-api with the database Postgresql. Change to the app directory; cd auth-api

Construct a User model.

rails g model User email, password_digest

Enter fullscreen mode Exit fullscreen mode

Now let's create the database and migrate the schema.

Runrails db:create and rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Update Gemfile
Add gem 'bcrypt' and gem 'racks-cors', :require => 'racks/cors'

Install the gems by running bundle install

Set up the session storage.
The user's session will be saved across the app using this file settings.

Create a file called session store.rb in config/initializers.

if Rails.env == "production"
  Rails.application.config.session_store :cookie_store, key: "_authentication_app", domain: "https://link-to-your-production-app.com/"
else
  Rails.application.config.session_store :cookie_store, key: "_authentication_app"
end
Enter fullscreen mode Exit fullscreen mode

Let's set up the CORS middleware (Cross-Origin Resource Sharing).
CORS (Cross-Origin Resource Sharing) is a method that uses extra HTTP headers to instruct browsers to allow a web application operating on one origin to access resources from another origin. When a web application accesses a resource from a domain, protocol, or port other than its own, it makes a cross-origin HTTP request. Find out more here.

Inside config/initializers, create the file cors.rb and paste the code below into it.

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

  allow do
    origins "https://link-to-production-app.com/"
    resource "*", headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head], credentials: true
  end
end

Enter fullscreen mode Exit fullscreen mode

Create the sessions controller
rails g controller Sessions

app/controllers/sessions_controller

class SessionsController < ApplicationController
  include CurrentUserConcern

  def create
    user = User.find_by(email: params["user"]["email"]).try(:authenticate, params["user"]["password"])

    if user
      session[:user_id] = user.id 
      render json: {
        status: :created,
        logged_in: true,
        user: user
      }
    else
      render json: { status: 401 }
    end
  end

  def logged_in
    if @current_user
      render json: {
        logged_in: true,
        user: @current_user
      }
    else
      render json: {
        logged_in: false
      }
    end
  end

  def logout
    reset_session
    render json: { 
      status: 200, 
      logged_out: true 
    }
  end
end
Enter fullscreen mode Exit fullscreen mode

Add the user's current worry.
Create a new file current user concern.rb in the app/controllers/concerns folder and add the following code to it.


module CurrentUserConcern
  extend ActiveSupport::Concern

  included do
    before_action :set_current_user
  end

  def set_current_user
    if session[:user_id]
      @current_user = User.find(session[:user_id])
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

The current user concern variable will be used to identify logged-in app users. We made it a worry so that other controllers could access it. Continue reading.

Editing the User Model
Add the line has secure password to app/models/user.rb. That line is required for bcrypt to encrypt our user passwords.

Add validation to the model as well

class User < ApplicationRecord
  has_secure_password
  validates_presence_of :email, :password
  validates_uniqueness_of :email
end
Enter fullscreen mode Exit fullscreen mode

Update our routes

Rails.application.routes.draw do
  resources :sessions, only: [:create]
  resources :users

  delete :logout, to: "sessions#logout"
  get :logged_in, to: "sessions#logged_in"

  root to: "static#home"
end
Enter fullscreen mode Exit fullscreen mode

Let's make the registrations controller for new user registration.
Let's provide our new users the chance to register now that our software is operating as planned.

Create the registrations controller.rb file in app/controllers.

Registrations for rails g controller

class RegistrationsController < ApplicationController
  def create 
    user = User.create!(
      email: params["user"]["email"],
      password: params["user"]["password"],
      password_confirmation: params["user"]["password_confirmation"]
    )

    if user 
      session[:user_id] = user.id 
      render json: {
        status: :created,
        user: user
      }
    else
      render json: { status: 500 }
    end
  end
ens
Enter fullscreen mode Exit fullscreen mode

Update the Application controller
Open app/controllers/application_controller.rb and change it to

class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token
end
Enter fullscreen mode Exit fullscreen mode

The line skip before action:verify authenticity token disables the controllers' forgery prevention. This is critical for resolving the ActionController:: Exceptions for InvalidCrossOriginRequest. More information may be found here.

The Rails portion of the application is now complete. Let's perform one more test by creating a user from the console to ensure that our model is functioning properly.

Image description

App in Reactjs
It's time to develop the Reactjs app and connect it to our Rails backend now that we've finished configuring the Rails API.

Prerequisites
Before proceeding, make sure you have the following installed and functioning on your computer.

Nodejs. Installers for Ubuntu may be found here.

Make a react app

Run npx create-react-app name-of-app . This creates a new react app with the name chosen.

Run npm start to ensure everything is working properly. If you built your app correctly, you should see this.

Image description

Create components
Let's install some packages that we'll use later before we start building our first component.

Run npm install --save react-route react-router react-router-dom axios

React-route will be used to generate our routes, axios will be used to communicate with our Rails API, and react-router and react-router-dom will be used to render the matching Route>.

Open src and make two new folders called components and auth. Create a new file registration and move app.js to components. Add the following code to js in the auth folder.

Open src and make two new folders called components and auth. Create a new file registration and move app.js to components. Add the following code to js in the auth folder:

import React, { Component } from "react";
import axios from "axios";
import { Link } from "react-router-dom";

export default class Registration extends Component {
  constructor(props) {
    super(props);

    this.state = {
      email: "",
      password: "",
      password_confirmation: "",
      registrationErrors: ""
    };
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  handleSubmit(event) {
    const {
      email,
      password,
      password_confirmation
    } = this.state;
    axios
      .post(
        "http://localhost:3001/users",
        {
          user: {
            email: email,
            password: password,
            password_confirmation: password_confirmation
          }
        },
        { withCredentials: true }
      )
      .then(response => {
        if (response.data.status === "created") {
          console.log("Registration data", response.data)
        }
      })
      .catch(error => {
        console.log("registration error", error);
      });

    event.preventDefault();
  }

  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <input
              className="form-control"
              type="email"
              name="email"
              placeholder="Email"
              required
              value={this.state.email}
              onChange={this.handleChange}
            />
          </div>

          <div className="form-group">
            <input
              className="form-control"
              type="password"
              name="password"
              placeholder="Password"
              required
              value={this.state.password}
              onChange={this.handleChange}
            />
          </div>

          <div className="form-group">
            <input
              className="form-control"
              type="password"
              name="password_confirmation"
              placeholder="Password Confirmation"
              required
              value={this.state.password_confirmation}
              onChange={this.handleChange}
            />
          </div>

          <button type="submit" className="btn btn-primary btn-sm">
            Register
          </button>
          <p>
            Have an account? <Link to="/">Login</Link>
          </p>
        </form>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

To begin, we construct a state object containing the parameters we require from a user in order to register successfully. Only the email, password, and password confirmation parameters are supported by our API.

Our handleSubmit method will use axios to send a POST request to the API when the form is submitted. If the user's credentials are legitimate, a successful login will transport them to the dashboard, and their login status will be updated to logged in.

The login.js component should now be added to the auth subdirectory.

import React, { Component } from "react";
import axios from "axios";

export default class Login extends Component {
  constructor(props) {
    super(props);

    this.state = {
      email: "",
      password: "",
      loginErrors: ""
    };
    this.handleSubmit = this.handleSubmit.bind(this);
    this.handleChange = this.handleChange.bind(this);
  }

  handleSubmit(event) {
    const { email, password } = this.state;
    axios
      .post(
        "http://localhost:3001/sessions",
        {
          user: {
            email: email,
            password: password
          }
        },
        { withCredentials: true }
      )
      .then(response => {
        if (response.data.logged_in && response.data.patient) {
          this.props.handleSuccessfulAuth(response.data);
        } else {
          this.props.handleSuccessfulDoctorAuth(response.data);
        }
      })
      .catch(error => {
        console.log("login error", error);
      });

    event.preventDefault();
  }

  handleChange(event) {
    this.setState({
      [event.target.name]: event.target.value
    });
  }

  render() {
    return (
      <div>
        <form onSubmit={this.handleSubmit}>
          <div className="form-group">
            <input
              className="form-control"
              type="email"
              name="email"
              placeholder="Email"
              required
              value={this.state.email}
              onChange={this.handleChange}
            />
          </div>
          <div className="form-group">
            <input
              className="form-control"
              type="password"
              name="password"
              placeholder="Password"
              required
              value={this.state.password}
              onChange={this.handleChange}
            />
          </div>
          <button type="submit" className="btn btn-primary btn-sm">
            Login
          </button>
        </form>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We have our login and registration components ready at this stage. So let's get started.

The Dashboard and Home components
For unauthenticated users, we require a home component, and for authorized users, we need a dashboard component.

A login form and a link to register will be on the main page. After logging in, users are brought to the dashboard, where they can logout.

Add home.js to src/components to build the home component.

import React, { Component } from "react";
import Login from "./auth/Login";
import { Link } from "react-router-dom";

export default class Home extends Component {
  constructor(props) {
    super(props);
    this.handleSuccessfulAuth = this.handleSuccessfulAuth.bind(this);
  }

  handleSuccessfulAuth(data) {
    this.props.handleLogin(data);
    this.props.history.push("/dashboard");
  }

  render() {
    return (
      <div>
        <h1>Home</h1>
        <h1>Status: {this.props.loggedInStatus}</h1>
        <Login
          handleSuccessfulAuth={this.handleSuccessfulAuth}
        />
        <p>
          Don't have an account? <Link to="/registration">Register</Link>
        </p>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

The App component
The App component is the topmost (parent) component.

import React, { Component } from "react";
import { BrowserRouter, Switch, Route } from "react-router-dom";
import Dashboard from "./dashboard";
import Home from "./home";
import axios from "axios";
import "bootstrap/dist/css/bootstrap.min.css";
import Registration from "../auth/Registration";

export default class App extends Component {
  constructor() {
    super();

    this.state = {
      loggedInStatus: "NOT_LOGGED_IN"
    };

    this.handleLogin = this.handleLogin.bind(this);
    this.handleLogout = this.handleLogout.bind(this);
    this.handleSuccessfulAuth = this.handleSuccessfulAuth.bind(this);
  }

  checkLoginStatus() {
    axios
      .get("http://localhost:3001/logged_in", { withCredentials: true })
      .then(response => {
        if (
          response.data.logged_in &&
          this.state.loggedInStatus === "NOT_LOGGED_IN"
        ) {
          this.setState({
            loggedInStatus: "LOGGED_IN"
          });
        } else if (
          !response.data.logged_in &&
          this.state.loggedInStatus === "LOGGED_IN"
        ) {
          this.setState({
            loggedInStatus: "NOT_LOGGED_IN"
          });
        }
      })
      .catch(error => {
        console.log("login error", error);
      });
  }

  handleSuccessfulAuth(data) {
    this.handleLogin(data);
  }

  componentDidMount() {
    this.checkLoginStatus();
  }

  handleLogin(data) {
    this.setState({
      loggedInStatus: "LOGGED_IN"
    });
  }

  handleLogout() {
    this.setState({
      loggedInStatus: "NOT_LOGGED_IN"
    });
  }

  render() {
    return (
      <div className="app">
        <BrowserRouter>
          <Switch>
            <Route
              exact
              path={"/"}
              render={props => (
                <Home
                  {...props}
                  loggedInStatus={this.state.loggedInStatus}
                  handleLogin={this.handleLogin}
                  handleLogout={this.handleLogout}
                />
              )}
            />
            <Route
              exact
              path={"/dashboard"}
              render={props => (
                <Dashboard
                  {...props}
                  loggedInStatus={this.state.loggedInStatus}
                />
              )}
            />
            <Route
              exact
              path={"/registration"}
              render={props => (
                <Registration
                  handleSuccessfulAuth={this.handleSuccessfulAuth}
                  {...props}
                  loggedInStatus={this.state.loggedInStatus}
                />
              )}
            />
          </Switch>
        </BrowserRouter>
      </div>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

At this stage, we must test our app to check how it functions.

Let's get the servers up and running: reactjs server; npm start This will run the program on port 3000 automatically.

rails s -p 3001 rails server

Home/Login Page
Let's utilize the user credentials we established while we were testing the user model to log in.

Top comments (1)

Collapse
 
nguyentungson0503 profile image
NguyenTungSon0503

thank you so much for this tutorial. But i have a question, in the app.js file, i saw ./dashboard but in the tutorial i dont see it. Can you update the source code of it? Thank you!