Rails 7.1 authentication from scratch

add rails 7.1 authentication without using a gem like devise
Icons/chart bar
Used 29 times
Created by
M Mathias Karstädt

Usage
It is best used within a clean new rails 7.1 project.
Commit all of your own changes before using this!
The template has to be run before you created any user models as this will generate a user model for you.

It will:
  • create a user model
  • a current model
  • an authentication concern
  • a user_sessions controller
  • a users_controller

To reverse all the changes run:
git restore .
git clean -df # this will delete all non commited files!!!
bin/rails db:reset # will reset everything in your database!!!

inspired by:
  • https://github.com/stevepolitodesign/rails-authentication-from-scratch
  • https://dev.to/kevinluo201/building-a-simple-authentication-in-rails-7-from-scratch-2dhb
  • https://gorails.com/episodes/rails-7-1-authentication-from-scratch

Run this command in your Rails app directory in the terminal:

rails app:template LOCATION="https://railsbytes.com/script/V33s2E"
Template Source

Review the code before running this template on your machine.

# add needed gems
gem('bcrypt')
run('bundle install')

# generates user model
generate(:model, 'user username email password_digest password_confirmation')
rails_command('db:migrate')

# adds authentication to user model
inject_into_file 'app/models/user.rb', after: "class User < ApplicationRecord\n" do
  <<ONE
  has_secure_password :password, validations: true
  validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }, presence: true, uniqueness: true
  normalizes :email, with: ->(email) { email.strip.downcase }
ONE
end

# create signup page
generate(:controller, 'users index new create', '--skip-routes')
route('resources :users, only: [:index, :new, :create]')
route("root 'users#index'")
file 'app/controllers/users_controller.rb', <<~TWO
  class UsersController < ApplicationController
    before_action :authenticate_user!, only: [:index]

    def index
      @users = User.all
    end

    def new
      @user = User.new
    end

    def create
      @user = User.new(user_params)

      if @user.save
        flash[:notice] = "User created successfully"
        redirect_to users_path
      else
        flash[:alert] = "User not created"
        render :new, status: :unprocessable_entity
      end
    end

    private

    def user_params
      params.require(:user).permit(:username, :email, :password, :password_confirmation)
    end
  end
TWO

file 'app/views/users/index.html.erb', <<~THREE
  <h1>Users#index</h1>

  <%= link_to 'New User', new_user_path %>

  <table>
    <thead>
      <tr>
        <th>id</th>
        <th>username</th>
        <th>email</th>
      </tr>
    </thead>
    <tbody>
      <% @users.each do |user| %>
        <tr>
          <td><%= user.id %></td>
          <td><%= user.username %></td>
          <td><%= user.email %></td>
        </tr>
      <% end %>
    </tbody>
  </table>

  <%= button_to "Logout", user_session_path(id: current_user.id), method: :delete %>
THREE

file 'app/views/users/new.html.erb', <<~FOUR
  <h1>Users#new</h1>
  <%= form_with model: @user do |f| %>
    <% if @user.errors.any? %>
      <div>
        <ul>
          <% @user.errors.full_messages.each do |message| %>
            <li><%= message %></li>
          <% end %>
        </ul>
      </div>
    <% end %>
    <div>
      <%= f.label :username %><br>
      <%= f.text_field :username, required: true %>
    </div>
    <div>
      <%= f.label :email %><br>
      <%= f.text_field :email, required: true %>
    </div>
    <div>
      <%= f.label :password %><br>
      <%= f.password_field :password, required: true %>
    </div>
    <div>
      <%= f.label :password_confirmation %><br>
      <%= f.password_field :password_confirmation, required: true %>
    </div>
    <p>
      <%= f.submit %>
    </p>
  <% end %>
FOUR

inject_into_file 'app/views/layouts/application.html.erb', after: "  <body>\n" do
  <<~FIVE
    <% flash.each do |type, msg| %>
      <div>
        <%= msg %>
      </div>
    <% end %>
  FIVE
end

# create current model
file 'app/models/current.rb', <<~CURRENT
  class Current < ActiveSupport::CurrentAttributes
    attribute :user
  end
CURRENT

# create authentication concern
file 'app/controllers/concerns/authentication.rb', <<~AUTHCONCERN
  module Authentication
    extend ActiveSupport::Concern

    included do
      before_action :current_user
      helper_method :current_user
      helper_method :user_signed_in?
    end

    def login(user)
      reset_session
      session[:current_user_id] = user.id
    end

    def logout
      reset_session
    end

    def redirect_if_authenticated
      redirect_to root_path, alert: "You are already logged in." if user_signed_in?
    end

    def authenticate_user!
      redirect_to new_user_session_path, alert: "You need to login to access that page." unless user_signed_in?
    end

    private

    def current_user
      Current.user ||= session[:current_user_id] && User.find_by(id: session[:current_user_id])
    end

    def user_signed_in?
      Current.user.present?
    end
  end
AUTHCONCERN

# create sign in page for users
generate(:controller, 'user_sessions new create', '--skip-routes')
route('resources :user_sessions, only: [:new, :create, :destroy]')
file 'app/controllers/user_sessions_controller.rb', <<~SIX
  class UserSessionsController < ApplicationController
    before_action :authenticate_user!, only: [:destroy]

    def new
      @user = User.new
    end

    def create
      if @user = User.authenticate_by(email: params[:user][:email], password: params[:user][:password])
        login @user
        redirect_to root_path, notice: "Signed in."
      else
        flash[:alert] = "Login failed"
        redirect_to new_user_session_path
      end
    end

    def destroy
      logout
      redirect_to root_path, notice: "Signed out."
    end
  end
SIX

file 'app/views/user_sessions/new.html.erb', <<~SEVEN
  <h1>Login page</h1>
  <%= form_with model: @user, url: user_sessions_path do |f| %>
    <div>
      <%= f.label :email %><br>
      <%= f.text_field :email %>
    </div>
    <div>
      <%= f.label :password %><br>
      <%= f.password_field :password %>
    </div>
    <p>
      <%= f.submit 'Login' %>
    </p>
  <% end %>
SEVEN

inject_into_file('app/controllers/application_controller.rb',
                 after: "class ApplicationController < ActionController::Base\n") do
  <<~EIGHT
    include Authentication
  EIGHT
end
Comments

Sign up or Login to leave a comment.