Skip to content

Commit

Permalink
Change user validation (#20)
Browse files Browse the repository at this point in the history
* #18 Partial implementation for user verification

* #18 Add validation for CNP

* #18 Add working implementation for validation

* #18 Fix unsafe stripping

* Add test for CNP validation, split up terms checkboxes

Add a very basic system spec for the new CNP verification flow.

Split up the checkbox to accept terms, residency, and adult status into
three separate checkboxes.

Co-authored-by: tsundokul <[email protected]>
  • Loading branch information
tvararu and tsundokul committed Sep 8, 2021
1 parent 63258e4 commit 1d6fcb1
Show file tree
Hide file tree
Showing 12 changed files with 348 additions and 23 deletions.
13 changes: 13 additions & 0 deletions app/controllers/custom/verification/letter_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Verification::LetterController < ApplicationController
before_action :authenticate_user!
skip_authorization_check

def new
redirect_to account_path
end

alias_method :create, :new
alias_method :show, :new
alias_method :edit, :new
alias_method :update, :new
end
36 changes: 36 additions & 0 deletions app/controllers/custom/verification/residence_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class Verification::ResidenceController < ApplicationController
before_action :authenticate_user!
before_action :verify_verified!
before_action :verify_lock, only: [:new, :create]
skip_authorization_check

def new
@residence = Verification::Residence.new
@sms = Verification::Sms.new
end

def create
@residence = Verification::Residence.new(residence_params.merge(user: current_user))
@sms = Verification::Sms.new(phone: params[:verification_sms][:phone], user: current_user)

# Check validity for both objects
# Not using just an if to force both checks
sv = @sms.valid?
rv = @residence.valid?

if sv && rv
@residence.save
@sms.save

redirect_to '/account', notice: t("verification.sms.update.flash.level_three.success")
else
render :new
end
end

private

def residence_params
params.require(:residence).permit(:document_number, :document_type, :date_of_birth, :postal_code, :terms_of_service, :adult, :resident, sms_attributes: [:phone])
end
end
11 changes: 11 additions & 0 deletions app/controllers/custom/verification/sms_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# disable SMS validation
class Verification::SmsController < ApplicationController
before_action :authenticate_user!
skip_authorization_check

def new
redirect_to account_path
end

alias_method :create, :new
end
5 changes: 5 additions & 0 deletions app/models/custom/setting.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require_dependency Rails.root.join('app', 'models', 'setting').to_s

# [code4ro]
# Enable user verification by default
Setting["feature.user.skip_verification"] = false
118 changes: 104 additions & 14 deletions app/models/custom/verification/residence.rb
Original file line number Diff line number Diff line change
@@ -1,26 +1,116 @@
require_dependency Rails.root.join("app", "models", "verification", "residence").to_s

class Verification::Residence
validate :local_postal_code
validate :local_residence
include ActiveModel::Model
include ActiveModel::Dates
include ActiveModel::Validations::Callbacks

attr_accessor :user, :document_number, :document_type, :date_of_birth, :postal_code, :terms_of_service,
:adult, :resident

before_validation :retrieve_census_data

validates :document_number, presence: true
validates :terms_of_service, acceptance: { allow_nil: false }
validates :adult, acceptance: { allow_nil: false }
validates :resident, acceptance: { allow_nil: false }

validate :document_number_uniqueness
validate :document_number_format

def local_postal_code
errors.add(:postal_code, I18n.t("verification.residence.new.error_not_allowed_postal_code")) unless valid_postal_code?
def initialize(attrs = {})
attrs = remove_date("date_of_birth", attrs)
super

clean_document_number
end

def local_residence
return if errors.any?
def save
self.date_of_birth = date_of_birth_from_document
return false unless valid?

user.take_votes_if_erased_document(document_number, document_type)

user.update(document_number: document_number,
document_type: document_type,
date_of_birth: date_of_birth.in_time_zone.to_datetime,
gender: gender,
residence_verified_at: Time.current)
end

unless residency_valid?
errors.add(:local_residence, false)
store_failed_attempt
Lock.increase_tries(user)
def save!
validate! && save
end

def allowed_age
return if errors[:date_of_birth].any?

errors.add(:date_of_birth, I18n.t("verification.residence.new.error_not_allowed_age"))
end

def document_number_uniqueness
errors.add(:document_number, I18n.t("errors.messages.taken")) if User.active.where(document_number: document_number).any?
end

def document_number_format
unless looks_like_cnp? document_number
errors.add(:document_number, I18n.t("errors.messages.invalid"))
end
end

def looks_like_cnp? doc_num
is_number?(doc_num) && doc_num.size == 13 && doc_num[0] != '9'
end

def store_failed_attempt
FailedCensusCall.create(
user: user,
document_number: document_number,
document_type: document_type,
date_of_birth: date_of_birth_from_document,
postal_code: postal_code
)
end

def date_of_birth_from_document
return unless looks_like_cnp? document_number

day = document_number[5..6].to_i
month = document_number[3..4].to_i
year = document_number[1..2].to_i
year += case gender_digit
when 1..2
1900
when 3..4
1800
when 5..8
2000
end

Date.new(year, month, day) rescue errors.add(:document_number, I18n.t("errors.messages.invalid"))
end

def gender_digit
document_number.first.to_i
end

def gender
if [1, 3, 5, 7].include?(gender_digit)
return 'male'
elsif [2, 4, 6, 8].include?(gender_digit)
return 'female'
end
end

def is_number? string
true if Float(string) rescue false
end

private

def valid_postal_code?
postal_code =~ /^280/
def retrieve_census_data
@census_data = CensusCaller.new.call(document_type, document_number, date_of_birth_from_document, postal_code)
end

def clean_document_number
self.document_number = document_number&.strip
end
end
28 changes: 28 additions & 0 deletions app/models/custom/verification/sms.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class Verification::Sms
include ActiveModel::Model

attr_accessor :user, :phone, :confirmation_code

validates :phone, presence: true
validates :phone, format: { with: /\A[\d\+]{10,}\z/ }
validate :uniqness_phone

def uniqness_phone
errors.add(:phone, :taken) if User.where(confirmed_phone: phone).any? unless phone.blank?
end

def save
return false unless valid?

update_user_phone_information
end

def update_user_phone_information
user.update(unconfirmed_phone: phone, confirmed_phone: phone, sms_confirmation_code: '', verified_at: Time.current)
end

# Always true since verification is disabled
def verified?
true
end
end
15 changes: 15 additions & 0 deletions app/views/custom/shared/_errors.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<% if resource.errors.any? %>
<div id="error_explanation" data-alert class="callout alert" data-closable>
<button class="close-button" aria-label="<%= t("application.close") %>" type="button" data-close>
<span aria-hidden="true">&times;</span>
</button>

<strong>
<% if local_assigns[:message].present? %>
<%= message %>
<% else %>
<%= sanitize(t("form.not_saved", resource: t("form.#{resource.class.to_s.underscore}"))) %>
<% end %>
</strong>
</div>
<% end %>
80 changes: 80 additions & 0 deletions app/views/custom/verification/residence/new.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<div class="verification account row">
<% track_event(category: "verification", action: "start_census") %>
<div class="small-12 column">

<%= back_link_to account_path, t("verification.back") %>

<h1 class="d-block"><%= t("verification.residence.new.title") %></h1>

<div class="user-permissions small-12">

<p><%= t("verification.user_permission_info_budgets") %></p>
<p><%= raw t("verification.residence.new.help_needed",
email: mail_to(Rails.configuration.deploy['support_email'])
) %>
</p>
<hr/>

</div>

<%= form_for @residence, as: "residence", url: residence_path do |f| %>
<%= render "errors" %>

<div class="row">
<div class="small-12 medium-4 column">

<div class="inline-block">
<%= f.label t("verification.residence.new.document_number") %>
</div>

<button type="button" class="inline-block" data-toggle="info-document-number">
<span class="icon-help"></span>
<span class="show-for-sr"><%= t("verification.residence.new.document_number_help_title") %></span>
</button>

<div class="dropdown-pane" id="info-document-number" data-dropdown
data-hover="true" data-hover-pane="true">
<%= sanitize(t("verification.residence.new.document_number_help_text")) %>
</div>

<%= f.text_field :document_number, label: false %>
</div>
</div>

<div class="row">
<%= fields_for @sms do |s| %>

<div class="small-12 medium-4 column">
<%= s.label :phone, t("verification.sms.new.phone"), class: "inline-block" %>
<p class="help-text" id="phone-text-help"><%= t("verification.sms.new.phone_note") %></p>
<%= s.telephone_field :phone, label: false,
aria: { describedby: "phone-help-text" }
%>
</div>
<% end %>
</div>

<div class="small-12 mt-3">
<%= f.check_box :terms_of_service,
label: t("verification.residence.new.accept_terms_adult") %>
<%= f.check_box :adult,
label: t("verification.residence.new.accept_terms_resident",
residence: Rails.configuration.deploy['city']) %>
<%= f.check_box :resident,
label: t("verification.residence.new.accept_terms_terms",
terms_url: link_to(t("verification.residence.new.terms"), "/census_terms",
title: t("shared.target_blank"),
target: "_blank")) %>
</div>

<div class="small-12 medium-3 clear">
<%= f.submit t("verification.residence.new.verify_residence"),
id: "new_residence_submit",
class: "button success expanded" %>
</div>
<% end %>
</div>
</div>
2 changes: 2 additions & 0 deletions config/application_custom.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Consul
class Application < Rails::Application
config.i18n.default_locale = :en
config.deploy = config_for(:deploy)
config.action_view.cache_template_loading = false
end
end
11 changes: 11 additions & 0 deletions config/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
development:
city: Brașov
support_email: [email protected]

staging:
city: Brașov
support_email: [email protected]

production:
city: Brașov
support_email: [email protected]
Loading

0 comments on commit 1d6fcb1

Please sign in to comment.