diff --git a/Gemfile b/Gemfile index db764027..7481b8a6 100644 --- a/Gemfile +++ b/Gemfile @@ -59,6 +59,8 @@ gem 'webp-ffi' gem 'rszr' gem 'zip_tricks' gem 'adequate_crypto_address' +gem 'twilio-ruby' +gem 'phonelib' group :development, :test do gem 'pry' diff --git a/Gemfile.lock b/Gemfile.lock index 73e62b7c..8dddcfa3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -146,6 +146,7 @@ GEM io-extra (1.4.0) ipaddress (0.8.3) json (2.6.2) + jwt (2.7.1) keccak (1.3.1) llhttp-ffi (0.4.0) ffi-compiler (~> 1.0) @@ -197,6 +198,7 @@ GEM ox (2.14.11) paypal-recurring (1.1.0) pg (1.5.3) + phonelib (0.8.3) progress (3.6.0) pry (0.14.1) coderay (~> 1.1) @@ -291,6 +293,10 @@ GEM timeout (0.3.0) tins (1.31.1) sync + twilio-ruby (6.3.1) + faraday (>= 0.9, < 3.0) + jwt (>= 1.5, < 3.0) + nokogiri (>= 1.6, < 2.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) @@ -362,6 +368,7 @@ DEPENDENCIES nokogiri paypal-recurring pg + phonelib pry puma (< 7) rack-cache @@ -391,6 +398,7 @@ DEPENDENCIES thread tilt timecop + twilio-ruby webmock webp-ffi will_paginate diff --git a/app.rb b/app.rb index f5a9d9b1..82d621f3 100644 --- a/app.rb +++ b/app.rb @@ -77,6 +77,8 @@ before do # Skips the CSRF/validation check for stripe web hooks elsif email_not_validated? && !(request.path =~ /^\/site\/.+\/confirm_email|^\/settings\/change_email|^\/signout|^\/welcome|^\/supporter/) redirect "/site/#{current_site.username}/confirm_email" + elsif current_site && current_site.phone_verification_needed? && !(request.path =~ /^\/site\/.+\/confirm_phone/) + redirect "/site/#{current_site.username}/confirm_phone" else content_type :html, 'charset' => 'utf-8' redirect '/' if request.post? && !csrf_safe? diff --git a/app/create.rb b/app/create.rb index 80c38269..aea33991 100644 --- a/app/create.rb +++ b/app/create.rb @@ -98,6 +98,20 @@ post '/create' do end @site.email_confirmed = true if self.class.development? + @site.phone_confirmed = true if self.class.development? + + begin + @site.phone_verification_required = true if self.class.production? && BlackBox.phone_verification_required?(site) + rescue => e + EmailWorker.perform_async({ + from: 'web@neocities.org', + to: 'errors@neocities.org', + subject: "[Neocities Error] Phone verification exception", + body: "#{e.inspect}\n#{e.backtrace}", + no_footer: true + }) + end + @site.save unless education_whitelisted? diff --git a/app/site.rb b/app/site.rb index 18e2ce89..85f019de 100644 --- a/app/site.rb +++ b/app/site.rb @@ -295,4 +295,91 @@ get '/site/:username/unblock' do |username| current_site.unblock! site redirect request.referer +end + +get '/site/:username/confirm_phone' do + require_login + redirect '/' unless current_site.phone_verification_needed? + @title = 'Verify your Phone Number' + erb :'site/confirm_phone' +end + +def restart_phone_verification + current_site.phone_verification_sent_at = nil + current_site.phone_verification_sid = nil + current_site.save_changes validate: false + redirect "/site/#{current_site.username}/confirm_phone" +end + +post '/site/:username/confirm_phone' do + require_login + redirect '/' unless current_site.phone_verification_needed? + + if params[:phone_intl] + phone = Phonelib.parse params[:phone_intl] + + if !phone.valid? + flash[:error] = "Invalid phone number, please try again." + redirect "/site/#{current_site.username}/confirm_phone" + end + + if phone.types.include?(:premium_rate) || phone.types.include?(:shared_cost) + flash[:error] = 'Neocities does not support this type of number, please use another number.' + redirect "/site/#{current_site.username}/confirm_phone" + end + + current_site.phone_verification_sent_at = Time.now + current_site.phone_verification_attempts += 1 + + if current_site.phone_verification_attempts > Site::PHONE_VERIFICATION_LOCKOUT_ATTEMPTS + flash[:error] = 'You have exceeded the number of phone verification attempts allowed.' + redirect "/site/#{current_site.username}/confirm_phone" + end + + current_site.save_changes validate: false + + verification = $twilio.verify + .v2 + .services($config['twilio_service_sid']) + .verifications + .create(to: phone.e164, channel: 'sms') + + current_site.phone_verification_sid = verification.sid + current_site.save_changes validate: false + + flash[:success] = 'Validation message sent! Check your phone and enter the code below.' + else + + restart_phone_verification if current_site.phone_verification_sent_at < Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME + minutes_remaining = ((current_site.phone_verification_sent_at - (Time.now - Site::PHONE_VERIFICATION_EXPIRATION_TIME))/60).round + + begin + # Check code + vc = $twilio.verify + .v2 + .services($config['twilio_service_sid']) + .verification_checks + .create(verification_sid: current_site.phone_verification_sid, code: params[:code]) + + # puts vc.status (pending if failed, approved if it passed) + if vc.status == 'approved' + current_site.phone_verified = true + current_site.save_changes validate: false + else + flash[:error] = "Code was not correct, please try again. If the phone number you entered was incorrect, you can re-enter the number after #{minutes_remaining} more minutes have passed." + end + + rescue Twilio::REST::RestError => e + if e.message =~ /60202/ + flash[:error] = "You have exhausted your check attempts. Please try again in #{minutes_remaining} minutes." + elsif e.message =~ /20404/ # Unable to create record + restart_phone_verification + else + raise e + end + end + end + + # Will redirect to / automagically if phone was verified + redirect "/site/#{current_site.username}/confirm_phone" end \ No newline at end of file diff --git a/config.yml.ci b/config.yml.ci index 7425457d..7587484e 100644 --- a/config.yml.ci +++ b/config.yml.ci @@ -20,4 +20,7 @@ cache_control_ips: - 1.2.3.4 - 4.5.6.7 hcaptcha_site_key: "10000000-ffff-ffff-ffff-000000000001" -hcaptcha_secret_key: "0x0000000000000000000000000000000000000000" \ No newline at end of file +hcaptcha_secret_key: "0x0000000000000000000000000000000000000000" +twilio_account_sid: ACEDERPDERP +twilio_auth_token: derpderpderp +twilio_service_sid: VADERPDERPDERP \ No newline at end of file diff --git a/config.yml.template b/config.yml.template index be470036..1fc968fc 100644 --- a/config.yml.template +++ b/config.yml.template @@ -55,3 +55,6 @@ test: cache_control_ips: - 1.2.3.4 - 4.5.6.7 + twilio_account_sid: ACEDERPDERP + twilio_auth_token: derpderpderp + twilio_service_sid: VADERPDERPDERP \ No newline at end of file diff --git a/environment.rb b/environment.rb index de6d5231..cbcba539 100644 --- a/environment.rb +++ b/environment.rb @@ -177,3 +177,5 @@ $image_optim = ImageOptim.new pngout: false, svgo: false Money.locale_backend = nil Money.default_currency = Money::Currency.new("USD") Money.rounding_mode = BigDecimal::ROUND_HALF_UP + +$twilio = Twilio::REST::Client.new $config['twilio_account_sid'], $config['twilio_auth_token'] diff --git a/migrations/119_verify_phone.rb b/migrations/119_verify_phone.rb new file mode 100644 index 00000000..206e1cae --- /dev/null +++ b/migrations/119_verify_phone.rb @@ -0,0 +1,15 @@ +Sequel.migration do + up { + DB.add_column :sites, :phone_verification_required, :boolean, default: false + DB.add_column :sites, :phone_verified, :boolean, default: false + DB.add_column :sites, :phone_verification_sid, :text + DB.add_column :sites, :phone_verification_sent_at, :time + } + + down { + DB.drop_column :sites, :phone_verification_required + DB.drop_column :sites, :phone_verified + DB.drop_column :sites, :phone_verification_sid + DB.drop_column :sites, :phone_verification_sent_at + } +end \ No newline at end of file diff --git a/migrations/120_fix_phone_sent_at.rb b/migrations/120_fix_phone_sent_at.rb new file mode 100644 index 00000000..a5d3a937 --- /dev/null +++ b/migrations/120_fix_phone_sent_at.rb @@ -0,0 +1,11 @@ +Sequel.migration do + up { + DB.drop_column :sites, :phone_verification_sent_at + DB.add_column :sites, :phone_verification_sent_at, Time + } + + down { + DB.drop_column :sites, :phone_verification_sent_at + DB.add_column :sites, :phone_verification_sent_at, :time + } +end \ No newline at end of file diff --git a/migrations/121_phone_verification_attempts.rb b/migrations/121_phone_verification_attempts.rb new file mode 100644 index 00000000..5f8e4277 --- /dev/null +++ b/migrations/121_phone_verification_attempts.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :phone_verification_attempts, :integer, default: 0 + } + + down { + DB.drop_column :sites, :phone_verification_attempts + } +end \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index d79c943b..46060a75 100644 --- a/models/site.rb +++ b/models/site.rb @@ -167,6 +167,9 @@ class Site < Sequel::Model BLACK_BOX_WAIT_TIME = 10.seconds MAX_DISPLAY_FOLLOWS = 56*3 + PHONE_VERIFICATION_EXPIRATION_TIME = 10.minutes + PHONE_VERIFICATION_LOCKOUT_ATTEMPTS = 3 + many_to_many :tags one_to_many :profile_comments @@ -1789,6 +1792,11 @@ class Site < Sequel::Model end end + def phone_verification_needed? + return true if phone_verification_required && !phone_verified + false + end + private def store_file(path, uploaded, opts={}) diff --git a/views/site/confirm_email.erb b/views/site/confirm_email.erb index 2a2ab660..553b928d 100644 --- a/views/site/confirm_email.erb +++ b/views/site/confirm_email.erb @@ -6,7 +6,7 @@ You're almost ready!
<% end %> - We sent an email to <%= current_site.email %> to make sure it's correct.
Please check your email, enter the confirmation code here, and you're all set. + We sent an email to <%= current_site.email %> to make sure it's correct.
Please check your email, and enter the confirmation code here.
diff --git a/views/site/confirm_phone.erb b/views/site/confirm_phone.erb new file mode 100644 index 00000000..b055b488 --- /dev/null +++ b/views/site/confirm_phone.erb @@ -0,0 +1,90 @@ +
+

Verify your phone number

+
+

+ Last thing!
+ To prevent spam and keep the searchability of your site high, we have one last step: +
please verify your mobile phone number. +

+ +
+
+ <% if flash[:success] %> +
+ <%== flash[:success] %> +
+ <% end %> + + <% if flash[:error] %> +
+ <%== flash[:error] %> +
+ <% end %> + +
+ <%== csrf_token_input_html %> + + <% if current_site.phone_verification_sid %> +
+ + +
+ + + + + <% else %> + +
+ + + +
+ + + + + + + + <% end %> + +
+
+
+ +