BanditHijo.dev

Devise Registration Tanpa Password, Set Password Setelah Confirmation

Created at: 2021-03-13
Author by: BanditHijo

Prerequisite

ruby 3.0.0 rails 6.1.3 postgresql 12.5 rspec 4.0.0

Latar Belakang Masalah

Catatan kali ini tentang pemanfaatan Devise gem untuk registration tanpa menginputkan password, password akan diminta setelah user membuka link konfirmasi yang dikirimkan via email.

Pemecahan Masalah

Gemfile

  1. Devise, untuk authentication
  2. Simple Form, untuk menghandle form agar lebih praktis*

* Optional

Gemfile
1gem 'devise', '~> 4.7', '>= 4.7.3'
2gem 'simple_form', '~> 5.1'

Jalankan bundle install,

$ bundle install

Kemudian install kedua gem tersebut ke dalam web aplikasi.

Dahulukan simple_form, dengan begitu form-form yang akan digenerate oleh Devise akan otomatis menggunakan form dari simple_form.

$ rails g simple_form:install

Setelah itu, devise.

$ rails g devise:install

ActionMailer

Selanjutnya konfigurasi ActionMailer untuk environment development.

config/environments/development.rb
1Rails.application.configure do
2 # ...
3
4 config.action_mailer.raise_delivery_errors = false
5 config.action_mailer.perform_caching = false
6 config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
7 config.action_mailer.delivery_method = :smtp
8 config.action_mailer.smtp_settings = { address: '127.0.0.1', port: 1025 }
9
10 # ...
11end

Baris 7 dan 8, adalah configurasi untuk MailCatcher.

Untuk yang belum tahu MailCatcher, dapat dibaca di sini, Konfigurasi Ruby on Rails ActionMailer pada Local Environment dengan MailCatcher.

Devise Initializer

Kita perlu merubah beberapa konfigurasi pada Devise initializer.

config/initializer/devise.rb
1Devise.setup do |config|
2 # ...
3
4 # ==> Mailer Configuration
5 # Configure the e-mail address which will be shown in Devise::Mailer,
6 # note that it will be overwritten if you use your own mailer class
7 # with default "from" parameter.
8 config.mailer_sender = 'no-reply@example.com'
9
10 # If true, requires any email changes to be confirmed (exactly the same way as
11 # initial account confirmation) to be applied. Requires additional unconfirmed_email
12 # db field (see migrations). Until confirmed, new email is stored in
13 # unconfirmed_email column, and copied to email column on successful confirmation.
14 config.reconfirmable = false
15
16 # ...
17end

Ganti config.mailer_sender = sesuai alamat yang teman-teman inginkan.

Ganti config.reconfirmable = menjadi false.

Devise User Model

Kita akan membuat User model dengan devise.

$ rails g devise User

Buka migrationsnya, dan enablekan :confirmation_token, :confirmed_at, :confirmation_sent_at pada bagian Confirmable.

db/migrate/20210312144943_devise_create_users.rb
1# frozen_string_literal: true
2
3class DeviseCreateUsers < ActiveRecord::Migration[5.2]
4 def change
5 create_table :users do |t|
6 # ...
7
8 ## Confirmable
9 t.string :confirmation_token
10 t.datetime :confirmed_at
11 t.datetime :confirmation_sent_at
12 # t.string :unconfirmed_email # Only if using reconfirmable
13
14 # ...
15 end
16
17 # ...
18 end
19end

Saya akan menambahkan filed name.

$ rails g migration add_name_to_users name:string
db/migration/20210312145059_add_name_to_users.rb
1class AddNameToUsers < ActiveRecord::Migration[5.2]
2 def change
3 add_column :users, :name, :string
4 end
5end

Sip, jalankan migration.

$ rails db:migrate

Aktifkan module :confirmable pada user.rb model.

app/models/user.rb
1class User < ApplicationRecord
2 # Include default devise modules. Others available are:
3 # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
4 devise :database_authenticatable, :registerable,
5 :recoverable, :rememberable, :validatable,
6 :confirmable
7end

Sekalian, tambahkan logic untuk menghandle registration tanpa password di bawah module-module Devise tersebut.

