first pass at phone validation

This commit is contained in:
Kyle Drake 2023-11-09 14:55:48 -06:00
parent 0c8696009f
commit 143704215f
10 changed files with 167 additions and 1 deletions

View file

@ -58,6 +58,8 @@ gem 'rss'
gem 'webp-ffi'
gem 'rszr'
gem 'zip_tricks'
gem 'twilio-ruby'
gem 'phonelib'
group :development, :test do
gem 'pry'

View file

@ -142,6 +142,7 @@ GEM
io-extra (1.4.0)
ipaddress (0.8.3)
json (2.6.2)
jwt (2.7.1)
llhttp-ffi (0.4.0)
ffi-compiler (~> 1.0)
rake (~> 13.0)
@ -192,6 +193,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)
@ -286,6 +288,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)
@ -356,6 +362,7 @@ DEPENDENCIES
nokogiri
paypal-recurring
pg
phonelib
pry
puma (< 7)
rack-cache
@ -385,6 +392,7 @@ DEPENDENCIES
thread
tilt
timecop
twilio-ruby
webmock
webp-ffi
will_paginate

2
app.rb
View file

@ -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?

View file

@ -98,6 +98,7 @@ post '/create' do
end
@site.email_confirmed = true if self.class.development?
#@site.phone_confirmed = true if self.class.development?
@site.save
unless education_whitelisted?

View file

@ -295,4 +295,62 @@ 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
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.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
# 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 re-enter.'
end
end
# Will redirect to / automagically if phone was verified
redirect "/site/#{current_site.username}/confirm_phone"
end

View file

@ -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']

View file

@ -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

View file

@ -1789,6 +1789,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={})

View file

@ -6,7 +6,7 @@
You're almost ready!<br>
<% end %>
We sent an email to <strong><%= current_site.email %></strong> to make sure it's correct.<br>Please check your email, enter the confirmation code here, and you're all set.
We sent an email to <strong><%= current_site.email %></strong> to make sure it's correct.<br>Please check your email, and enter the confirmation code here.
</h3>
<div class="row">

View file

@ -0,0 +1,73 @@
<section class="section plans welcome">
<h2>Verify your phone number</h2>
<div class="txt-Center"><img src="/img/catbus.png" width="90px"></div>
<h3 class="subtitle">
You're almost ready!<br>
To prevent spam and keep the searchability of your site high, we have one last step:
<br>please verify your mobile phone number.
</h3>
<div class="row">
<div class="col col-100 txt-Center" style="margin-top: 10px;">
<% if flash[:success] %>
<div class="alert alert-block alert-success" style="margin-top: 20px">
<%== flash[:success] %>
</div>
<% end %>
<% if flash[:error] %>
<div class="alert alert-block alert-error" style="margin-top: 20px">
<%== flash[:error] %>
</div>
<% end %>
<form method="POST" action="/site/<%= current_site.username %>/confirm_phone" class="content">
<%== csrf_token_input_html %>
<% if current_site.phone_verification_sid %>
<fieldset>
<label for="token">Enter the code:<br></label>
<input name="code" type="text" class="input-Area" autofill="off" autocapitalize="off" autocorrect="off" value="<%= flash[:code] %>" style="width: 290px">
</fieldset>
<input class="btn-Action" type="submit" value="Verify Code">
<% else %>
<fieldset>
<label for="token">Enter your phone number<br><small>(including country code)</small></label>
<input id="phone" name="phone" type="text" class="input-Area" autofill="off" autocapitalize="off" autocorrect="off" autocomplete="off" style="width: 290px">
<input id="phone_intl" name="phone_intl" type="hidden">
</fieldset>
<input id="submitButton" class="btn-Action" type="submit" value="Send Verification Code" style="display: none">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/css/intlTelInput.css">
<script src="https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/intlTelInput.min.js"></script>
<script>
const input = document.querySelector("#phone");
const iti = window.intlTelInput(input, {
nationalMode: true,
utilsScript: "https://cdn.jsdelivr.net/npm/intl-tel-input@18.2.1/build/js/utils.js",
});
const handleChange = () => {
let text;
if(iti.isValidNumber()) {
document.getElementById('submitButton').style = "display: inline-block"
document.getElementById('phone_intl').value = iti.getNumber()
} else {
document.getElementById('submitButton').style = "display: none"
}
};
// listen to "keyup", but also "change" to update when the user selects a country
input.addEventListener('change', handleChange);
input.addEventListener('keyup', handleChange);
</script>
<% end %>
</form>
</div>
</div>
</section>