# Magic Link Signup

<figure><img src="https://2221554152-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FPCf6vtFuZEPHXJd7JfIe%2Fuploads%2F6AVcufNRDAWFDcScrnl0%2FScreenshot%202024-09-12%20at%2011.22.40.png?alt=media&#x26;token=d5cdabc8-0a1a-4bdd-aae1-e3d60aff064f" alt=""><figcaption><p>Magic link just needs an email, no password 🥳</p></figcaption></figure>

If you want to increase your signups, ditching the password is the best way.&#x20;

{% hint style="info" %}
Before starting with this setup, make sure that transactional emails are ready and set, [follow the LightningRails guide here.](https://docs.lightningrails.com/features-setup/automatic-emails/editor)  With Postmark set up, you will get 100 free emails per month.
{% endhint %}

### Config

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

```ruby
# config/environments/development.rb
config.action_mailer.delivery_method = :letter_opener
```

```ruby
#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:

```bash
rails g model email_link token expires_at:datetime user:references
```

Migrate:

```
rails db:migrate
```

In your model file `email_link.rb`

```ruby
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
```

In your `user.rb` file

```ruby
class User < ApplicationRecord
  has_many :email_links, dependent: :destroy
end
```

### Generate Mailer

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

```bash
rails g mailer email_link
```

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

```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! 🚀" # modify the subject to your liking
  end
end
```

Let's create the view file:

```bash
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:

```ruby
# 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.

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

### Controller

We will also need a new controller.

```bash
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.

```ruby
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:

```ruby
 # 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 🚀&#x20;

{% hint style="info" %}
Don't forget to restart your server as we modified the config files 😉
{% endhint %}

Happy Magic linking! 🦄
