diff --git a/Gemfile b/Gemfile index af32aa9..6b01b67 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gem 'browser' gem 'dotenv' gem 'haml' gem 'i18n' +gem 'koala' gem 'localeapp', require: false gem 'odnoklassniki' gem 'omniauth' @@ -12,15 +13,17 @@ gem 'omniauth-facebook' gem 'omniauth-instagram' gem 'omniauth-twitter' gem 'omniauth-vkontakte' -gem 'koala' gem 'pg' +gem 'phony_rails' gem 'rack-flash3' gem 'rake' gem 'rest-client' gem 'require_all' +gem 'rotp' gem 'sinatra' gem 'sinatra-activerecord' gem 'sinatra-contrib', require: false +gem 'smsc_ru', git: 'https://github.com/startwifi/smsc_ru' gem 'twitter' group :development do diff --git a/Gemfile.lock b/Gemfile.lock index 38f9171..f4436f4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,3 +1,12 @@ +GIT + remote: https://github.com/startwifi/smsc_ru + revision: 654de5ac383a0db05746262cd061dd91dea83bc3 + specs: + smsc_ru (0.1.3) + addressable (~> 2.3) + faraday (~> 0.9) + virtus (~> 1.0) + GEM remote: https://rubygems.org/ specs: @@ -16,6 +25,10 @@ GEM public_suffix (~> 2.0, >= 2.0.2) arel (7.1.4) ast (2.3.0) + axiom-types (0.1.1) + descendants_tracker (~> 0.0.4) + ice_nine (~> 0.11.0) + thread_safe (~> 0.3, >= 0.3.1) backports (3.6.8) bond (0.5.1) brakeman (3.5.0) @@ -25,8 +38,12 @@ GEM childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) coderay (1.1.1) + coercible (1.0.0) + descendants_tracker (~> 0.0.1) concurrent-ruby (1.0.5) database_cleaner (1.5.3) + descendants_tracker (0.0.4) + thread_safe (~> 0.3, >= 0.3.1) diff-lcs (1.3) domain_name (0.5.20170223) unf (>= 0.0.5, < 1.0.0) @@ -63,6 +80,7 @@ GEM http-form_data (1.0.1) http_parser.rb (0.6.0) i18n (0.8.1) + ice_nine (0.11.2) iniparse (1.4.2) json (2.0.3) jwt (1.5.6) @@ -122,6 +140,10 @@ GEM parser (2.4.0.0) ast (~> 2.2) pg (0.19.0) + phony (2.15.41) + phony_rails (0.14.5) + activesupport (>= 3.0) + phony (~> 2.15) powerpack (0.1.1) pry (0.10.4) coderay (~> 1.1.0) @@ -153,6 +175,7 @@ GEM rack (>= 1.0) rack-test (~> 0.6.2) ripl (>= 0.7.0) + rotp (3.3.0) rspec (3.5.0) rspec-core (~> 3.5.0) rspec-expectations (~> 3.5.0) @@ -218,6 +241,11 @@ GEM unf_ext unf_ext (0.0.7.2) unicode-display_width (1.1.3) + virtus (1.0.5) + axiom-types (~> 0.1) + coercible (~> 1.0) + descendants_tracker (~> 0.0, >= 0.0.3) + equalizer (~> 0.0, >= 0.0.9) PLATFORMS ruby @@ -243,12 +271,14 @@ DEPENDENCIES omniauth-vkontakte overcommit pg + phony_rails pry-byebug rack-flash3 rack-test rake require_all rest-client + rotp rspec rubocop ruby_css_lint @@ -257,6 +287,7 @@ DEPENDENCIES sinatra sinatra-activerecord sinatra-contrib + smsc_ru! tux twitter diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index c665c32..bb6cb4a 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -30,6 +30,7 @@ class EventsController < ApplicationController end get '/:provider/auth' do + return auth_sms if current_user.provider == 'sms' current_user.add_event(:auth) redirect to(router_url) end @@ -39,6 +40,19 @@ class EventsController < ApplicationController redirect to(router_url) end + def auth_sms + case params[:provider] + when 'sms_ident_auth' + return current_user.add_event(:auth) if session[:sms_auth_success].present? + current_user.add_event(:sms_ident) + when 'sms_ident' + current_user.add_event(:sms_ident) + when 'sms_ident_adv' + current_user.add_event(:sms_ident_adv) + end + redirect to(router_url) + end + def post_facebook FacebookPostService.new(current_user.company, session[:user_token], params).call current_user.add_event(:post) diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f23c1e3..8568e81 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -19,6 +19,9 @@ class SessionsController < ApplicationController provider :instagram, setup: true, scope: 'likes comments relationships' + provider :sms, + smsauth_url: '/sms/auth', + uid_field: 'otp_phone' provider :twitter, setup: true provider :vkontakte, diff --git a/app/controllers/sms_controller.rb b/app/controllers/sms_controller.rb new file mode 100644 index 0000000..52431e6 --- /dev/null +++ b/app/controllers/sms_controller.rb @@ -0,0 +1,59 @@ +class SmsController < ApplicationController + before do + load_company + end + + get '/sms/authorize' do + @sms = SmsSender.new + haml :'sms/index' + end + + post '/sms/send' do + @sms = SmsSender.new(params) + + if @sms.valid? + session[:otp_secret] ||= ROTP::Base32.random_base32 + session[:otp_counter] ||= 0 + session[:otp_phone] = @sms.phone + + # Start generate OTP password + session[:otp_counter] += 1 # Increment counter + + hotp = ROTP::HOTP.new(session[:otp_secret]) + code = hotp.at(session[:otp_counter]) + + @sms.send_sms(code) + flash[:notice] = t('sms.success', phone: @sms.phone) + redirect to('/sms/validate') + else + haml :'sms/index' + end + end + + get '/sms/validate' do + haml :'sms/validate' + end + + post '/sms/validate' do + hotp = ROTP::HOTP.new(session[:otp_secret]) + + if hotp.verify(params[:code], session[:otp_counter]) + if @company.sms.action.eql?('ident_auth') && session[:sms_auth_success].present? + redirect to('/sms/auth') + end + redirect to('/sms/callback') + else + flash[:alert] = t(:errors) + haml :'sms/validate' + end + end + + private + + def load_company + @company = Company.find_by(token: session[:company_token]) + if @company.nil? || @company.sms.action == 'disabled' + redirect to('/') + end + end +end diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb index 5559e51..e9da89e 100644 --- a/app/controllers/widgets_controller.rb +++ b/app/controllers/widgets_controller.rb @@ -6,6 +6,7 @@ class WidgetsController < ApplicationController case current_user.provider when 'facebook' then widget_facebook when 'instagram' then widget_instagram + when 'sms' then widget_sms when 'twitter' then widget_twitter when 'vkontakte' then widget_vkontakte end @@ -28,6 +29,24 @@ def widget_instagram redirect to('/instagram/auth') end + def widget_sms + case @company.sms.action + when 'auth' + redirect to('/sms/auth') + when 'ident_auth' + current_user.add_event(:sms_ident) + session[:sms_auth_success] = true + haml :'visitors/index' + when 'ident' + if @company.sms.adv + current_user.add_event(:sms_ident) + haml :'widgets/sms/adv', layout: :'layouts/social' + else + redirect to('/sms/sms_ident') + end + end + end + def widget_twitter case @company.tw.action when 'post' diff --git a/app/models/sms_sender.rb b/app/models/sms_sender.rb new file mode 100644 index 0000000..7a5ebff --- /dev/null +++ b/app/models/sms_sender.rb @@ -0,0 +1,20 @@ +class SmsSender + include ActiveModel::Model + + attr_accessor :phone + + validates_plausible_phone :phone, presence: true, with: /\A\+\d+/ + + def send_sms(code) + smsc_client.send_sms(phone, "Your password: #{code}") + end + + private + + def smsc_client + Smsc::Client.new do |client| + client.login = ENV['SMSC_LOGIN'] + client.password = ENV['SMSC_PASSWORD'] + end + end +end diff --git a/app/views/sms/index.haml b/app/views/sms/index.haml new file mode 100644 index 0000000..64d4d15 --- /dev/null +++ b/app/views/sms/index.haml @@ -0,0 +1,19 @@ +.text-center + %h1= @company.name + %p= t('sms.header') + %ul.list-inline +.row + .col-md-offset-4.col-md-4 + - if @sms.errors.any? + #error_explanation + %h2= t(:errors) + %ul + - @sms.errors.full_messages.each do |error| + %li= error + %form{ action: '/sms/send', method: :post } + .form-group + %label{ for: :phone }= t('sms.phone') + %input.form-control{ name: :phone, type: :tel, placeholder: t('sms.placeholder') } + %span.help-block= "#{t('sms.format')} #{t('sms.placeholder')}" + .form-group + %button.btn.btn-primary{ type: :submit }= t('sms.continue') diff --git a/app/views/sms/validate.haml b/app/views/sms/validate.haml new file mode 100644 index 0000000..14d6eee --- /dev/null +++ b/app/views/sms/validate.haml @@ -0,0 +1,15 @@ +.text-center + %h1= @company.name + %p= t('sms.header') + %ul.list-inline +.row + .col-md-offset-4.col-md-4 + %form{ action: '/sms/validate', method: :post } + .form-group + %label{ for: :code }= t('sms.code') + %input.form-control{ name: :code } + .form-group + %button.btn.btn-primary{ type: :submit }= t('sms.confirm') +.text-center + %a{ href: '/' } + = t(:cancel) diff --git a/config.ru b/config.ru index 0110e58..cd633bc 100644 --- a/config.ru +++ b/config.ru @@ -2,6 +2,7 @@ require './config/environment' use EventsController use SessionsController +use SmsController use VisitorsController use WidgetsController diff --git a/config/environment.rb b/config/environment.rb index 68de03d..2490ece 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -6,3 +6,4 @@ Bundler.require(:default, ENV['SINATRA_ENV']) require_all 'app' +require_all 'lib' diff --git a/config/locales/en.yml b/config/locales/en.yml index 586ff23..63379f2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,5 +1,15 @@ en: + activemodel: + errors: + models: + sms_sender: + attributes: + phone: + blank: can't be blank + improbable_phone: is improbable + invalid: is invalid cancel: Cancel + errors: Form contains errors facebook: join: header: 'Facebook: like for continue' @@ -7,6 +17,17 @@ en: header: 'Facebook: share post for continue' placeholder: Your comment share: Post to Facebook + sms: + code: Auth code + confirm: Confirm + continue: Next + format: 'Format:' + header: You have to authorize with SMS + phone: Mobile number + placeholder: 123206241212 + success: 'Verification code has been sent on %{phone} + +' today: Today, %{time} twitter: join: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index ea10020..3bc5d66 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1,5 +1,15 @@ ru: + activemodel: + errors: + models: + sms_sender: + attributes: + phone: + blank: не может быть пустым + improbable_phone: является недействительным номером + invalid: является недействительным номером cancel: Отмена + errors: Форма содержит ошибки facebook: join: header: 'Facebook: Like для продолжения' @@ -7,6 +17,15 @@ ru: header: 'Facebook: поделиться для продолжения' placeholder: Ваше сообщение share: Поделиться на Facebook + sms: + code: Код авторизации + confirm: Подтвердить + continue: Продолжить + format: 'Формат:' + header: Для продолжения необходимо авторизироваться с помощью кода из sms-сообщения, полученного на ваш мобильный телефон + phone: Мобильный телефон + placeholder: 123206241212 + success: На номер %{phone} был выслан код подтверждения today: Сегодня, %{time} twitter: join: diff --git a/lib/omniauth-sms.rb b/lib/omniauth-sms.rb new file mode 100644 index 0000000..f0e17fa --- /dev/null +++ b/lib/omniauth-sms.rb @@ -0,0 +1,23 @@ +require 'omniauth' + +module OmniAuth + module Strategies + class Sms + include OmniAuth::Strategy + + option :smsauth_url, '/smsauth' + + def request_phase + redirect options.smsauth_url + end + + uid do + session[options.uid_field.to_s] + end + + info do + { name: uid } + end + end + end +end