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
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
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
:
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
:
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
.
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:
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:
<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:
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:
<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
:
<%= 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
:
<% 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. 👋
Top comments (7)
Hey, thank you for the article. I was wondering whether to use a gem or just build from scratch when I stumbled into your article. I'm tweaking here and there (using UUID instead of the default bigint and using it as the token itself) but mostly it's the same flow.
But, talking about your flow, I'd recommend you adding an index to your
emails_links.token
column since it's going to be searched so often.Definitely, it's a good practice!
I did the whole tutorial and put
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.delivery_method = :letter_opener
in development environment but it says "There was an error, please try again!"
Can u share me more info? When you get that error? Server logs ss would help :D
Sorry for the late response!
This's what I got!
ibb.co/By07pQz
Didn't see the error there. Last request has 200 response code, have you reproduced the error?
Insightful article! Your clear analysis and engaging writing style make complex topics accessible. I appreciate the well-researched content. Looking forward to more from your expertise! Ruby On Rails Training