app/modles/user.rb
1class User < ApplicationRecord
2 # Include default devise modules. Others available are:
3 # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
4 devise :database_authenticatable, :registerable,
5 :recoverable, :rememberable, :validatable,
6 :confirmable
7
8 # new function to set the password without knowing the current
9 # password used in our confirmation controller.
10 def attempt_set_password(params)
11 p = {}
12 p[:password] = params[:password]
13 p[:password_confirmation] = params[:password_confirmation]
14 update_attributes(p)
15 end
16
17 def password_match?
18 self.errors[:password] << "can't be blank" if password.blank?
19 self.errors[:password_confirmation] << "can't be blank" if password_confirmation.blank?
20 self.errors[:password_confirmation] << "does not match password" if password != password_confirmation
21 password == password_confirmation && !password.blank?
22 end
23
24 # new function to return whether a password has been set
25 def has_no_password?
26 self.encrypted_password.blank?
27 end
28
29 # Devise::Models:unless_confirmed` method doesn't exist in Devise 2.0.0 anymore.
30 # Instead you should use `pending_any_confirmation`.
31 def only_if_unconfirmed
32 pending_any_confirmation {yield}
33 end
34
35 def password_required?
36 # Password is required if it is being set, but not for new records
37 if !persisted?
38 false
39 else
40 !password.nil? || !password_confirmation.nil?
41 end
42 end
43end

Controller

Kita akan membuat 2 custom controller yang merupakan turunan dari Devise controller.

  1. RegistrationsController < Devise::RegistrationsController
  2. ConfirmationsController < Devise::ConfirmationsController
app/controllers/registrations_controller.rb
1class RegistrationsController < Devise::RegistrationsController
2 private
3
4 def sign_up_params
5 params.require(:user).permit(:name, :email, :password, :password_confirmation)
6 end
7
8 def account_update_params
9 params.require(:user).permit(:name, :email, :password, :password_confirmation, :current_password)
10 end
11end
app/controllers/confirmations_controller.rb
1class ConfirmationsController < Devise::ConfirmationsController
2 # Remove the first skip_before_filter (:require_no_authentication) if you
3 # don't want to enable logged users to access the confirmation page.
4 # If you are using rails 5.1+ use: skip_before_action
5 # skip_before_filter :require_no_authentication
6 # skip_before_filter :authenticate_user!
7
8 # PUT /resource/confirmation
9 def update
10 with_unconfirmed_confirmable do
11 if @confirmable.has_no_password?
12 @confirmable.attempt_set_password(params[:user])
13 if @confirmable.valid? and @confirmable.password_match?
14 do_confirm
15 else
16 do_show
17 @confirmable.errors.clear # so that we wont render :new
18 end
19 else
20 @confirmable.errors.add(:email, :password_already_set)
21 end
22 end
23
24 if !@confirmable.errors.empty?
25 self.resource = @confirmable
26 render 'devise/confirmations/new' # Change this if you don't have the views on default path
27 end
28 end
29
30 # GET /resource/confirmation?confirmation_token=abcdef
31 def show
32 with_unconfirmed_confirmable do
33 if @confirmable.has_no_password?
34 do_show
35 else
36 do_confirm
37 end
38 end
39 unless @confirmable.errors.empty?
40 self.resource = @confirmable
41 render 'devise/confirmations/new' # Change this if you don't have the views on default path
42 end
43 end
44
45 protected
46
47 def with_unconfirmed_confirmable
48 @confirmable = User.find_or_initialize_with_error_by(:confirmation_token, params[:confirmation_token])
49 if !@confirmable.new_record?
50 @confirmable.only_if_unconfirmed {yield}
51 end
52 end
53
54 def do_show
55 @confirmation_token = params[:confirmation_token]
56 @requires_password = true
57 self.resource = @confirmable
58 render 'devise/confirmations/show' # Change this if you don't have the views on default path
59 end
60
61 def do_confirm
62 @confirmable.confirm
63 set_flash_message :notice, :confirmed
64 sign_in_and_redirect(resource_name, @confirmable)
65 end
66end

Baris baris ke 58, kita akan membuat sendiri custom view template tersebut.

Pada catatan ini, saya membuat homepage, untuk tempat bernaung setelah melakukan registrasi dan juga sebagai root_path.

$ rails g controller Home index

Routes

config/routes.rb
1Rails.application.routes.draw do
2 # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
3 root to: 'home#index'
4
5 devise_for :users, controllers: {
6 registrations: 'registrations',
7 confirmations: 'confirmations'
8 }
9
10 as :user do
11 put '/users/confirmation' => 'confirmations#update', via: :patch, as: :update_user_confirmation
12 end
13end

Devise Views

Kita akan mengenerate Devise views.

$ rails g devise:views

Yang perlu dimodifikasi adalah:

  1. mengedit registrations/new
  2. menambahkan field name
  3. menghilangkan field password & password_confirmation
  4. membuat confirmations/show
  5. meletakkan field password & password_confirmation

