Hot Glue Example #2 – Users and their Widgets

Example 2: Authentication and Ownership

In this example app, I’ll cover how to set up Devise for authentication, and specify access control for your object using the logged-in user. To do this, you’ll want to set up a new app. This app is also very simple. It will contain only two tables: Users and Widgets.

A User has_many :widgets, and a Widget belongs_to :user.

Authentication, Authorization, and Access Control

Before we begin, let’s make sure we understand some basic web app terminology.

Authentication β€” The process where the computer (website) makes sure you are who you say you are.

Authorization β€” The idea that the user is allowed to access this website at all, or this part of the website.

Access Control β€” The idea that specific objects (like, objects in your database) are granted access (reading/writing) to some people and not to others.

Although these 3 terms of often confused, specifically because when we discuss these concepts in app development they are commingled, they have three distinct meanings.

Authentication is typically what we mean when we say “login.” By verifying the user’s username & password, we know that they are who they say they are. (At least, that they have the correct password.)

Authorization is often best thought of as a “public” part of your website which has no authorization (because any anonymous user can load the content) vs. an admin area where only admins are authorized to look at that part of the website.

If I have access to a part of the website, then the question becomes which objects I can access. In this example, users will log-in and create widgets. Another user, which we will simulate in an incognito window, will login and the 2nd user will not be able to see the 1st user’s widgets. The fact that the 1st and 2nd users can see only their own widgets and not each others is called access control.

Start with:

rails new WidgetsApp --database=postgresql

Now go through all Steps 4 in the Getting Started above.

After you install Devise, there’s one final step that the setup docs leave out: You have to actually create a user and tell Devise to know how to log that user in.

Remember, you’ve just run this command: rails generate devise:install (The last command in the Setup). That’s the Devise installer. Now you must create a User and also tell Devise to attach its fields to your user object. (Several fields are attached by default). You can use any name for “user” you likeβ€” account, person, persona, customer, etc. This will be the primary way people will log-in, so think about the mental model of what you want your authentication to be called.

Most apps use “user”; I often write my apps using the word “account.” In this example, I will use User.

Devise Setup

Let’s create the User table. The first migration will create the basic table. Be sure not to add the field email, which will be added by the second Devise migation.

From the Devise Rails setup, follow these steps:

rails generate model User first_name:string last_name:string

Notice we have a User model, a migration to create the users table, specs and a factory.

Then run:

rails generate devise User

Devise has created a second migration for the Users table,

Devise’s migration is quite large, and is worth looking at because it shows you the many great features that Devise comes without of the box. Note that every line below that begins with # is commented-out, meaning it won’t run when the migration runs. If you want the associated devise features (for example, to make your user accounts trackable, confirmable, or lockable, you’ll want to uncomment these lines now before running the migration. If you leave them commented, you can always create a migration later to add these feilds.)

# frozen_string_literal: true

class AddDeviseToUsers < ActiveRecord::Migration[6.1]
def self.up
change_table :users do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""

## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at

## Rememberable
t.datetime :remember_created_at

## Trackable
# t.integer :sign_in_count, default: 0, null: false
# t.datetime :current_sign_in_at
# t.datetime :last_sign_in_at
# t.string :current_sign_in_ip
# t.string :last_sign_in_ip

## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable

## Lockable
# t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at


# Uncomment below if timestamps were not included in your original model.
# t.timestamps null: false
end

add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
# add_index :users, :confirmation_token, unique: true
# add_index :users, :unlock_token, unique: true
end

def self.down
# By default, we don't want to make any assumption about how to roll back a migration when your
# model already existed. Please edit below which fields you would like to remove in this migration.
raise ActiveRecord::IrreversibleMigration
end
end

Go to app/models/user.rb

Notice that devise has installed a macro onto the class which enables log-in/log-out functionality for this model. (As with the optional fields in the migration, the additional features are shown by default by the devise installer as commented out too.)

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
end

Let’s add some code for Hot Glue. Because Hot Glue needs any model to have a special field for its label, we’ll add a name method that will concatenate first & last names which are columns on the database. (If we had specified

The 5 magic names that Hot Glue will recognize are

 1) name, 2) to_label, 3) full_name, 4) display_name, or 5) email

If you have either a method name on your class (model object) OR a column name on your database table, Hot Glue will treat that as the label when displaying this record in drop-down lists and other places.

Remember, the order chosen is pre-determined. If you have both name and full_name, name will be used because it comes first in the list above. That means Hot Glue always prefers name first, and then to_label.

In this case, it turns out this step is not necessary if we had wanted to use email instead, Devise has installed it for us onto our User model in the 2nd migration.

Add the code shown in orange to the User model.

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  
  def name 
    "#{first_name} #{last_name}"
  end
  
  has_many :widgets
end

Now let’s create the Widgets.

rails generate model Widget user_id:integer name:string

Go to the app/models/widget.rb file and add a relationship to User.

class Widget < ApplicationRecord
  belongs_to :user
end

In this example app, we will not build any scaffolding for Users. Instead, Devise already comes with the sign up, log-in, and log-out screens we need.

Instead, we will simply log in and log out with Devise and create new widgets.

As well, our widget interface will be the only thing that happens. Open routes.rb and edit it like so

Rails.application.routes.draw do
  devise_for :users

  resources :widgets
  root to: redirect("/widgets")
