πŸͺ„Magic Link Signup

Who likes passwords? Easily setup magic link in your LightningRails app.

If you want to increase your signups, ditching the password is the best way.

Before starting with this setup, make sure that transactional emails are ready and set, follow the LightningRails guide here. With Postmark set up, you will get 100 free emails per month.

Config

If it hasn't been done yet, make sure that the environment is set to the correct email delivery method:

#Β config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
#config/environments/production.rb
config.action_mailer.delivery_method = :postmark

Model

To send Magic links, generate an instance of a magic Link with a unique token, think of it as an invitation instance:

rails g model email_link token expires_at:datetime user:references

Migrate:

rails db:migrate

In your model file email_link.rb

class EmailLink < ApplicationRecord
  belongs_to :user
  after_create :send_mail
  
  def self.generate(email)
    user = User.find_by(email: email)
    if !user
      user = User.create(email: email, password: Devise.friendly_token.first(8))
    end
    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
    # Defined in mailer class
    EmailLinkMailer.sign_in_mail(self).deliver_now
  end
end

Generate Mailer

In the model we are creating a send_mail method, now we define it in our mailer method:

rails g mailer email_link

This should generate email_link_mailer.rb a few other files, in this mailer file, add this code:

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! πŸš€" # modify the subject to your liking
  end
end

Let's create the view file:

touch app/views/email_link_mailer/sign_in_mail.html.erb

Now let's write a nice welcome email, no need to go above the board, the user just needs a link to sign up:

# app/views/email_link_mailer/sign_in_mail.html.erb

<p>Hello, <%= @user.email %>!</p>
<p>Someone requested a sign-in link for your account. Click the link below to sign in:</p>

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

Cheers,
Bob, Founder

Route

We will need new routes to make this work.

  post 'email_links/create', as: :magic_link
  get 'email_links/validate', as: :email_link

Controller

We will also need a new controller.

rails g controller email_links

In the controller, we will add these methods. One method to create the new link, and one to validate that the link is correct, the latter will be linked directly in the email.

class EmailLinksController < ApplicationController
  skip_before_action :authenticate_user!

  def create
    @email_link = EmailLink.generate(params[:email])
    if @email_link
      flash[:notice] = "Access email sent! Please, check your inbox to enter"
      redirect_to root_path
    else
      flash[:alert] = "There was an error, please try again!"
      redirect_to new_user_session_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_user_session_path
    end

    sign_in(email_link.user, scope: :user)
    flash[:notice] = "Welcome! You signed in successfully πŸš€"
    redirect_to root_path
  end
end

Views

The last thing we have to do is go to the new sessions path and replace the existing form with this new one:

 # app/views/devise/sessions/new.html.erb

 <%= form_with(url: magic_link_path, method: :post) do %>
     <div class="col-span-6">
          <label class="input input-bordered flex items-center gap-2">
            <svg
              xmlns="http://www.w3.org/2000/svg"
              viewBox="0 0 16 16"
              fill="currentColor"
              class="h-4 w-4 opacity-70">
              <path
                d="M2.5 3A1.5 1.5 0 0 0 1 4.5v.793c.026.009.051.02.076.032L7.674 8.51c.206.1.446.1.652 0l6.598-3.185A.755.755 0 0 1 15 5.293V4.5A1.5 1.5 0 0 0 13.5 3h-11Z" />
              <path
                d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
            </svg>
            <%= email_field_tag :email, nil, placeholder: "mclovin@gmail.com" %>
          </label>
          <div class="col-span-6 sm:flex sm:items-center sm:gap-4 mt-6">
            <%= submit_tag "Log in", class: "btn btn-primary" %>
          </div>
      </div>
  <% end %>

This form will work for both Signup and Login, so no need to do the same setup in registrations. Just change all the signup links to redirect to sessions/new by default.

The reason I recommend adding the sessions new instead of registration is that the authenticated_user method redirects to sessions, so you can reuse that feature with your new magic link πŸš€

Don't forget to restart your server as we modified the config files πŸ˜‰

Happy Magic linking! πŸ¦„

Last updated