Modifikasi view template registrations/new.

app/views/devise/registrations/new.html.erb
1<h2>Sign up</h2>
2
3<%= simple_form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
4 <%= f.error_notification %>
5
6 <div class="form-inputs">
7 <%= f.input :name,
8 required: true,
9 autofocus: true,
10 input_html: { autocomplete: "name" }%>
11 <%= f.input :email,
12 required: true,
13 input_html: { autocomplete: "email" }%>
14
15 <%# :password & :password_confirmation, dipindahkan ke views/devise/confirmations/show.html.erb %>
16 </div>
17
18 <div class="form-actions">
19 <%= f.button :submit, "Sign up" %>
20 </div>
21<% end %>
22
23<%= render "devise/shared/links" %>

Gambar 1

Gambar 1. Sign up form or Registration form

Dapat dilihat, pada form registrasi ini, hanya menampilkan input field berupa name dan email.

Saya memindahkan field password dan password_confirmation ke halaman yang lain, yaitu halaman views/devise/confirmations/show.html.erb.

app/views/devise/confirmations/show.html.erb
1<h2>Account Activation<% if resource.try(:user_name) %> for <%= resource.user_name %><% end %></h2>
2
3<%= simple_form_for(resource, as: resource_name, url: update_user_confirmation_path, html: { method: :put }) do |f| %>
4 <%= devise_error_messages! %>
5
6 <div class="form-inputs">
7 <% if @requires_password %>
8 <%= f.input :password,
9 required: true,
10 autofocus: true,
11 hint: ("#{@minimum_password_length} characters minimum" if @minimum_password_length),
12 input_html: { autocomplete: "new-password" } %>
13 <%= f.input :password_confirmation,
14 required: true,
15 input_html: { autocomplete: "new-password" } %>
16 <% end %>
17
18 <%= hidden_field_tag :confirmation_token,@confirmation_token %>
19 </div>
20
21 <div class="form-actions">
22 <%= f.button :submit, "Activate" %>
23 </div>
24<% end %>

Gambar 2

Gambar 2. Account Activation form

app/views/devise/confirmations/new.html.erb
1<h2>Resend confirmation instructions</h2>
2
3<%= simple_form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4 <%= f.error_notification %>
5 <%= f.full_error :confirmation_token %>
6
7 <div class="form-inputs">
8 <%= f.input :email,
9 required: true,
10 autofocus: true,
11 value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email),
12 input_html: { autocomplete: "email" } %>
13 </div>
14
15 <div class="form-actions">
16 <% if resource.pending_reconfirmation? %>
17 <%= f.button :submit, "Resend confirmation instructions" %>
18 <% end %>
19 </div>
20<% end %>
21
22<% unless user_signed_in? %>
23 <%= render "devise/shared/links" %>
24<% end %>

Pasang nav untuk menempatkan link indikator apabila user telah login atau belum.

app/layouts/application.html.erb
1<!DOCTYPE html>
2<html>
3 <head>
4 <!-- ... -->
5 </head>
6
7 <body>
8 <nav style="margin-bottom: 20px;">
9 <% if user_signed_in? %>
10 <%= link_to current_user.name, edit_user_registration_path, class:"navbar-item" %>
11 <%= link_to "Log Out", destroy_user_session_path, method: :delete, class:"navbar-item" %>
12 <% else %>
13 <% unless [
14 new_user_session_path,
15 new_user_registration_path,
16 new_user_confirmation_path,
17 user_confirmation_path,
18 new_user_password_path
19 ].include? request.path %>
20 <%= link_to "Sign In", new_user_session_path, class:"navbar-item" %>
21 <%= link_to "Sign up", new_user_registration_path, class:"navbar-item"%>
22 <% end %>
23 <% end %>
24 </nav>
25
26 <%= yield %>
27 </body>
28</html>

Demonstrasi

Gambar 3

Gambar 3. Demonstrasi register and activation

Repositori

github.com/bandithijo/demo_devise_confirmable

Pesan Penulis

Sepertinya, segini dulu yang dapat saya tuliskan.

Selanjutnya, saya serahkan kepada imajinasi dan kreatifitas teman-teman. Hehe.

Mudah-mudahan dapat bermanfaat.

Terima kasih.

(^_^)

Referensi

  1. mailcatcher.me
    Diakses tanggal: 2021/03/13

  2. Let’s Build: With Ruby on Rails - Project Management App - Part 2
    Diakses tanggal: 2021/03/13