From 1d6fcb11d4c1c5ed12493212433973a75f9f139c Mon Sep 17 00:00:00 2001 From: Theodor Vararu Date: Wed, 8 Sep 2021 12:43:41 +0300 Subject: [PATCH] Change user validation (#20) * #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 --- .../custom/verification/letter_controller.rb | 13 ++ .../verification/residence_controller.rb | 36 ++++++ .../custom/verification/sms_controller.rb | 11 ++ app/models/custom/setting.rb | 5 + app/models/custom/verification/residence.rb | 118 +++++++++++++++--- app/models/custom/verification/sms.rb | 28 +++++ app/views/custom/shared/_errors.html.erb | 15 +++ .../verification/residence/new.html.erb | 80 ++++++++++++ config/application_custom.rb | 2 + config/deploy.yml | 11 ++ config/locales/ro-RO/verification.yml | 29 +++-- .../verification/cnp_validation_spec.rb | 23 ++++ 12 files changed, 348 insertions(+), 23 deletions(-) create mode 100644 app/controllers/custom/verification/letter_controller.rb create mode 100644 app/controllers/custom/verification/residence_controller.rb create mode 100644 app/controllers/custom/verification/sms_controller.rb create mode 100644 app/models/custom/setting.rb create mode 100644 app/models/custom/verification/sms.rb create mode 100644 app/views/custom/shared/_errors.html.erb create mode 100644 app/views/custom/verification/residence/new.html.erb create mode 100644 config/deploy.yml create mode 100644 spec/system/verification/cnp_validation_spec.rb diff --git a/app/controllers/custom/verification/letter_controller.rb b/app/controllers/custom/verification/letter_controller.rb new file mode 100644 index 00000000000..f9633e79129 --- /dev/null +++ b/app/controllers/custom/verification/letter_controller.rb @@ -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 diff --git a/app/controllers/custom/verification/residence_controller.rb b/app/controllers/custom/verification/residence_controller.rb new file mode 100644 index 00000000000..f12a9d59e4d --- /dev/null +++ b/app/controllers/custom/verification/residence_controller.rb @@ -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 diff --git a/app/controllers/custom/verification/sms_controller.rb b/app/controllers/custom/verification/sms_controller.rb new file mode 100644 index 00000000000..682e8a8efc2 --- /dev/null +++ b/app/controllers/custom/verification/sms_controller.rb @@ -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 diff --git a/app/models/custom/setting.rb b/app/models/custom/setting.rb new file mode 100644 index 00000000000..693f180876b --- /dev/null +++ b/app/models/custom/setting.rb @@ -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 diff --git a/app/models/custom/verification/residence.rb b/app/models/custom/verification/residence.rb index 598c71c38b1..9d0fcad92c3 100644 --- a/app/models/custom/verification/residence.rb +++ b/app/models/custom/verification/residence.rb @@ -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 diff --git a/app/models/custom/verification/sms.rb b/app/models/custom/verification/sms.rb new file mode 100644 index 00000000000..11f0ece55db --- /dev/null +++ b/app/models/custom/verification/sms.rb @@ -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 diff --git a/app/views/custom/shared/_errors.html.erb b/app/views/custom/shared/_errors.html.erb new file mode 100644 index 00000000000..89da8349cb5 --- /dev/null +++ b/app/views/custom/shared/_errors.html.erb @@ -0,0 +1,15 @@ +<% if resource.errors.any? %> +
+ + + + <% if local_assigns[:message].present? %> + <%= message %> + <% else %> + <%= sanitize(t("form.not_saved", resource: t("form.#{resource.class.to_s.underscore}"))) %> + <% end %> + +
+<% end %> diff --git a/app/views/custom/verification/residence/new.html.erb b/app/views/custom/verification/residence/new.html.erb new file mode 100644 index 00000000000..670cf2b8cd5 --- /dev/null +++ b/app/views/custom/verification/residence/new.html.erb @@ -0,0 +1,80 @@ + diff --git a/config/application_custom.rb b/config/application_custom.rb index 4fe92a43265..d5c965bfd99 100644 --- a/config/application_custom.rb +++ b/config/application_custom.rb @@ -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 diff --git a/config/deploy.yml b/config/deploy.yml new file mode 100644 index 00000000000..d31d8a2fca0 --- /dev/null +++ b/config/deploy.yml @@ -0,0 +1,11 @@ +development: + city: Brașov + support_email: contact@code4.ro + +staging: + city: Brașov + support_email: contact@code4.ro + +production: + city: Brașov + support_email: contact@code4.ro diff --git a/config/locales/ro-RO/verification.yml b/config/locales/ro-RO/verification.yml index e7e540ed611..265807c1c99 100644 --- a/config/locales/ro-RO/verification.yml +++ b/config/locales/ro-RO/verification.yml @@ -46,10 +46,17 @@ ro: success: Adresă verificată new: accept_terms_text: Accept %{terms_url} + accept_terms_all: | + Sunt de acord cu toate afirmațiile de mai jos + accept_terms_adult: Declar pe proprie răspundere că am împlinit 18 ani + accept_terms_resident: Declar pe proprie răspundere că am reședinta temporară sau permanentă în %{residence} + accept_terms_terms: Accept %{terms_url} și politica de confidențialitate accept_terms_text_title: Accept termenii și condițiile de acces al Recensământ-ului - document_number: Numărul documentului + accept_terms_residence: "Declar pe proprie răspundere că am reședinta temporară sau permanentă în " + accept_terms_age: "Declar pe proprie răspundere că am împlinit 18 ani" + document_number: Cod Numeric Personal (CNP) document_number_help_title: Ajutor - document_number_help_text: "DNI: 12345678A
Pașaport: AAA000001
Card de reședință: X1234567P" + document_number_help_text: "Exemplu CNP: 2900411192534" document_type: passport: Pașaport residence_card: Card de reședință @@ -57,11 +64,13 @@ ro: error_not_allowed_age: Nu aveți vârsta necesară pentru a participa error_not_allowed_postal_code: Pentru a putea fi verificat, trebuie să aveți cont. error_verifying_census: Nu am reușit să verificăm informațiile. Rugăm să luați legătura cu un administrator. - form_errors: a împiedicat verificarea reședinței dumneavoastră + form_errors: Unele date introduse nu sunt valide postal_code_note: Pentru a vă verifica contul trebuie să vă înregistrați terms: termenii și condițiile de acces - title: Verificați reședința - verify_residence: Verificați reședința + title: Verificare cont + verify_residence: Verifică + help_needed: | + Dacă întâmpini probleme cu procesul de verificare, contactează-ne: %{email} sms: create: flash: @@ -72,9 +81,10 @@ ro: submit_button: Trimite title: Confirmare cod de securitate new: - phone: Introduceți numărul de telefon pentru a primi codul + phone: Număr de telefon + phone_error: Număr de telefon invalid phone_format: "(Exemplu: 612345678 sau +34612345678)" - phone_note: Folosim numărul dvs. de telefon doar pentru a vă trimite un cod, nu vă vom contacta niciodată. + phone_note: Introduceți numărul dumneavoastră de telefon phone_placeholder: "Exemplu: 612345678 sau +34612345678" submit_button: Trimite title: Trimite codul de confirmare @@ -82,7 +92,7 @@ ro: error: Cod de confirmare incorect flash: level_three: - success: Cod corect. Contul dumneavoastră este acum verificat + success: Contul dumneavoastră este acum verificat level_two: success: Cod corect step_1: Reședință @@ -90,8 +100,9 @@ ro: step_3: Verificare finală user_permission_debates: Participați la dezbateri user_permission_info: Verificându-vă informațiile veți putea să... + user_permission_info_budgets: Verificându-vă informațiile veți avea acces la procesul de bugetare participativă user_permission_proposal: Creează noi propuneri - user_permission_support_proposal: Sprijină propunerile* + user_permission_support_proposal: Sprijiniți propunerile user_permission_votes: Participă la votarea finală* verified_user: form: diff --git a/spec/system/verification/cnp_validation_spec.rb b/spec/system/verification/cnp_validation_spec.rb new file mode 100644 index 00000000000..a170e5d796a --- /dev/null +++ b/spec/system/verification/cnp_validation_spec.rb @@ -0,0 +1,23 @@ +require "rails_helper" + +describe "CNP" do + before { create(:geozone) } + + scenario "User can verify with their CNP" do + user = create(:user) + login_as(user) + + visit account_path + click_link "Verify my account" + + fill_in "residence_document_number", with: "1870113780091" + fill_in "verification_sms_phone", with: "0745123456" + check "residence_terms_of_service" + check "residence_adult" + check "residence_resident" + + click_button "Verify residence" + + expect(page).to have_content "Account verified" + end +end