end

Now, generate the Hot Glue scaffold:

rails generate hot_glue:scaffold widget 

Make to run rails db:create and rails db:migrate

Let’s see what happens when we go to https://localhost:3000/widgets

Even though I typed /widgets into my browser bar, this controller is protected by authentication and I’m not yet logged in. I get redirect to /users/sign_in and I see a message telling me I need to sign-in before continuing.

Remember, this functionality comes along with Devise β€” all of it has already been implemented for you.

Here I’m using the dark_knight theme provided by Hot Glue (see Step 6).

Since there is no users in the database yet, go ahead and use the Sign Up link.

(Alternatively, you can open the rails console and create a new user with User.create(email: "nonone@nowhere", password: "password", password_confirmation: "password") )

Now Devise will give you an ugly crash:

Geez that’s ugly! This happens because we need to define a special method on the ApplicationController to tell Devise where to send the user after they sign-up. Add this code to application_controller.rb

class ApplicationController < ActionController::Base
  def after_sign_in_path_for(resource)
    widgets_path
  end
end

IMPORTANT: Devise currently has serious compatibility issues with Turbo Rails. In particular, your log-in screens do not work out of the box. Follow the next step to fix them.

Manually port the Devise views into your app with

rails generate devise:views

Edit devise/registrations/new and devise/sessions/new, devise/passwords/new and devise/confirmations/new modifying all four templates like so:

form_for(resource, as: resource_name, url: session_path(resource_name) ) do |f|

change it to

form_for(resource, as: resource_name, html: {'data-turbo' => "false"}, url: session_path(resource_name) ) do |f|

This tells Devise to fall back to non-Turbo interaction for the log-in and registration. For the rest of the app, we will use Turbo Rails interactions.

Finally, add a Logout button to our application.html file (code shown in orange below).

<!DOCTYPE html>
<html>
  <head>
    <title>WidgetsApp</title>
    <meta name="viewport" content="width=device-width,initial-scale=1">
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
  <% if current_user %>
    <%= button_to "Log Out", destroy_user_session_path, method: :delete, 'data-turbo': false%>
  <% end %>
<%= render partial: 'layouts/flash_notices' %>

    <%= yield %>
  </body>
</html>

Once you have completed the steps above, you should now be able to sign up, log in, and log-out without problems. (If you fail to follow the steps to disable Turbo Rails in the Devise views, you will encounter several issues with logging in and logging out.)

We can log in, log out. User 1 (noone@nowhere.com) can create widgets owned only by them; User 2 (noone2@nowhere.com) can’t see User 1’s widgets and vice-versa.

And finally we see the power of Hot Glue’s authentication: Hot Glue has automatically detected that the Widgets belong to users, and it has installed access control on them for you. If your authentication model isn’t called User, you want to use Hot Glue’s --auth flag (see docs for details). But because we used the model name “User,” Hot Glue assumed this was how your visitors authenticated to the website. From there, Hot Glue has assumed that all Widgets are owned by the user who is logged in and given those users read, update, and delete access to them. Also, a special create action exists (at the proper placeβ€” the POST http verb of the /widgets endpoint) that will automatically set the user_id of the newly created widget to the current user’s id. You can see this at the top of the create action in the widgets_controller.rb.

def create
modified_params = modify_date_inputs_on_params(widget_params.dup.merge!(user: current_user ) , current_user)
@widget = Widget.create(modified_params)

Since this controller is intended to be used by users only, this makes sense. If you didn’t want the users to be able to make their own new widgets (for example, just to edit & update them), you would use the --no-create flag. Likewise, if you want to make a controller that doesn’t have delete use --no-delete. You can also use --no-edit when you want to not allow viewing/editing/update (although the LIST is still output). But note that if you use --no-edit along with --magic-buttons the magic buttons will override the no-edit, creating an update action used for the magic buttons only but there will be no ‘Edit’ button on the lines on the list view. (So from the user’s perspective, there is “no edit” button even though there are magic buttons.)

If you instead didn’t want this user to own this object in the context of this controller, then you’d have two options:

  1. use a --gd controller as in the Example 1.
  2. Implement your own access control

By default, when not creating –gd controllers, Hot Glue will impose this access control model on all objects. That is, to interact with the Widgets controller, the user must be logged in and own the widget itself (the user_id on the widget is set to the user’s id). For simple small apps, this is called “poor man’s authentication,” but it is not appropriate if you want granularity in access control (for example, contextual rules within the access control rules). For apps like these, I suggest you mix the --auth and --auth-identifier flags and implement your own access control methods. They can easily replace or augment the existing code, or live in the automatically created base_controller.rb which you will find created alongside every controller. (The examples on this page do not cover a non-standard access control implementation.)

Check out all of the generated code in app/controllers/widgets_controller.rb and make sure you understand it. The code in that controller behaves as described here. You should not use the glue blindly: instead, read the code, learn from it and work within Hot Glue’s lego-like structures. The structures are in place to encourage you to write secure, safe, fast, and performant apps but you will need to understand Rails when things get a little more complicated.

If Widgets had any child objects, those too would automatically be enforced with access control: Hot Glue would make sure that the child’s parent Widget belonged to the user who is logged in.

Example 2 App

You can get Hot Glue one of two ways: 1) Get the complete course on Teachable or 2) buy a Hot Glue license directly from my site.