Twilio - Rails 5 上的电话号码验证



我已经通过 Twilio 进行了电话号码验证,但我找不到一种方法来实现再次发送 PIN 码的功能(如果用户没有收到它),但也不超过 3 次(因此用户无法一遍又一遍地发送代码)。另外,我的代码看起来有点反模式,所以请随时建议更好的实现。

当 Devise User 注册时,我会派他创建一个belongs_to用户的ProfileProfile保存所有用户信息(和电话号码)。这是表格:

<%= form_for @profile, remote: true do |f| %>
<%= f.label 'Your name' %><br />
<%= f.text_field :first_name, autofocus: true, class: 'form-control' %>
<%= f.label 'Phone number' %><br />
<%= f.text_field :phone, class: 'form-control' %>
</br>
<div id="hideAfterSubmit">
<%= f.submit 'Save', class: 'btn btn-lg btn-primary btn-block' %>
</div>
<% end %>
<div id="verify-pin">
<h3>Enter your PIN</h3>
<%= form_tag profiles_verify_path, remote: true do |f| %>
<div class="form-group">
<%= text_field_tag :pin %>
</div>
<%= submit_tag "Verify PIN", class: "btn btn-primary" %>
<% end %>
</div>
<div id="status-box" class="alert alert-success">
<p id="status-message">Status: Haven’t done anything yet</p>
</div> 

#verify-pin#status-boxdisplay: none.我用回应create.js.erb取消隐藏它们.

创建操作:

def create
if user_signed_in? && current_user.profile
redirect_to profile_path(current_user), notice: 'Jūs jau sukūrėte paskyrą'
else     
@profile = Profile.new(profile_params)
@phone_number = params[:profile][:phone]
@profile.user_id = current_user.id
SmsTool.generate_pin
SmsTool.send_pin(phone_number: @phone_number)
if @profile.save
respond_to do |format|
format.js 
end
else
render :new 
end
end
end  

因此,此时配置文件已创建,保存并生成PIN码并将其发送到用户刚刚添加的电话号码。

短信工具:

def self.generate_pin
@@pin = rand(0000..9999).to_s.rjust(4, "0")
puts "#{@@pin}, Generated"
end    
def self.send_pin(phone_number:)
@client.messages.create(
from: ENV['TWILIO_PHONE_NUMBER'],
to: "+370#{phone_number}",
body: "Your pin is #{@@pin}"
)
end   
def self.verify(entered_pin)
puts "#{@@pin}, pin #{entered_pin} entered"
if @@pin == entered_pin
Current.user.profile.update(verified: true) 
else
return
end
end

Profiles#verify

def verify
SmsTool.verify(params[:pin])
@profile = current_user.profile     
respond_to do |format|
format.js
end
if @profile.verified
redirect_to root_path, notice: 'Account created'
end           
end

所以我不喜欢的是 SmsTool - 正如你所看到的,我使用类变量 - 找不到另一种方法。此外,我创建了一个单独的当前模块,只是为了访问Devisecurrent_user对象。:

module Current
thread_mattr_accessor :user
end

应用控制器:

around_action :set_current_user
def set_current_user
Current.user = current_user
yield
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
end 

正如我上面提到的 - 我找不到一种方法来实现再次发送 PIN 码的功能(如果用户没有收到它)。

并且请 - 随意建议优雅的实现。

附言这是我迄今为止最长的帖子。很抱歉,但我认为需要所有信息才能向您展示。

更新:

所以重新发送密码很容易,我只是添加了:

<div id="hiddenUnlessWrongPin">
<%= button_to "Re-send pin", action: "send_pin_again" %>
</div> 

和行动:

def send_pin_again
@phone_number = current_user.profile.phone
SmsTool.generate_pin
SmsTool.send_pin(phone_number: @phone_number)
end 

但是,如果用户已经发送了其中三个,我仍然不知道如何停止发送密码。我看到的唯一方法是在 db 中用整数值创建新行,并在每次用户发送 pin 时递增它。这是唯一的办法吗?

一个好的起点是查看处理电子邮件确认的Devise::Confirmable模块。我真正喜欢它的是它将确认建模为普通的旧资源。

我会尝试类似的东西,但使用单独的模型,因为它可以很容易地添加基于时间的限制。

class User < ApplicationRecord
has_one :profile
has_many :activations, through: :profiles
end
class Profile < ApplicationRecord
belongs_to :user
has_many :activations
end
# columns: 
# - pin [int or string]
# - profile_id [int] - foreign_key
# - confirmed_at [datetime]
class Activation < ApplicationRecord
belongs_to :profile
has_one :user, through: :profile
delegate :phone_number, to: :profile
authenticate :resend_limit, if: :new_record?
authenticate :valid_pin, unless: :new_record?
attr_accessor :response_pin
after_initialize :set_random_pin!, if: :new_record?
def set_random_pin!
self.pin = rand(0000..9999).to_s.rjust(4, "0")
end
def resend_limit
if self.profile.activations.where(created_at: (1.day.ago..Time.now)).count >= 3
errors.add(:base, 'You have reached the maximum allow number of reminders!')
end
end
def valid_pin
unless response_pin.present? && response_pin == pin
errors.add(:response_pin, 'Incorrect pin number')
end
end
def send_sms!
// @todo add logic to send sms
end
end

随意想出一个更好的名字。此外,这允许您使用普通的旧轨道验证来处理逻辑。

然后,您可以像任何其他资源一样对其进行 CRUD:

devise_scope :user do
resources :activations, only: [:new, :create, :edit, :update]
end

class ActivationsController < ApplicationController
before_action :authenticate_user!
before_action :set_profile
before_action :set_activation, only: [:edit, :update]
# Form to resend a pin notification.
# GET /users/activations/new
def new
@activation = @profile.phone_authentication.new
end
# POST /users/activations/new
def create
@activation = @profile.phone_authentication.new
if @activation.save
@activation.send_sms!
redirect_to edit_user_phone_activations_path(@activation)
else
render :new
end
end
# Renders form where user enters the activation code
# GET /users/activations/:id/edit
def edit
end
# confirms the users entered the correct pin number.
# PATCH /users/activations/:id
def update
if @activation.update(update_params)
# cleans up
@profile.activations.where.not(id: @activation.id).destroy_all
redirect_to profile_path(@profile), success: 'Your account was activated'
else
render :edit
end
end
private 
def update_params
params.require(:activation)
.permit(:response_pin)
.merge(confirmed_at: Time.now)
end
def set_profile
@profile = current_user.profile
end
def set_activation
@profile.activations.find(params[:id])
end
end

app/views/activations/new.html.erb:

<%= form_for(@activation) do |f| %>
<%= f.submit("Send activation to #{@activation.phone_number}") %>
<% end %>
No activation SMS? <%= link_ to "Resend", new_user_activation_path %>

app/views/activations/edit.html.erb:

<%= form_for(@activation) do |f| %>
<%= f.text_field :response_pin %>
<%= f.submit("Confirm") %>
<% end %>

最新更新