Initial commit
This commit is contained in:
17
app/services/api_keys_verifier.rb
Normal file
17
app/services/api_keys_verifier.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class APIKeysVerifier
|
||||
def initialize(params = {})
|
||||
@kid = params[:kid]
|
||||
@signature = params[:signature]
|
||||
@nonce = params[:nonce] || nil
|
||||
@api_key = APIKey.find_by!(kid: @kid)
|
||||
end
|
||||
|
||||
def verify_hmac_payload?
|
||||
data = @nonce.to_s + @kid
|
||||
algorithm = 'SHA' + @api_key.algorithm[2..4]
|
||||
true_signature = OpenSSL::HMAC.hexdigest(algorithm, @api_key.secret, data)
|
||||
true_signature == @signature
|
||||
end
|
||||
end
|
||||
66
app/services/captcha_service.rb
Normal file
66
app/services/captcha_service.rb
Normal file
@@ -0,0 +1,66 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'digest'
|
||||
require 'net/http'
|
||||
|
||||
module CaptchaService
|
||||
# Google recaptcha verifier
|
||||
class RecaptchaVerifier
|
||||
include Recaptcha::Adapters::ControllerMethods
|
||||
|
||||
attr_reader :request
|
||||
|
||||
def initialize(request:)
|
||||
@request = request
|
||||
end
|
||||
|
||||
def response_valid?(skip_remote_ip:, response:)
|
||||
# according to changes in v2 support https://github.com/ambethia/recaptcha#recaptcha-v2-api-and-usage
|
||||
# method comes from Recaptcha::Adapters::ControllerMethods
|
||||
# https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/adapters/controller_methods.rb#L10
|
||||
verify_recaptcha(secret_key: Barong::App.config.recaptcha_secret_key,
|
||||
model: User,
|
||||
skip_remote_ip: skip_remote_ip,
|
||||
response: response)
|
||||
end
|
||||
end
|
||||
|
||||
# Geetest.com captcha verifier
|
||||
class GeetestVerifier
|
||||
def initialize
|
||||
@api = 'http://api.geetest.com'
|
||||
@validate_path = '/validate.php'
|
||||
@register_path = '/register.php'
|
||||
@geetest_id = Barong::App.config.geetest_id
|
||||
@geetest_key = Barong::App.config.geetest_key
|
||||
end
|
||||
|
||||
def validate(response)
|
||||
md5 = Digest::MD5.hexdigest(@geetest_key + 'geetest' + response['geetest_challenge'])
|
||||
if response['geetest_validate'] == md5
|
||||
back = begin
|
||||
post(@api + @validate_path, seccode: response['geetest_seccode'])
|
||||
rescue StandardError
|
||||
''
|
||||
end
|
||||
return back == Digest::MD5.hexdigest(response['geetest_seccode'])
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
def register
|
||||
challenge = get(@api + @register_path + "?gt=#{@geetest_id}")
|
||||
{ gt: @geetest_id, challenge: Digest::MD5.hexdigest(challenge + @geetest_key) }
|
||||
rescue StandardError
|
||||
''
|
||||
end
|
||||
|
||||
def get(uri)
|
||||
Net::HTTP.get_response(URI(uri)).body
|
||||
end
|
||||
|
||||
def post(uri, data)
|
||||
Net::HTTP.post_form(URI(uri), data).body
|
||||
end
|
||||
end
|
||||
end
|
||||
75
app/services/encryption_service.rb
Normal file
75
app/services/encryption_service.rb
Normal file
@@ -0,0 +1,75 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class EncryptionService
|
||||
# Example: 202040
|
||||
# Week number starts from 0
|
||||
def self.current_salt
|
||||
Time.now.strftime('%Y%W')
|
||||
end
|
||||
|
||||
def self.pack(salt, value)
|
||||
[salt, value].join('.')
|
||||
end
|
||||
|
||||
def self.unpack(str)
|
||||
raise "Invalid encrypted value: #{str}" unless str =~ (/(\w*)\.(.*)/)
|
||||
[$1, $2]
|
||||
end
|
||||
|
||||
def self.encrypt(value)
|
||||
# Get current salt
|
||||
salt = current_salt
|
||||
# Get or generate new master key from salt
|
||||
current_key = get_master_key(salt)
|
||||
# Encrypt attribute value
|
||||
encrypted_key = encryptor(current_key).encrypt_and_sign(value)
|
||||
# Add salt before encrypted value
|
||||
pack(salt, encrypted_key)
|
||||
end
|
||||
|
||||
def self.decrypt(value)
|
||||
# Unpack salt and encrypted_key
|
||||
salt, encrypted_key = unpack(value)
|
||||
# Get master key from salt
|
||||
master_key = get_master_key(salt)
|
||||
# Decrypt encrypted value for attribute
|
||||
encryptor(master_key).decrypt_and_verify(encrypted_key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.encryptor(key)
|
||||
ActiveSupport::MessageEncryptor.new(key)
|
||||
end
|
||||
|
||||
def self.get_master_key(salt)
|
||||
@cache ||= {}
|
||||
# Delete keys with expired date
|
||||
delete_expired_keys
|
||||
|
||||
unless @cache[salt]
|
||||
# Initialize hash for specific salt
|
||||
@cache[salt] = {}
|
||||
|
||||
# Put key value from key generator
|
||||
@cache[salt]['key'] = ActiveSupport::KeyGenerator.new(
|
||||
ENV.fetch('SECRET_KEY_BASE')
|
||||
).generate_key(salt, ActiveSupport::MessageEncryptor.key_len)
|
||||
|
||||
# Put expire date for specific key
|
||||
@cache[salt]['expire_date'] = 1.week.from_now
|
||||
end
|
||||
|
||||
@cache[salt]['key']
|
||||
end
|
||||
|
||||
def self.delete_expired_keys
|
||||
return unless @cache.is_a?(Hash)
|
||||
|
||||
# Iterate through all @cache values
|
||||
@cache.each do |salt, values|
|
||||
# Delete key if expire date expired
|
||||
@cache.delete(salt) if values['expire_date'].present? && values['expire_date'] < Time.now
|
||||
end
|
||||
end
|
||||
end
|
||||
219
app/services/event_mailer.rb
Normal file
219
app/services/event_mailer.rb
Normal file
@@ -0,0 +1,219 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'bunny'
|
||||
require 'ostruct'
|
||||
|
||||
class EventMailer
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class VerificationError < Error; end
|
||||
|
||||
def initialize(events, exchanges, keychain)
|
||||
@exchanges = exchanges
|
||||
@keychain = keychain
|
||||
@events = events
|
||||
|
||||
Kernel.at_exit { unlisten }
|
||||
end
|
||||
|
||||
def call
|
||||
listen
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def listen
|
||||
unlisten
|
||||
|
||||
@bunny_session = Bunny::Session.new(rabbitmq_credentials).tap do |session|
|
||||
session.start
|
||||
Kernel.at_exit { session.stop }
|
||||
end
|
||||
|
||||
@bunny_channel = @bunny_session.channel
|
||||
# Delete old queue if some exists
|
||||
@bunny_channel.queue_delete('barong.postmaster.event.mailer') if @bunny_session.queue_exists?('barong.postmaster.event.mailer')
|
||||
|
||||
# Define fanout exchanges which will broadcast
|
||||
# all the messages they receives to all the queues they know
|
||||
retry_exchange = @bunny_channel.fanout('barong.event.mailer.retry.exchange')
|
||||
main_exchange = @bunny_channel.fanout('barong.event.mailer.main.exchange')
|
||||
|
||||
queue = @bunny_channel.queue('barong.event.mailer.main', auto_delete: false, durable: true,
|
||||
arguments: {
|
||||
:'x-dead-letter-exchange' => retry_exchange.name,
|
||||
}
|
||||
)
|
||||
queue.bind(main_exchange)
|
||||
|
||||
retry_queue = @bunny_channel.queue('barong.event.mailer.retry', auto_delete: false, durable: true,
|
||||
arguments: {
|
||||
:'x-dead-letter-exchange' => main_exchange.name,
|
||||
:'x-message-ttl' => 120000 # will trigger retry every 2 minutes
|
||||
})
|
||||
retry_queue.bind(retry_exchange)
|
||||
|
||||
@events.each do |event|
|
||||
exchange_name = @exchanges[event[:exchange].to_sym][:name]
|
||||
exchange = @bunny_channel.direct(exchange_name)
|
||||
|
||||
queue.bind(exchange, routing_key: event[:key])
|
||||
end
|
||||
|
||||
Rails.logger.info { 'Listening for events.' }
|
||||
queue.subscribe(manual_ack: true, block: true, &method(:handle_message))
|
||||
end
|
||||
|
||||
def unlisten
|
||||
if @bunny_session || @bunny_channel
|
||||
Rails.logger.info { 'No longer listening for events.' }
|
||||
end
|
||||
|
||||
@bunny_channel&.work_pool&.kill
|
||||
@bunny_session&.stop
|
||||
ensure
|
||||
@bunny_channel = nil
|
||||
@bunny_session = nil
|
||||
end
|
||||
|
||||
def algorithm_verification_options(signer)
|
||||
{ algorithms: @keychain[signer][:algorithm] }
|
||||
end
|
||||
|
||||
def jwt_public_key(signer)
|
||||
OpenSSL::PKey.read(Base64.urlsafe_decode64(@keychain[signer][:value]))
|
||||
end
|
||||
|
||||
def rabbitmq_credentials
|
||||
if Barong::App.config.event_api_rabbitmq_url.present?
|
||||
Barong::App.config.event_api_rabbitmq_url
|
||||
else
|
||||
{
|
||||
host: Barong::App.config.event_api_rabbitmq_host,
|
||||
port: Barong::App.config.event_api_rabbitmq_port,
|
||||
username: Barong::App.config.event_api_rabbitmq_username,
|
||||
password: Barong::App.config.event_api_rabbitmq_password
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_message(delivery_info, _metadata, payload)
|
||||
Rails.logger.info { "Start handling a message" }
|
||||
Rails.logger.info { "\nPayload: \n #{payload} \n\n Metadata: \n #{_metadata} \n\n Delivery info: \n #{delivery_info} \n" }
|
||||
exchange = @exchanges.select { |_, ex| ex[:name] == delivery_info[:exchange] }
|
||||
|
||||
# In case of retry message
|
||||
# we should get exchange name from _metadata info
|
||||
if exchange.empty?
|
||||
exchange_name = _metadata[:headers]['x-death'][1]['exchange']
|
||||
exchange = @exchanges.select { |_, ex| ex[:name] == exchange_name }
|
||||
end
|
||||
|
||||
exchange_id = exchange.keys.first.to_s
|
||||
signer = exchange[exchange_id.to_sym][:signer]
|
||||
|
||||
result = verify_jwt(payload, signer.to_sym)
|
||||
|
||||
raise VerificationError, "Failed to verify signature from #{signer}." \
|
||||
unless result[:verified].include?(signer.to_sym)
|
||||
|
||||
config = @events.select do |event|
|
||||
event[:key] == delivery_info[:routing_key] &&
|
||||
event[:exchange] == exchange_id
|
||||
end.first
|
||||
|
||||
event = result[:payload].fetch(:event)
|
||||
obj = JSON.parse(event.to_json, object_class: OpenStruct)
|
||||
|
||||
user = User.includes(:profiles).find_by(uid: obj.record.user.uid)
|
||||
language = user.language.downcase.to_sym
|
||||
Rails.logger.info { "User #{user.email} has '#{language}' email language" }
|
||||
template_config = config[:templates].transform_keys(&:downcase)
|
||||
|
||||
unless template_config.keys.include?(language)
|
||||
Rails.logger.error { "Language #{language} is not supported. Skipping." }
|
||||
return
|
||||
end
|
||||
|
||||
if config[:expression].present? && skip_event(event, config[:expression])
|
||||
Rails.logger.info { "Event #{obj.name} skipped" }
|
||||
return
|
||||
end
|
||||
|
||||
params = {
|
||||
logo: Barong::App.config.smtp_logo_link,
|
||||
subject: template_config[language][:subject],
|
||||
template_name: template_config[language][:template_path],
|
||||
record: obj.record,
|
||||
changes: obj.changes,
|
||||
user: user
|
||||
}
|
||||
|
||||
Postmaster.process_payload(params).deliver_now
|
||||
|
||||
# Acknowledges a message
|
||||
# Acknowledged message is completely removed from the queue
|
||||
@bunny_channel.ack(delivery_info.delivery_tag)
|
||||
rescue StandardError => e
|
||||
Rails.logger.error { e.inspect }
|
||||
|
||||
if e.is_a?(JWT::ExpiredSignature) || e.is_a?(JWT::VerificationError) || e.is_a?(VerificationError)
|
||||
# Acknowledges a message
|
||||
@bunny_channel.ack(delivery_info.delivery_tag)
|
||||
else
|
||||
# Rejects a message
|
||||
# A rejected message dropped by RabbitMQ and goes to dead letter exchange queue
|
||||
@bunny_channel.reject(delivery_info.delivery_tag)
|
||||
end
|
||||
|
||||
unlisten if db_connection_error?(e)
|
||||
end
|
||||
|
||||
def verify_jwt(payload, signer)
|
||||
options = algorithm_verification_options(signer)
|
||||
JWT::Multisig.verify_jwt JSON.parse(payload), { signer => jwt_public_key(signer) },
|
||||
options.compact
|
||||
end
|
||||
|
||||
def skip_event(event, expression)
|
||||
# valid operators: and / or / not
|
||||
operator = expression.keys.first.downcase
|
||||
# { field_name: field_value }
|
||||
values = expression[operator]
|
||||
|
||||
# return array of boolean [false, true]
|
||||
res = values.keys.map do |field_name|
|
||||
safe_dig(event, field_name.to_s.split('.')) == values[field_name]
|
||||
end
|
||||
|
||||
# all? works as AND operator, any? works as OR operator
|
||||
return false if (operator == :and && res.all?) || (operator == :or && res.any?) ||
|
||||
(operator == :not && !res.all?)
|
||||
|
||||
return true if operator == :not && res.all?
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def db_connection_error?(exception)
|
||||
exception.is_a?(Mysql2::Error::ConnectionError) || exception.cause.is_a?(Mysql2::Error)
|
||||
end
|
||||
|
||||
def safe_dig(hash, keypath, default = nil)
|
||||
stringified_hash = JSON.parse(hash.to_json)
|
||||
stringified_keypath = keypath.map(&:to_s)
|
||||
|
||||
stringified_keypath.reduce(stringified_hash) do |accessible, key|
|
||||
return default unless accessible.is_a? Hash
|
||||
return default unless accessible.key? key
|
||||
|
||||
accessible[key]
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
def call(*args)
|
||||
new(*args).call
|
||||
end
|
||||
end
|
||||
end
|
||||
110
app/services/jibit_service.rb
Normal file
110
app/services/jibit_service.rb
Normal file
@@ -0,0 +1,110 @@
|
||||
class JibitService
|
||||
|
||||
attr_reader :base_url
|
||||
attr_reader :api_key
|
||||
attr_reader :api_secret
|
||||
attr_reader :conn
|
||||
attr_accessor :response
|
||||
attr_accessor :access_token
|
||||
attr_accessor :refresh_token
|
||||
attr_accessor :called_method
|
||||
|
||||
# one day
|
||||
JIBIT_EXPIRE_TIME = 86400
|
||||
def initialize
|
||||
@base_url = Barong::App.config.jibit_api_url
|
||||
@api_key = Barong::App.config.jibit_api_key
|
||||
@api_secret = Barong::App.config.jibit_api_secret
|
||||
@conn = Faraday.new(base_url)
|
||||
conn.headers['Content-Type'] = 'application/json'
|
||||
@access_token = read_access_token
|
||||
end
|
||||
|
||||
def retry_options
|
||||
{ max: 5,
|
||||
retry_statuses: [401, 409, 500] }
|
||||
end
|
||||
|
||||
def read_access_token
|
||||
return Rails.cache.read('jibit_access_token') if Rails.cache.read('jibit_access_token').present?
|
||||
|
||||
generate_access_token
|
||||
|
||||
raise 'please try again(jibit not create token)' unless access_token.present?
|
||||
|
||||
Rails.cache.write('jibit_access_token', access_token, expires_in: JIBIT_EXPIRE_TIME)
|
||||
access_token
|
||||
end
|
||||
|
||||
def generate_access_token
|
||||
@called_method = __method__.to_s
|
||||
@response = conn.post('tokens/generate') do |req|
|
||||
req.body = { apiKey: api_key, secretKey: api_secret }.to_json
|
||||
end
|
||||
@access_token, @refresh_token = parse('accessToken', 'refreshToken')
|
||||
end
|
||||
|
||||
def generate_refresh_token
|
||||
@called_method = __method__.to_s
|
||||
@response = conn.post('tokens/refresh') do |req|
|
||||
req.body = { accessToken: access_token, refreshToken: refresh_token }.to_json
|
||||
end
|
||||
@access_token, @refresh_token = parse('accessToken', 'refreshToken')
|
||||
end
|
||||
|
||||
def old_iban_info(iban)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.post('services/ibanInfo') do |req|
|
||||
req.body = { iban: iban }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
def iban_info(iban)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.get('ibans') do |req|
|
||||
req.params[:value] = iban
|
||||
end
|
||||
end
|
||||
|
||||
def old_card_info(card_number)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.post('services/cardInfo') do |req|
|
||||
req.body = { cardNumber: card_number }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
def card_info(card_number)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.get('cards') do |req|
|
||||
req.params[:number] = card_number
|
||||
end
|
||||
end
|
||||
|
||||
def old_mobile_info(number, national_code)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.post('services/matchNationalCodeAndMobileNumber') do |req|
|
||||
req.body = { nationalCode: national_code, mobile: number }.to_json
|
||||
end
|
||||
end
|
||||
|
||||
def mobile_info(number, national_code)
|
||||
@called_method = __method__.to_s
|
||||
conn.authorization :Bearer, access_token
|
||||
@response = conn.get('services/matching') do |req|
|
||||
req.params[:nationalCode] = national_code
|
||||
req.params[:mobileNumber] = number
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse(*keys)
|
||||
parsed_data = JSON.parse(response.body)
|
||||
keys.map { |key| parsed_data.dig(key) }
|
||||
end
|
||||
end
|
||||
43
app/services/kave_negar_sms_service.rb
Normal file
43
app/services/kave_negar_sms_service.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# kavenegar sms sender
|
||||
class KaveNegarSmsService
|
||||
class << self
|
||||
def send_confirmation(user)
|
||||
Rails.logger.info("Sending SMS to #{user.phones.last.number}")
|
||||
totp = TOTPServiceAction.new('register-mobile')
|
||||
totp.create(user.uid, user.email, period: '120')
|
||||
user.write_cache('register_mobile', 'true', 120)
|
||||
send_sms(user.mobile('pending').number.to_s, totp.read_code(user.uid))
|
||||
end
|
||||
|
||||
def send_call_confirmation(user)
|
||||
Rails.logger.info("Sending call to #{user.phones.find_by(category: 'landline').number}")
|
||||
totp = TOTPServiceAction.new('register-phone')
|
||||
totp.create(user.uid, user.email, period: '120')
|
||||
user.write_cache('register_phone', 'true', 120)
|
||||
send_call(user.phones.find_by(category: 'landline').number.to_s, totp.read_code(user.uid))
|
||||
end
|
||||
|
||||
def send_call(number, code)
|
||||
client = KaveRestApi::Lookup.new({ receptor: number,
|
||||
token: code,
|
||||
template: 'phone',
|
||||
type: 'call' # or call
|
||||
})
|
||||
|
||||
client.call.valid? ? true : false
|
||||
end
|
||||
|
||||
def send_sms(number, code)
|
||||
client = KaveRestApi::Lookup.new({ receptor: number,
|
||||
token: code,
|
||||
template: 'mobile',
|
||||
type: 'sms' # or call
|
||||
})
|
||||
|
||||
client.call.valid? ? true : false
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
65
app/services/kyc_service.rb
Normal file
65
app/services/kyc_service.rb
Normal file
@@ -0,0 +1,65 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class KycService
|
||||
# passport front + selfie OR driver license front and back + selfie IR id card front and back + selfie
|
||||
REQUIRED_DOC_AMOUNT = { 'Passport': 2, 'Driver license': 3, 'Identity card': 3 }.freeze
|
||||
|
||||
class << self
|
||||
def profile_step(profile)
|
||||
user = profile.user
|
||||
profile_label = user.labels.find_by(key: :profile)
|
||||
if profile_label.nil? # first profile everw
|
||||
user.labels.create(key: :profile, value: profile.state, scope: :private)
|
||||
else
|
||||
profile_label.update(value: profile.state) # re-submitted profile
|
||||
end
|
||||
return unless profile.state == 'verified'
|
||||
|
||||
# verify treasury after profile verified
|
||||
KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::TreasuryWorker.perform_async(profile.id)
|
||||
end
|
||||
|
||||
def document_step(document)
|
||||
user = document.user
|
||||
user_document_label = user.labels.find_by(key: document.doc_type.downcase)
|
||||
if user_document_label.nil? # first document ever
|
||||
user.labels.create(key: document.doc_type.downcase, value: document.state, scope: :private)
|
||||
else
|
||||
return if user_document_label.value == 'verified'
|
||||
|
||||
user_document_label.update(value: document.state) # re-submitted document
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def kycaid_callback(params)
|
||||
422
|
||||
end
|
||||
|
||||
def treasury_label_update(treasury)
|
||||
user = treasury&.user
|
||||
return unless user.present?
|
||||
|
||||
treasury_label = user.labels.find_by(key: treasury.kind)
|
||||
if treasury_label.nil? # first profile ever
|
||||
user.labels.create(key: treasury.kind, value: treasury.state, scope: :private)
|
||||
else
|
||||
treasury.ownership! if treasury.state == 'processing' && treasury.result.present?
|
||||
# user must have just one kind of treasury and new treasury like card, must not change before verified card
|
||||
treasury_label.update(value: treasury.reload.state) unless treasury_label.value == 'verified'
|
||||
end
|
||||
end
|
||||
|
||||
def phone_label_update(phone)
|
||||
user = phone&.user
|
||||
return unless user.present?
|
||||
|
||||
phone_label = user.labels.find_by(key: phone.step)
|
||||
if phone_label.nil?
|
||||
user.labels.create(key: phone.step, value: phone.state, scope: :private)
|
||||
else
|
||||
phone_label.update(value: phone.state)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
24
app/services/mock_phone_verify_service.rb
Normal file
24
app/services/mock_phone_verify_service.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# twilio sms sender
|
||||
class MockPhoneVerifyService
|
||||
class << self
|
||||
def send_confirmation(phone, _channel)
|
||||
Rails.logger.info("Sending SMS to #{phone.number}")
|
||||
|
||||
send_sms(number: phone.number,
|
||||
content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code))
|
||||
end
|
||||
|
||||
def send_sms(number:, content:)
|
||||
from_phone = Barong::App.config.twilio_phone_number
|
||||
client = Barong::MockSMS.new('', '')
|
||||
client.messages.create(from: from_phone, to: '+' + number, body: content)
|
||||
end
|
||||
|
||||
# always return true
|
||||
def verify_code?(number:, code:, user:)
|
||||
user.phones.find_by_number(number).present?
|
||||
end
|
||||
end
|
||||
end
|
||||
23
app/services/password_strength_checker.rb
Normal file
23
app/services/password_strength_checker.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# password entropy calculation
|
||||
class PasswordStrengthChecker
|
||||
class <<self
|
||||
# this method typically called by validate! from model and from API /pass/validate controller
|
||||
def calculate_entropy(password)
|
||||
@checker ||= StrongPassword::StrengthChecker.new(min_entropy: Barong::App.config.password_min_entropy,
|
||||
use_dictionary: Barong::App.config.password_use_dictionary)
|
||||
@checker.calculate_entropy(password)
|
||||
end
|
||||
|
||||
# User model invokes this method while validating password on create and update
|
||||
def validate!(password)
|
||||
password_regex = Barong::App.config.password_regexp
|
||||
return 'requirements.short' unless password_regex.match(password)
|
||||
|
||||
return 'weak' if calculate_entropy(password) < Barong::App.config.password_min_entropy
|
||||
|
||||
'strong'
|
||||
end
|
||||
end
|
||||
end
|
||||
57
app/services/secret_storage.rb
Normal file
57
app/services/secret_storage.rb
Normal file
@@ -0,0 +1,57 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SecretStorage
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class <<self
|
||||
|
||||
def server_available?
|
||||
read_data('sys/health').present?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def store_secret(secret, kid)
|
||||
write!(secret_path(kid), secret) unless exist?(kid)
|
||||
end
|
||||
|
||||
def get_secret(kid)
|
||||
read(secret_path(kid)) if exist?(kid)
|
||||
end
|
||||
|
||||
def exist?(kid)
|
||||
read(secret_path(kid)).present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def secret_path(kid)
|
||||
"secret/barong/api_key/#{kid}"
|
||||
end
|
||||
|
||||
def with_human_error
|
||||
yield
|
||||
rescue Vault::VaultError => error
|
||||
Rails.logger.error { error }
|
||||
raise Error, error.message
|
||||
end
|
||||
|
||||
def read(key)
|
||||
with_human_error do
|
||||
Vault.logical.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
def write!(key, params)
|
||||
with_human_error do
|
||||
Vault.logical.write(key, value: params)
|
||||
end
|
||||
end
|
||||
|
||||
def delete!(key)
|
||||
with_human_error do
|
||||
Vault.logical.delete(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
96
app/services/totp_service.rb
Normal file
96
app/services/totp_service.rb
Normal file
@@ -0,0 +1,96 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TOTPService
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
class <<self
|
||||
ISSUER_NAME = 'Barong'
|
||||
|
||||
def server_available?
|
||||
read_data('sys/health').present?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def otp_secret(otp)
|
||||
CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0]
|
||||
end
|
||||
|
||||
def safe_create(uid, email)
|
||||
return if exist?(uid)
|
||||
|
||||
create(uid, email)
|
||||
end
|
||||
|
||||
def create(uid, email)
|
||||
write_data(totp_key(uid),
|
||||
generate: true,
|
||||
issuer: ::Barong::App.config.app_name,
|
||||
account_name: email,
|
||||
qr_size: 300)
|
||||
end
|
||||
|
||||
def exist?(uid)
|
||||
read_data(totp_key(uid)).present?
|
||||
end
|
||||
|
||||
def validate?(uid, code)
|
||||
return false unless exist?(uid)
|
||||
|
||||
write_data(totp_code_key(uid), code: code).data[:valid]
|
||||
end
|
||||
|
||||
def delete(uid)
|
||||
delete_data(totp_key(uid))
|
||||
end
|
||||
|
||||
def with_human_error
|
||||
raise ArgumentError, 'Block is required' unless block_given?
|
||||
|
||||
yield
|
||||
rescue Vault::VaultError => e
|
||||
Rails.logger.error { e }
|
||||
|
||||
raise Error, '2FA server is under maintenance' if e.message.include?('connection refused')
|
||||
|
||||
raise Error, 'This code was already used. Wait until the next time period' if e.message.include?('code already used')
|
||||
|
||||
raise e
|
||||
end
|
||||
|
||||
def totp_key(uid)
|
||||
"totp/keys/#{Vault.application}_#{uid}"
|
||||
end
|
||||
|
||||
def totp_code_key(uid)
|
||||
"totp/code/#{Vault.application}_#{uid}"
|
||||
end
|
||||
|
||||
def read_data(key)
|
||||
with_human_error do
|
||||
vault.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
def read_code(uid)
|
||||
read_data(totp_code_key(uid)).data[:code]
|
||||
end
|
||||
|
||||
|
||||
def write_data(key, params)
|
||||
with_human_error do
|
||||
vault.write(key, params)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_data(key)
|
||||
with_human_error do
|
||||
vault.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def vault
|
||||
Vault.logical
|
||||
end
|
||||
end
|
||||
end
|
||||
104
app/services/totp_service_action.rb
Normal file
104
app/services/totp_service_action.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class TOTPServiceAction
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
ISSUER_NAME = 'Barong'
|
||||
attr_reader :action
|
||||
|
||||
def initialize(action)
|
||||
raise 'Please determine action for totp code' if action.blank?
|
||||
|
||||
@action = action
|
||||
end
|
||||
|
||||
def server_available?
|
||||
read_data('sys/health').present?
|
||||
rescue StandardError
|
||||
false
|
||||
end
|
||||
|
||||
def otp_secret(otp)
|
||||
CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0]
|
||||
end
|
||||
|
||||
def safe_create(uid, email)
|
||||
return if exist?(uid)
|
||||
|
||||
create(uid, email)
|
||||
end
|
||||
|
||||
def create(uid, email, period: '120')
|
||||
write_data(totp_key(uid),
|
||||
generate: true,
|
||||
issuer: ::Barong::App.config.app_name,
|
||||
period: period, account_name: email, qr_size: 300)
|
||||
end
|
||||
|
||||
def exist?(uid)
|
||||
read_data(totp_key(uid)).present?
|
||||
end
|
||||
|
||||
def validate?(uid, code)
|
||||
return false unless exist?(uid)
|
||||
|
||||
result = write_data(totp_code_key(uid), code: code)
|
||||
return false unless result
|
||||
|
||||
result.data[:valid]
|
||||
end
|
||||
|
||||
def safe_validate?(uid, code)
|
||||
return false unless exist?(uid)
|
||||
|
||||
code == read_code(uid)
|
||||
end
|
||||
|
||||
|
||||
def delete(uid)
|
||||
delete_data(totp_key(uid))
|
||||
end
|
||||
|
||||
def with_human_error
|
||||
raise ArgumentError, 'Block is required' unless block_given?
|
||||
|
||||
yield
|
||||
rescue Vault::VaultError => e
|
||||
Rails.logger.error { e }
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def totp_key(uid)
|
||||
"totp/keys/#{Vault.application}_#{@action}_#{uid}"
|
||||
end
|
||||
|
||||
def totp_code_key(uid)
|
||||
"totp/code/#{Vault.application}_#{@action}_#{uid}"
|
||||
end
|
||||
|
||||
def read_data(key)
|
||||
with_human_error do
|
||||
vault.read(key)
|
||||
end
|
||||
end
|
||||
|
||||
def read_code(uid)
|
||||
read_data(totp_code_key(uid)).data[:code]
|
||||
end
|
||||
|
||||
def write_data(key, params)
|
||||
with_human_error { vault.write(key, params) }
|
||||
end
|
||||
|
||||
def delete_data(key)
|
||||
with_human_error do
|
||||
vault.delete(key)
|
||||
end
|
||||
end
|
||||
|
||||
def vault
|
||||
Vault.logical
|
||||
end
|
||||
|
||||
end
|
||||
28
app/services/twilio_sms_send_service.rb
Normal file
28
app/services/twilio_sms_send_service.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# twilio sms sender
|
||||
class TwilioSmsSendService
|
||||
class << self
|
||||
def send_confirmation(phone, _channel)
|
||||
Rails.logger.info("Sending SMS to #{phone.number}")
|
||||
|
||||
send_sms(number: phone.number,
|
||||
content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code))
|
||||
end
|
||||
|
||||
def send_sms(number:, content:)
|
||||
from_phone = Barong::App.config.twilio_phone_number
|
||||
client = Barong::App.config.twilio_client
|
||||
client.messages.create(
|
||||
from: from_phone,
|
||||
to: '+' + number,
|
||||
body: content
|
||||
)
|
||||
end
|
||||
|
||||
# returns true if given code matches number in DB
|
||||
def verify_code?(number:, code:, user:)
|
||||
user.phones.find_by_number(number, code: code)
|
||||
end
|
||||
end
|
||||
end
|
||||
35
app/services/twilio_verify_service.rb
Normal file
35
app/services/twilio_verify_service.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# twilio process verification
|
||||
class TwilioVerifyService
|
||||
class << self
|
||||
def send_confirmation(phone, channel)
|
||||
Rails.logger.info("Sending code to #{phone.number} via #{channel}")
|
||||
|
||||
send_code(number: phone.number, channel: channel)
|
||||
end
|
||||
|
||||
def send_code(number:, channel:)
|
||||
verify_client.services(@service_sid)
|
||||
.verifications
|
||||
.create(to: '+' + number, channel: channel)
|
||||
end
|
||||
|
||||
# return true if twilio accepts given code for the given number
|
||||
def verify_code?(number:, code:, user:)
|
||||
status = verify_client.services(@service_sid)
|
||||
.verification_checks
|
||||
.create(to: '+' + number, code: code)
|
||||
.status
|
||||
|
||||
status == 'approved'
|
||||
end
|
||||
|
||||
def verify_client
|
||||
client = Barong::App.config.twilio_client
|
||||
@service_sid = Barong::App.config.twilio_service_sid
|
||||
|
||||
client.verify
|
||||
end
|
||||
end
|
||||
end
|
||||
10
app/services/uid_generator.rb
Normal file
10
app/services/uid_generator.rb
Normal file
@@ -0,0 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class UIDGenerator
|
||||
def self.generate(prefix = 'ID')
|
||||
loop do
|
||||
uid = "%s%s" % [prefix.upcase, SecureRandom.hex(5).upcase]
|
||||
return uid if User.where(uid: uid).empty? && ServiceAccount.where(uid: uid).empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user