Magic Links with Ruby On Rails and Devise
Tags: ruby, rails
Published: 2020-04-18 21:47:35

Passwords are a problem for users. A good practice to skip this is by sending an access link to your account by email, that link has a token that we will use to validate and finally, sign in to the user to their account. Next, I'm going to show you an implementation in Ruby On Rails 6 with Devise.

**NOTE**: I am going to create an application from 0 for avoid mistakes.

### Setting up
Create the project: `$ rails new magicLinks -T --skip-turbokinks --database=postgresql`

####Installing dependencies...
Add this to your `Gemfile.rb`
```ruby
gem 'devise', '~> 4.7', '>= 4.7.1'
group :development, :test do
  gem 'letter_opener', '~> 1.7' // For open emails in the browser
end
```

Then, `$ bundle install`
**Let's install Devise:** with `$ rails g devise:install && rails g devise User && rails db:create && rails db:migrate`

```ruby
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.delivery_method = :letter_opener
```

Devise asks us for a root path, let's do that: `$ rails g controller welcome index`.
Add this in your `app/config/routes.rb`:
```ruby
root 'welcome#index'
```

### Let's do it 👊
First, we need to create a table for save the generated tokens on our db.
So, `$ rails g model EmailLink token expires_at:datetime user:references && rails db:migrate`

Now we are going to generate the token and send the email when this happens, for that, in `app/models/email_link.rb`:
```ruby
class EmailLink < ApplicationRecord
  belongs_to :user
  after_create :send_mail

  def self.generate(email)
    user = User.find_by(email: email)
    return nil if !user

    create(user: user, expires_at: Date.today + 1.day, token: generate_token)
  end

  def self.generate_token
    Devise.friendly_token.first(16)
  end

  private
  def send_mail
    EmailLinkMailer.sign_in_mail(self).deliver_now
  end
end
```

Let's generate the controller to trigger the previous callback when the user gives us their email and sends the form, `rails g controller EmailLinks new create`.

Let's add the necessary routes in our `app/config/routes.rb`.
```ruby
root 'welcome#index'
get 'email_links/new', as: :new_magic_link
post 'email_links/create', as: :magic_link
get 'email_links/validate', as: :email_link
```

Now, let's give functionality to our controller:
```ruby
class EmailLinksController < ApplicationController
  def new

  end

  def create
    @email_link = EmailLink.generate(params[:email])

    if @email_link
      flash[:notice] = "Email sent! Please, check your inbox."
      redirect_to root_path
    else
      flash[:alert] = "There was an error, please try again!"
      redirect_to new_magic_link_path
    end
  end

  def validate
    email_link = EmailLink.where(token: params[:token]).where("expires_at > ?", DateTime.now).first

    unless email_link
      flash[:alert] = "Invalid or expired token!"
      redirect_to new_magic_link_path
    end

    sign_in(email_link.user, scope: :user)
    redirect_to root_path
  end
end
```

At this point, our program is capable of generating the token, validating it and logging in. What we have left is to send the email and the view where the user gives us their email.

**NOTE**: If you want to see these alerts, you must add them in `app/views/layouts/application.html.erb`, inside the body tag:
```html
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>
```

####Sending the email
Very easy, first we are going to generate our mailer, `$ rails g mailer EmailLinkMailer` and then we give it functionality:
```ruby
class EmailLinkMailer < ApplicationMailer
  def sign_in_mail(email_link)
    @token = email_link.token
    @user = email_link.user

    mail to: @user.email, subject: "Here is your magic link! 🚀"
  end
end
```

Let's edit our email (`app/views/email_link_mailer/sign_in_mail.html.erb`) a little:
```html
<p>Hello, <%= @user.email %>!</p>
<p>Recently someone requested a link to enter your account, if it was you, just press the button below to log in</p>

<%= link_to "Sign in to my account", email_link_url(token: @token) %>
```

Brilliant! At this point our program is already capable of sending the email, we just need the main view, where the user will give us their email and we can fire all this backend.

Just add this simple form in `app/views/email_links/new.html.erb`:
```html
<%= form_with(url: magic_link_path, method: :post) do %>
  <%= label_tag :email, "E-mail address" %>
  <%= email_field_tag :email, nil, placeholder:"carpintinimatias@gmail.com", autofocus: true, required: true %>
  <%= submit_tag "Send!" %>
<% end %>
```

To validate on the front that this works, we can add this dummy example in `app/views/welcome/index.html.erb`:
```html
<% if user_signed_in? %>
  <p>Hello, <%= current_user.email %></p>
  <%= link_to "Sign out", destroy_user_session_path, method: :delete %>
<% else %>
  <p>Hey, Sign In with Magic Links!</p>
  <%= link_to "Click here", new_magic_link_path %>
<% end %>
```

All right, that's it. Thanks for reading. 👋