Tutorial Adding Authorization and Flash Messages to Inertia Rails app

Updated to reflect changes in InertiaJS 1.0

In our previous tutorial, we learned how to add authentication to our IRVST app.

In today's tutorial we will be adding authorization (restrict pages to logged in users) to our new app. And while we are at it, we will add a flash message component to our app to show the authorization messages.

Add authorization in an Inertia Rails app is pretty much the same as adding authorization to a normal Rails app. We will use Pundit as our authorization gem because of its simplicity and flexibility. You can use any authorization gem you prefer, for example CanCanCan, as long as they work with Rails.

Add Pundit to the app

This part is exactly the same as adding Pundit to any Rails app.

Add Pundit gem to the Gemfile

gem 'pundit'

Then install the gem

bundle install

Include Pundit and supply auth failed action to application controller.
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit::Authorization
  ...

  rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

  private

  def user_not_authorized
    redirect_to request.referrer || root_path, alert: "You are not authorized to perform this action."
  end
end

Then, we can run the generator to create a default policy for us.

rails g pundit:install

The command above will create a sample policy file for us in this path:

app/policies/application_policy.rb

We will update this sample a little bit to handle the exception when an auth error happened.
app/policies/application_policy.rb

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    raise Pundit::NotAuthorizedError, "must be logged in" unless user

    @user = user
    @record = record
  end

  ...

  class Scope
    def initialize(user, scope)
      raise Pundit::NotAuthorizedError, "must be logged in" unless user

      @user = user
      @scope = scope
    end
  ...
  end
end

And that's it. We should now be able to use authorization policies in our app.

Create policy for Articles

Create the policy file for our Article model.
app/policies/article_policy.rb

class ArticlePolicy < ApplicationPolicy

  def create?
    user.admin?
  end

  def index?
    true
  end

  def show?
    true
  end

  def update?
    user.admin?
  end

  def destroy?
    user.admin?
  end

end

Because in our base policy we have denied access to all actions (which is a good default in general), we will allow the index and show action to any visitors here.

Next step we will add authorization checks to the articles controller.

Remember in our previous tutorial, we have added a user logged in check to our articles controller? We can now safely remove it because we have moved the user login check to Pundit policy now.

app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  # remove the auth functions we added in our previous tutorial
  # include Auth
  # skip_before_action :authenticate_user!, only: %i[ index show ]

  # notice we also removed show action from set_article before action
  # this is because we want to allow visitors not logged in to be able to view the show page
  before_action :set_article, only: %i[ edit update destroy ]
  # add policy check
  after_action :verify_authorized, except: %i[ index show ]

The verify_authorized means we have promised we will perform a policy check in all actions except the listed index and show actions.

So, as promised, we will add policy checks to these actions. And because we have set_article action for all actions except create, we can just perform the policy check inside the set_article action.
app/controllers/articles_controller.rb

  def create
    article = Article.new(article_params)
    # we don't run set_article for create, so we need to authorize the resource here
    authorize article
    ...
  end
  ...
  def show
    # because we removed set_article before action for show, we will add it back here
    @article = Article.find(params[:id])
    ...
  end
  ...
  private
    # Use callbacks to share common setup or constraints between actions.
    def set_article
      @article = Article.find(params[:id])

      # we authorize the resource here so all actions that depends on set_article can use it as authorized
      authorize @article
      rescue ActiveRecord::RecordNotFound
      redirect_to articles_path
    end
  ...

And that's it! Go to the pages and you should see our authorization works.

If you go to a restricted path by typing directly in the browser address bar such as localhost:3000/articles/1/edit while not logged in, you should be redirected to the home page.

Add flash message to the app

The authorization now works good and all, but we also want to tell the user they need to login when they go to a restricted area. Let's create a flash message in our app.

We will do this by creating a Rails concern.
app/controllers/concerns/inertia_flash.rb

require 'active_support/concern'

# Make flash messages available as shared data
#
module InertiaFlash
  extend ActiveSupport::Concern

  included do
    inertia_share do
      {
        has_flash: !flash.empty?,
        flash: flash.to_hash
      }
    end
  end
end

Then include the concern in application controller.
app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  include Pundit::Authorization
  include InertiaCsrf
  include InertiaFlash
  ...

Now the flash message will be transferred to the client through Inertia shared messages. We will now create the component to display the message.

frontend/components/Layouts/FlashMessages.svelte

<script>
  import { page } from '@inertiajs/svelte'

  // list color classes to prevent purge by Tailwind
  // bg-red-50
  // text-red-400
  // text-red-800
  // bg-blue-50
  // text-blue-400
  // text-blue-800
  // bg-green-50
  // text-green-400
  // text-green-800

  let color = 'green'

  $: flash = $page.props.flash
  $: show = $page.props.has_flash
  
  let msg = ''

  $: if (show) {
    if (flash.alert) {
      color = 'red'
      msg = flash.alert
    }

    if (flash.notice) {
      color = 'blue'
      msg = flash.notice
    }

    if (flash.success) {
      color = 'green'
      msg = flash.success
    }
  }
</script>

{#if show}
  <div class="mx-auto max-w-7xl rounded-md bg-{color}-50 p-4 mb-4">
    <div class="flex">
      <div class="flex-shrink-0">
        <svg class="h-5 w-5 text-{color}-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
          <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
        </svg>
      </div>
      <div class="ml-3">
        <h3 class="text-sm font-medium text-{color}-800">{msg}</h3>
      </div>
    </div>
  </div>
{/if}

Notice that we also added a check on the flash message type, and apply color for each different status respectively. We can achieve this fairly easily with Svelte.

The $: in Svelte means the assignment / statement is reactive. This means whenever one value changes, other values in the assignment / statement will be changed automatically. Creating reactivity can be quite cumbersome and verbose in Stimulus, or even in React. But in Svelte, it's as simple as declaring the variable with $:. Very nice, isn't it?

Then add the component to our layout.
frontend/layouts/Layout.svelte

<script>
  import MainNav from "../components/Layouts/MainNav.svelte";
  import FlashMessages from "../components/Layouts/FlashMessages.svelte";
</script>

<div class="bg-white">
  <MainNav />
  <FlashMessages />
  <main>
    <div class="max-w-7xl mx-auto px-4 sm:px-8">
      <slot />
    </div>
  </main>
</div>

Handling the login error

So far so good, but there is one more place we need to address: the errors on the login page.

Because the error messages on the login page are created by Devise, we will need to make failure responses compatible with Inertia's response.

We can achieve this by creating our own FailureApp for Devise:

app/services/auth_failure.rb

# Devise: Redirect user to login page on auth failure
#
class AuthFailure < Devise::FailureApp
  def http_auth?
    if request.inertia?
      # Explicitly disable HTTP authentication on Inertia
      # requests and force a redirect on failure
      false
    else
      super
    end
  end
end

Then we need to tell devise we wish to use this FailureApp:

app/config/initializers/devise.rb

  ......
  # ==> Warden configuration
  # If you want to use other strategies, that are not supported by Devise, or
  # change the failure app, you can configure them inside the config.warden block.
  #
  # config.warden do |manager|
  #   manager.intercept_401 = false
  #   manager.default_strategies(scope: :user).unshift :some_external_strategy
  # end

  # Add custom FailureApp
  config.warden do |manager|
    manager.failure_app = AuthFailure
  end
  ......

Now restart the server and you should be able to see the flash error messages for the login page is working.

And that's it. We now have a working flash message for our Inertia Rails app! 🎉

In case you need it, you can find the full code in this repository: https://github.com/planetaska/irvst-app2/tree/authorization