Initial commit
This commit is contained in:
18
app/models/ability.rb
Normal file
18
app/models/ability.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Full list of roles abilities could be found on docs/roles.md
|
||||
class Ability
|
||||
class << self
|
||||
def abilities
|
||||
@abilities ||= YAML.load_file("#{Rails.root}/config/abilities.yml")
|
||||
end
|
||||
|
||||
def admin_permissions
|
||||
abilities['admin_permissions']
|
||||
end
|
||||
|
||||
def roles
|
||||
abilities['roles']
|
||||
end
|
||||
end
|
||||
end
|
||||
52
app/models/activity.rb
Normal file
52
app/models/activity.rb
Normal file
@@ -0,0 +1,52 @@
|
||||
class Activity < ApplicationRecord
|
||||
RESULTS = %w[succeed failed denied].freeze
|
||||
CATEGORIES = %w[admin user].freeze
|
||||
|
||||
belongs_to :user
|
||||
has_one :target, primary_key: :target_uid, foreign_key: :uid, class_name: 'User'
|
||||
|
||||
validates :user_ip, presence: true, allow_blank: false
|
||||
validates :user_agent, presence: true, trusty_agent: true
|
||||
validates :topic, presence: true
|
||||
validates :result, presence: true, inclusion: { in: RESULTS }
|
||||
validates :category, presence: true, inclusion: { in: CATEGORIES }
|
||||
validate :target_user
|
||||
|
||||
# this method allows to use all the methods of ::Browser module (platofrm, modern?, version etc)
|
||||
def browser
|
||||
Browser.new(user_agent)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def target_user
|
||||
errors.add(:target_uid, :invalid) if target_uid.present? && User.where(uid: target_uid).empty?
|
||||
errors.add(:target_uid, :not_allowed) if target_uid.present? && category.present? && category == 'user'
|
||||
end
|
||||
|
||||
def readonly?
|
||||
!new_record?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: activities
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint not null
|
||||
# target_uid :string(255)
|
||||
# category :string(255)
|
||||
# user_ip :string(255) not null
|
||||
# user_agent :string(255) not null
|
||||
# topic :string(255) not null
|
||||
# action :string(255) not null
|
||||
# result :string(255) not null
|
||||
# data :text(65535)
|
||||
# created_at :datetime
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_activities_on_target_uid (target_uid)
|
||||
# index_activities_on_user_id (user_id)
|
||||
#
|
||||
17
app/models/admin_ability.rb
Normal file
17
app/models/admin_ability.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AdminAbility
|
||||
include CanCan::Ability
|
||||
|
||||
def initialize(user)
|
||||
return if Ability.admin_permissions[user.role].nil?
|
||||
|
||||
# Iterate through user permissions
|
||||
Ability.admin_permissions[user.role].each do |action, models|
|
||||
# Iterate through a list of user model access
|
||||
models.each do |model|
|
||||
can action.to_sym, model == 'all' ? model.to_sym : model.constantize
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
90
app/models/api_key.rb
Normal file
90
app/models/api_key.rb
Normal file
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class APIKey < ApplicationRecord
|
||||
self.table_name = :apikeys
|
||||
|
||||
include Vault::EncryptedModel
|
||||
|
||||
ALGORITHMS = ['HS256'].freeze
|
||||
JWT_OPTIONS = {
|
||||
verify_expiration: true,
|
||||
verify_iat: true,
|
||||
verify_jti: true,
|
||||
sub: 'api_key_jwt',
|
||||
verify_sub: true,
|
||||
iss: 'external',
|
||||
verify_iss: true,
|
||||
algorithm: 'RS256'
|
||||
}.freeze
|
||||
|
||||
|
||||
vault_lazy_decrypt!
|
||||
|
||||
vault_attribute :secret
|
||||
|
||||
serialize :scope, Array
|
||||
|
||||
belongs_to :key_holder_account, polymorphic: true
|
||||
|
||||
validates :kid, :secret, presence: true
|
||||
validates :kid, uniqueness: true
|
||||
validates :algorithm, inclusion: { in: ALGORITHMS }
|
||||
|
||||
before_validation :assign_kid, if: :hmac?
|
||||
before_validation :validate_key_holder_state, on: :create
|
||||
before_validation :validate_api_key_state, on: :update
|
||||
|
||||
scope :active, -> { where(state: 'active') }
|
||||
|
||||
def assign_kid
|
||||
return unless kid.blank?
|
||||
|
||||
loop do
|
||||
self.kid = random_kid
|
||||
break unless APIKey.where(kid: kid).any?
|
||||
end
|
||||
end
|
||||
|
||||
def random_kid
|
||||
SecureRandom.hex(8)
|
||||
end
|
||||
|
||||
def hmac?
|
||||
self.algorithm.include?('HS')
|
||||
end
|
||||
|
||||
def active?
|
||||
self.state == 'active'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_key_holder_state
|
||||
errors.add(:key_holder_account, :invalid, message: 'non active state for key holder account') unless key_holder_account.active?
|
||||
end
|
||||
|
||||
def validate_api_key_state
|
||||
errors.add(:state, :invalid, message: 'cant activate api key with disabled key holder account') if active? && !key_holder_account.active?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: apikeys
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# key_holder_account_id :bigint unsigned, not null
|
||||
# key_holder_account_type :string(255) default("User"), not null
|
||||
# kid :string(255) not null
|
||||
# algorithm :string(255) not null
|
||||
# scope :string(255)
|
||||
# secret_encrypted :string(1024)
|
||||
# state :string(255) default("active"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# idx_apikey_on_account (key_holder_account_type,key_holder_account_id)
|
||||
# index_apikeys_on_kid (kid) UNIQUE
|
||||
#
|
||||
6
app/models/application_record.rb
Normal file
6
app/models/application_record.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
class ApplicationRecord < ActiveRecord::Base
|
||||
|
||||
include Iso8601TimeFormat
|
||||
|
||||
self.abstract_class = true
|
||||
end
|
||||
32
app/models/city.rb
Normal file
32
app/models/city.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
class City < ApplicationRecord
|
||||
belongs_to :province, required: true
|
||||
has_one :profile
|
||||
|
||||
validates :name, presence: true,
|
||||
length: 1..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-']+\z/,
|
||||
message: 'only allows letters, digits "-", "\'", and space'
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210425062048
|
||||
#
|
||||
# Table name: cities
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string(255)
|
||||
# province_id :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_cities_on_province_id (province_id)
|
||||
#
|
||||
# Foreign Keys
|
||||
#
|
||||
# fk_rails_ea36d40e0b (province_id => provinces.id)
|
||||
#
|
||||
24
app/models/comment.rb
Normal file
24
app/models/comment.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Comment < ApplicationRecord
|
||||
belongs_to :user
|
||||
|
||||
validates :title, :data, :author_uid, presence: true
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: comments
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint unsigned, not null
|
||||
# author_uid :string(16) not null
|
||||
# title :string(64) not null
|
||||
# data :text(65535) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_comments_on_user_id (user_id)
|
||||
#
|
||||
0
app/models/concerns/.keep
Normal file
0
app/models/concerns/.keep
Normal file
21
app/models/concerns/data_is_json_validator.rb
Normal file
21
app/models/concerns/data_is_json_validator.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Data json validation
|
||||
class DataIsJsonValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, data)
|
||||
return if data.nil?
|
||||
|
||||
unless validate_data_is_json!(data)
|
||||
record.errors.add(attribute, :invalid_format, message: 'data is not json compatible string')
|
||||
end
|
||||
end
|
||||
|
||||
def validate_data_is_json!(data)
|
||||
begin
|
||||
JSON.parse(data)
|
||||
true
|
||||
rescue JSON::ParserError => e
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
25
app/models/concerns/encryptable.rb
Normal file
25
app/models/concerns/encryptable.rb
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Encryptable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def attr_encrypted(*attributes)
|
||||
attributes.each do |attribute|
|
||||
define_method("#{attribute}=".to_sym) do |value|
|
||||
return if value.nil?
|
||||
|
||||
self.public_send(
|
||||
"#{attribute}_encrypted=".to_sym,
|
||||
EncryptionService.encrypt(value)
|
||||
)
|
||||
end
|
||||
|
||||
define_method(attribute) do
|
||||
value = self.public_send("#{attribute}_encrypted".to_sym)
|
||||
EncryptionService.decrypt(value) if value.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
app/models/concerns/iso8601_time_format.rb
Normal file
11
app/models/concerns/iso8601_time_format.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Convert time to utc 8601
|
||||
module Iso8601TimeFormat
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def format_iso8601_time(time)
|
||||
utc_time = time.respond_to?(:utc) ? time.utc : time
|
||||
utc_time&.iso8601
|
||||
end
|
||||
end
|
||||
23
app/models/concerns/model_caching.rb
Normal file
23
app/models/concerns/model_caching.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
module ModelCaching
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def write_cache(action, value, expiration_time)
|
||||
Rails.cache.write(cache_key_generator(action), value, expires_in: expiration_time.seconds)
|
||||
end
|
||||
|
||||
def read_cache(action)
|
||||
Rails.cache.read(cache_key_generator(action))
|
||||
end
|
||||
|
||||
def delete_cache(action)
|
||||
Rails.cache.delete(cache_key_generator(action))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cache_key_generator(action)
|
||||
class_name = self.class.name.to_s
|
||||
object_id = id.to_s
|
||||
"#{class_name}_#{object_id}_#{action}"
|
||||
end
|
||||
end
|
||||
13
app/models/concerns/trusty_agent_validator.rb
Normal file
13
app/models/concerns/trusty_agent_validator.rb
Normal file
@@ -0,0 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# User agent validator
|
||||
class TrustyAgentValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, _attribute, value)
|
||||
browser = Browser.new(value)
|
||||
return if browser.known?
|
||||
|
||||
return record.data = { note: 'Detected suspicious browser' }.to_json if record.data.nil?
|
||||
|
||||
record.data = JSON.parse(record.data).merge(note: 'Detected suspicious browser').to_json
|
||||
end
|
||||
end
|
||||
41
app/models/data_storage.rb
Normal file
41
app/models/data_storage.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Data Storage model
|
||||
class DataStorage < ApplicationRecord
|
||||
BLACKLISTED_TITLES = %w[document label profile phone user].freeze
|
||||
acts_as_eventable prefix: 'data_storage', on: %i[create update]
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :title, :data, presence: true
|
||||
validates_length_of :data, maximum: 5120 # maximum 5kb of data
|
||||
validates :data, data_is_json: true
|
||||
validates :title, uniqueness: { scope: :user_id, case_sensitive: false },
|
||||
inclusion: { in: UserStorageTitles.list }, exclusion: { in: BLACKLISTED_TITLES }
|
||||
|
||||
def as_json_for_event_api
|
||||
{
|
||||
user: user.as_json_for_event_api,
|
||||
title: title,
|
||||
data: data,
|
||||
created_at: format_iso8601_time(created_at),
|
||||
updated_at: format_iso8601_time(updated_at)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: data_storages
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint unsigned, not null
|
||||
# title :string(64) not null
|
||||
# data :text(65535) not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_data_storages_on_user_id_and_title (user_id,title) UNIQUE
|
||||
#
|
||||
100
app/models/document.rb
Normal file
100
app/models/document.rb
Normal file
@@ -0,0 +1,100 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# User document model
|
||||
class Document < ApplicationRecord
|
||||
include Encryptable
|
||||
|
||||
# acts_as_eventable prefix: 'document', on: %i[create]
|
||||
|
||||
mount_uploader :upload, Barong::App.config.uploader
|
||||
|
||||
enum state: { pending: 0, verified: 1, rejected: 2, replaced: 3 }
|
||||
attr_encrypted :doc_number
|
||||
|
||||
belongs_to :user
|
||||
|
||||
validates :doc_type, :upload, presence: true
|
||||
validates :doc_type, inclusion: { in: DocumentTypes.list }
|
||||
validates :doc_category, inclusion: { in: DocumentTypes.category_list }
|
||||
validates :doc_expire, presence: true, if: -> { Barong::App.config.required_docs_expire }
|
||||
validates :metadata, data_is_json: true
|
||||
validate :exist_verified
|
||||
|
||||
validates :doc_number, length: { maximum: 128 },
|
||||
format: {
|
||||
with: /\A[A-Za-z0-9\-\s]+\z/,
|
||||
message: 'only allows letters and digits'
|
||||
}, if: proc { |a| a.doc_number.present? }
|
||||
|
||||
validate :doc_expire_not_in_the_past, if: -> { Barong::App.config.required_docs_expire }
|
||||
before_save :start_document_kyc_verification, :save_doc_number_index
|
||||
|
||||
attr_writer :update_labels
|
||||
|
||||
def exist_verified
|
||||
errors.add(:document, 'already have verified') if user.documents.find_by(state: 'verified', doc_type: doc_type).present?
|
||||
end
|
||||
|
||||
def as_json_for_event_api
|
||||
{
|
||||
user: user.as_json_for_event_api,
|
||||
upload: CGI::escape(upload.url),
|
||||
doc_type: doc_type,
|
||||
doc_number: doc_number,
|
||||
doc_expire: doc_expire,
|
||||
metadata: metadata,
|
||||
created_at: format_iso8601_time(created_at),
|
||||
updated_at: format_iso8601_time(updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
def sub_masked_doc_number
|
||||
doc_number.sub(/(?<=\A.{2})(.*)(?=.{2}\z)/) { |match| '*' * match.length } if doc_number
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start_document_kyc_verification
|
||||
KycService.document_step(self)
|
||||
end
|
||||
|
||||
def update_labels
|
||||
@update_labels.nil? ? true : @update_labels
|
||||
end
|
||||
|
||||
def doc_expire_not_in_the_past
|
||||
return if doc_expire.blank?
|
||||
|
||||
errors.add(:doc_expire, :invalid) if doc_expire < Date.current
|
||||
end
|
||||
|
||||
def save_doc_number_index
|
||||
self.doc_number_index = SaltedCrc32.generate_hash(doc_number) if doc_number.present?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210714111301
|
||||
#
|
||||
# Table name: documents
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint unsigned, not null
|
||||
# upload :string(255)
|
||||
# doc_type :string(255)
|
||||
# doc_expire :date
|
||||
# doc_number_encrypted :string(255)
|
||||
# doc_number_index :bigint
|
||||
# doc_issue :date
|
||||
# doc_category :string(255)
|
||||
# identificator :string(255)
|
||||
# metadata :text(65535)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# state :integer default("pending"), not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_documents_on_doc_number_index (doc_number_index)
|
||||
# index_documents_on_user_id (user_id)
|
||||
#
|
||||
142
app/models/label.rb
Normal file
142
app/models/label.rb
Normal file
@@ -0,0 +1,142 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Resposible for storing configurations
|
||||
class Label < ApplicationRecord
|
||||
# acts_as_eventable prefix: 'label', on: %i[create update]
|
||||
|
||||
belongs_to :user
|
||||
|
||||
SCOPES = HashWithIndifferentAccess.new(public: 'public', private: 'private')
|
||||
|
||||
SCOPES.keys.each do |name|
|
||||
define_method "#{name}?" do
|
||||
scope == SCOPES[name]
|
||||
end
|
||||
end
|
||||
|
||||
scope :with_private_scope, -> { where(scope: 'private') }
|
||||
|
||||
validates :user_id, :key, :value, :scope, presence: true
|
||||
validates :scope,
|
||||
inclusion: { in: SCOPES.keys }
|
||||
|
||||
validates :key,
|
||||
length: 3..255,
|
||||
format: { with: /\A[a-z0-9_-]+\z/ },
|
||||
uniqueness: { scope: %i[user_id scope] }
|
||||
|
||||
validates :value,
|
||||
length: 3..255,
|
||||
format: { with: /\A[a-z0-9_-]+\z/ }
|
||||
|
||||
validate :regard_kyc_steps
|
||||
after_commit :update_state_if_label_defined, :update_level_if_label_defined, on: %i[create update]
|
||||
after_commit :run_owner_mobile, on: %i[create update]
|
||||
after_destroy :update_state_if_label_defined, :destroy_level_if_label_deleted
|
||||
|
||||
before_validation :normalize_fields
|
||||
|
||||
def as_json_for_event_api
|
||||
{
|
||||
id: id,
|
||||
key: key,
|
||||
value: value,
|
||||
description: description,
|
||||
user: user.as_json_for_event_api
|
||||
}
|
||||
end
|
||||
|
||||
def kyc_wanted_point
|
||||
point = Level.find_by(key: key)
|
||||
raise "not found this key: `#{key}` in levels" unless point.present?
|
||||
|
||||
[point.id, Level.sum_ex_points(point.id)]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def normalize_fields
|
||||
self.key = key.to_s.downcase.squish
|
||||
self.value = value.to_s.downcase.squish
|
||||
end
|
||||
|
||||
def update_state_if_label_defined
|
||||
return unless scope == 'private' || previous_changes[:scope]&.include?('private')
|
||||
|
||||
user.update_state
|
||||
end
|
||||
|
||||
def update_level_if_label_defined
|
||||
return unless scope == 'private' || previous_changes[:scope]&.include?('private')
|
||||
|
||||
user.update_level
|
||||
# TODO must be clear witch step need notification
|
||||
# send_document_review_notification if key == 'document'
|
||||
end
|
||||
|
||||
def destroy_level_if_label_deleted
|
||||
# just kyc labels
|
||||
if value == 'verified' && Level::KYC_LEVEL_KEYS.include?(key)
|
||||
raise "can not delete kyc key: `#{key}`"
|
||||
end
|
||||
|
||||
update_level_if_label_defined
|
||||
end
|
||||
|
||||
# TODO: Fix it when EventAPI will be added.
|
||||
def send_document_review_notification
|
||||
if value == 'verified'
|
||||
EventAPI.notify('system.document.verified', record: as_json_for_event_api)
|
||||
elsif value == 'rejected'
|
||||
EventAPI.notify('system.document.rejected', record: as_json_for_event_api)
|
||||
end
|
||||
end
|
||||
|
||||
# level confirm by labels
|
||||
# labels create from LEVEL table by key
|
||||
# every key in level table has points as id
|
||||
# so user must not has label out of its now level
|
||||
def regard_kyc_steps
|
||||
return unless scope == 'private' || previous_changes[:scope]&.include?('private')
|
||||
|
||||
# just kyc labels
|
||||
return unless ::BarongConfig.list['kyc_levels'].values.flatten.include?(key)
|
||||
|
||||
next_level = user.level + 1
|
||||
# this means user has latest level now and dont need check it more
|
||||
return if next_level > Level::LEVEL_POINTS.keys&.last&.to_i
|
||||
|
||||
ask_point, ex_points = kyc_wanted_point
|
||||
# check wanted point in correct area
|
||||
raise 'regard steps in KYC' if (ask_point + ex_points) > Level::LEVEL_POINTS.dig(next_level.to_s)
|
||||
end
|
||||
|
||||
# just trigger for check owner mobile
|
||||
def run_owner_mobile
|
||||
return unless user.access_phone.present?
|
||||
|
||||
if value == 'verified' && key == 'selfie'
|
||||
KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::OwnerMobileWorker.perform_async(user.access_phone.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210218135634
|
||||
#
|
||||
# Table name: labels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint unsigned, not null
|
||||
# key :string(255) not null
|
||||
# value :string(255) not null
|
||||
# scope :string(255) default("public"), not null
|
||||
# description :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_labels_on_user_id (user_id)
|
||||
# index_labels_on_user_id_and_key_and_scope (user_id,key,scope)
|
||||
#
|
||||
43
app/models/level.rb
Normal file
43
app/models/level.rb
Normal file
@@ -0,0 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Label level mapping model
|
||||
class Level < ApplicationRecord
|
||||
validates :key, :value, :description, presence: true
|
||||
validates :value, uniqueness: { scope: :key }
|
||||
|
||||
# points calculate with sum ids of record
|
||||
KYC_ONE_POINTS = ::BarongConfig.list['kyc_levels'][1].keys.sum + 1
|
||||
KYC_TWO_POINTS = KYC_ONE_POINTS + ::BarongConfig.list['kyc_levels'][2].keys.sum
|
||||
KYC_THREE_POINTS = KYC_TWO_POINTS + ::BarongConfig.list['kyc_levels'][3].keys.sum
|
||||
LEVEL_POINTS = { '1' => KYC_ONE_POINTS, '2' => KYC_TWO_POINTS, '3' => KYC_THREE_POINTS }.freeze
|
||||
LEVEL_ID_BOUNDS = { '0' => 1,
|
||||
'1' => ::BarongConfig.list['kyc_levels'][1].keys.last,
|
||||
'2' => ::BarongConfig.list['kyc_levels'][2].keys.last,
|
||||
'3' => ::BarongConfig.list['kyc_levels'][3].keys.last}.freeze
|
||||
KYC_LEVEL_IDS = (1..::BarongConfig.list['kyc_levels'].values.last.keys.last).freeze
|
||||
KYC_LEVEL_KEYS = ::BarongConfig.list['kyc_levels'].values.map(&:values).flatten
|
||||
|
||||
scope :sum_ex_points, ->(wanted_id) { where('id < (?)', wanted_id).sum(:id) }
|
||||
|
||||
class << self
|
||||
def what_level(points)
|
||||
return 0 if points < LEVEL_POINTS.dig('1')
|
||||
|
||||
return 1 if points < LEVEL_POINTS.dig('2')
|
||||
|
||||
2
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: levels
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# key :string(255) not null
|
||||
# value :string(255)
|
||||
# description :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
35
app/models/permission.rb
Normal file
35
app/models/permission.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Permissions model for RBAC
|
||||
class Permission < ApplicationRecord
|
||||
validates :role, :verb, :action, :path, presence: true
|
||||
|
||||
before_validation :upcase_action_verb
|
||||
|
||||
private
|
||||
|
||||
def upcase_action_verb
|
||||
return if action.blank? || verb.blank?
|
||||
|
||||
self.action.upcase!
|
||||
self.verb.upcase!
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: permissions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# action :string(255) not null
|
||||
# role :string(255) not null
|
||||
# verb :string(255) not null
|
||||
# path :string(255) not null
|
||||
# topic :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_permissions_on_topic (topic)
|
||||
#
|
||||
142
app/models/phone.rb
Normal file
142
app/models/phone.rb
Normal file
@@ -0,0 +1,142 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#
|
||||
# Class Phone
|
||||
#
|
||||
class Phone < ApplicationRecord
|
||||
include Encryptable
|
||||
|
||||
TWILIO_CHANNELS = %w[call sms].freeze
|
||||
DEFAULT_COUNTRY_CODE_COUNT = 2
|
||||
|
||||
belongs_to :user
|
||||
|
||||
enum state: { pending: 0, verified: 1, rejected: 2, replaced: 3 }
|
||||
enum step: { access_phone: 0, owner_phone: 1, telephone: 2 }
|
||||
enum category: { mobile: 0, landline: 1 }
|
||||
|
||||
attr_encrypted :number
|
||||
validates :number, phone: true
|
||||
|
||||
before_create :generate_code
|
||||
before_validation :parse_country
|
||||
before_validation :sanitize_number
|
||||
|
||||
before_save :save_number_index
|
||||
after_commit :start_phone_kyc_verification, on: %i[create update]
|
||||
scope :verified, -> { where.not(validated_at: nil) }
|
||||
|
||||
# validate :validate_phone_number
|
||||
|
||||
def validate_phone_number
|
||||
return errors.add(:number, 'number is not a phone') if number.match(/\A..(9)\d+/) && self.phone?
|
||||
end
|
||||
|
||||
#FIXME: Clean code below
|
||||
class << self
|
||||
def sanitize(unsafe_phone)
|
||||
unsafe_phone.to_s.gsub(/\D/, '')
|
||||
end
|
||||
|
||||
def parse(unsafe_phone)
|
||||
Phonelib.parse self.sanitize(unsafe_phone)
|
||||
end
|
||||
|
||||
def valid?(unsafe_phone)
|
||||
parse(unsafe_phone).valid?
|
||||
end
|
||||
|
||||
def valid_number(unsafe_phone)
|
||||
parse(unsafe_phone).national.gsub(/\s+/, "").delete_prefix("0")
|
||||
end
|
||||
|
||||
def international(unsafe_phone)
|
||||
parse(unsafe_phone).international(false)
|
||||
end
|
||||
|
||||
def find_by_number(number, attrs={})
|
||||
attrs.merge!(number_index: SaltedCrc32.generate_hash(number))
|
||||
find_by(attrs)
|
||||
end
|
||||
|
||||
def find_by_number!(number)
|
||||
find_by!(number_index: SaltedCrc32.generate_hash(number))
|
||||
end
|
||||
end
|
||||
|
||||
def sub_masked_number
|
||||
code_count = parse_code&.length
|
||||
code_count = DEFAULT_COUNTRY_CODE_COUNT unless code_count
|
||||
|
||||
if number.present?
|
||||
number.sub(/(?<=\A.{#{code_count}})(.*)(?=.{4}\z)/) { |match| '*' * match.length }
|
||||
else
|
||||
number
|
||||
end
|
||||
end
|
||||
|
||||
def check_owner_ship
|
||||
jibit = JibitService.new
|
||||
response = jibit.mobile_info(self.national_number, self.user.verified_profile.national_code)
|
||||
|
||||
JSON.parse(response.body).dig('matched').present?
|
||||
end
|
||||
|
||||
def national_number
|
||||
return if number.blank?
|
||||
|
||||
::Phonelib.parse(number).national.delete(' ').rjust(11, '0')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start_phone_kyc_verification
|
||||
::KycService.phone_label_update(self)
|
||||
end
|
||||
|
||||
def generate_code
|
||||
self.code = rand.to_s[2..6]
|
||||
end
|
||||
|
||||
def parse_country
|
||||
# data = Phonelib.parse(number)
|
||||
self.country = "IR"
|
||||
end
|
||||
|
||||
def parse_code
|
||||
data = Phonelib.parse(number)
|
||||
data.country_code
|
||||
end
|
||||
|
||||
def sanitize_number
|
||||
self.number = Phone.sanitize(number)
|
||||
end
|
||||
|
||||
def save_number_index
|
||||
self.number_index = SaltedCrc32.generate_hash(number) if number.present?
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210425062048
|
||||
#
|
||||
# Table name: phones
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :integer unsigned, not null
|
||||
# country :string(255) not null
|
||||
# code :string(5)
|
||||
# number_encrypted :string(255) not null
|
||||
# number_index :bigint not null
|
||||
# validated_at :datetime
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# category :integer not null
|
||||
# state :integer default("pending")
|
||||
# step :integer default("access_phone")
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_phones_on_number_index (number_index)
|
||||
# index_phones_on_user_id (user_id)
|
||||
#
|
||||
182
app/models/profile.rb
Normal file
182
app/models/profile.rb
Normal file
@@ -0,0 +1,182 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Profile model
|
||||
# in original version user has only one drafted or submitted at time but can has many verified and rejected
|
||||
# but in my version user can only one verified profile too
|
||||
# only drafted profile editable, so dont send confirm param in creation or update time
|
||||
class Profile < ApplicationRecord
|
||||
include Encryptable
|
||||
|
||||
# acts_as_eventable prefix: 'profile', on: %i[create update]
|
||||
|
||||
belongs_to :user
|
||||
belongs_to :city, required: false
|
||||
|
||||
enum state: { drafted: 0, submitted: 1, verified: 3, rejected: 4 }
|
||||
|
||||
EDITABLE_PARAMS = { drafted: %w[first_name last_name dob address postcode city country metadata national_code] }.freeze
|
||||
OPTIONAL_PARAMS = %w[first_name last_name dob address postcode city country national_code].freeze
|
||||
|
||||
attr_encrypted :first_name
|
||||
attr_encrypted :last_name
|
||||
attr_encrypted :dob
|
||||
attr_encrypted :address
|
||||
attr_encrypted :national_code
|
||||
|
||||
validates :first_name, presence: true,
|
||||
length: 1..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-']+\z/,
|
||||
message: 'only allows letters, digits "-", "\'", and space'
|
||||
},
|
||||
if: proc { |a| a.first_name.present? }
|
||||
validates :last_name, presence: true,
|
||||
length: 1..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-']+\z/,
|
||||
message: 'only allows letters, digits "-", "\'", and space'
|
||||
},
|
||||
if: proc { |a| a.last_name.present? }
|
||||
|
||||
validates :city, presence: false
|
||||
|
||||
validate :validate_country_format, if: ->(p) { p.country.present? }
|
||||
validates :postcode, length: 2..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-]+\z/,
|
||||
message: 'only allows letters, digits, "-" and space'
|
||||
},
|
||||
if: proc { |a| a.postcode.present? }
|
||||
|
||||
validates :address, length: 1..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-\–\,\.~;\/:\#"\\&\')\(]+\z/,
|
||||
message: 'only allows letters, digits "-", "–", "\'", ".", ",", "#", ":", ";", "&" and space'
|
||||
},
|
||||
if: proc { |a| a.address.present? }
|
||||
|
||||
validates :national_code, length: { is: 10 },
|
||||
format: { with: /\A\d+\z/, message: 'Ten Integer only. No sign allowed.' }
|
||||
|
||||
validates :metadata, data_is_json: true
|
||||
validate :profile_state!, on: :create
|
||||
# validate :profile_update!, on: :update
|
||||
validate :dob_not_in_the_future
|
||||
|
||||
after_commit :start_profile_kyc_verification
|
||||
|
||||
scope :kept, -> { joins(:user).where(users: { discarded_at: nil }) }
|
||||
|
||||
before_validation do
|
||||
squish_spaces
|
||||
end
|
||||
|
||||
def full_name
|
||||
"#{first_name} #{last_name}"
|
||||
end
|
||||
|
||||
def reverse_full_name
|
||||
"#{last_name} #{first_name}"
|
||||
end
|
||||
|
||||
def sub_masked_last_name
|
||||
last_name.sub(/(?<=\A.{1})(.*)/) { |match| '*' * match.length } if last_name
|
||||
end
|
||||
|
||||
def sub_masked_national_code
|
||||
national_code.sub(/(?<=\A.{5})(.*)/) { |match| '*' * match.length } if national_code
|
||||
end
|
||||
|
||||
def sub_masked_dob
|
||||
dob.to_s.sub(/(?<=\A.{8})(.*)/) { |match| '*' * match.length } if dob
|
||||
end
|
||||
|
||||
def as_json_for_event_api
|
||||
{
|
||||
user: user.as_json_for_event_api,
|
||||
first_name: first_name,
|
||||
last_name: last_name,
|
||||
dob: format_iso8601_time(dob),
|
||||
address: address,
|
||||
postcode: postcode,
|
||||
city: city,
|
||||
country: country,
|
||||
metadata: metadata,
|
||||
created_at: format_iso8601_time(created_at),
|
||||
updated_at: format_iso8601_time(updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
|
||||
def validate_country_format
|
||||
return if ISO3166::Country.find_country_by_alpha2(country) ||
|
||||
ISO3166::Country.find_country_by_alpha3(country)
|
||||
|
||||
errors.add(:country, 'must have alpha2 or alpha3 format')
|
||||
end
|
||||
|
||||
def squish_spaces
|
||||
self.first_name = first_name&.squish
|
||||
self.last_name = last_name&.squish
|
||||
self.postcode = postcode&.squish
|
||||
end
|
||||
|
||||
def profile_state!
|
||||
# No limits for storing drafted and rejected profiles
|
||||
# return if state.in?(%w[drafted rejected])
|
||||
return if state.in?(%w[drafted rejected])
|
||||
|
||||
# This check is actual for profile states [drafted submitted]
|
||||
# User cant create submitted or verified profile, when already had one of them
|
||||
user_profiles_states = user.profiles.pluck(:state)
|
||||
errors.add(:state, :exists, message: 'already exists') unless (user_profiles_states & %w[submitted verified]).empty?
|
||||
end
|
||||
|
||||
def profile_update!
|
||||
# we cant update rejected or verified profile
|
||||
errors.add(:state, :exists, message: 'just edit submitted') if %w[rejected verified].include?(state_was)
|
||||
end
|
||||
|
||||
def start_profile_kyc_verification
|
||||
KycService.profile_step(self)
|
||||
end
|
||||
|
||||
def dob_not_in_the_future
|
||||
return if dob.nil?
|
||||
|
||||
self.dob = dob.to_date
|
||||
return errors.add(:dob, :invalid_format, message: 'invalid date format') unless dob.is_a?(Date)
|
||||
return errors.add(:dob, :invalid, message: 'cant be in future') if dob > Date.current
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210425062048
|
||||
#
|
||||
# Table name: profiles
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint
|
||||
# author :string(255)
|
||||
# applicant_id :string(255)
|
||||
# first_name_encrypted :string(1024)
|
||||
# last_name_encrypted :string(1024)
|
||||
# dob_encrypted :string(255)
|
||||
# address_encrypted :string(1024)
|
||||
# postcode :string(255)
|
||||
# city :string(255)
|
||||
# country :string(255)
|
||||
# state :integer default("drafted"), unsigned
|
||||
# metadata :text(65535)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
# national_code_encrypted :string(255)
|
||||
# city_id :bigint
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_profiles_on_city_id (city_id)
|
||||
# index_profiles_on_user_id (user_id)
|
||||
#
|
||||
21
app/models/province.rb
Normal file
21
app/models/province.rb
Normal file
@@ -0,0 +1,21 @@
|
||||
class Province < ApplicationRecord
|
||||
has_many :cities
|
||||
|
||||
validates :name, presence: true,
|
||||
length: 1..255,
|
||||
format: {
|
||||
with: /\A[[:word:]\s\-']+\z/,
|
||||
message: 'only allows letters, digits "-", "\'", and space'
|
||||
}
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210420111532
|
||||
#
|
||||
# Table name: provinces
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# name :string(255)
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
56
app/models/restriction.rb
Normal file
56
app/models/restriction.rb
Normal file
@@ -0,0 +1,56 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Restriction < ApplicationRecord
|
||||
# please, note that order in CATEGORIES contstant defines the ierarchy
|
||||
CATEGORIES = %w[whitelist maintenance blacklist blocklogin].freeze
|
||||
SCOPES = %w[continent country ip ip_subnet all]
|
||||
# 423 Locked 403 Forbidden 401 Forbidden
|
||||
DEFAULT_CODES = { continent: 423, country: 423, ip_subnet: 403, ip: 401, all: 401 }.stringify_keys.freeze
|
||||
STATES = %w[enabled disabled]
|
||||
SUBNET_REGEX = /\A([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])\z/
|
||||
|
||||
validates :scope, :value, :category, presence: true
|
||||
validates :scope, inclusion: { in: SCOPES }
|
||||
validates :state, inclusion: { in: STATES }
|
||||
validates :category, inclusion: { in: CATEGORIES }
|
||||
|
||||
validates_uniqueness_of :value, scope: %i[scope category]
|
||||
|
||||
validates :value, if: -> { scope == 'ip' },
|
||||
format: { :with => Resolv::IPv4::Regex }
|
||||
|
||||
validates :value, if: -> { scope == 'ip_subnet' },
|
||||
format: { :with => SUBNET_REGEX }
|
||||
|
||||
before_validation :assign_code
|
||||
|
||||
after_save :destroy_sessions
|
||||
|
||||
private
|
||||
|
||||
def destroy_sessions
|
||||
if category == 'blocklogin' && state == 'enabled'
|
||||
Rails.cache.delete_matched('*_session_id*') if state_previously_changed? || created_at_previously_changed?
|
||||
end
|
||||
end
|
||||
|
||||
def assign_code
|
||||
return unless code.blank? || category == 'whitelist'
|
||||
|
||||
self.code = category == 'maintenance' ? 471 : DEFAULT_CODES[scope]
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
#
|
||||
# Table name: restrictions
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# category :string(255) not null
|
||||
# scope :string(64) not null
|
||||
# value :string(64) not null
|
||||
# code :integer
|
||||
# state :string(16) default("enabled"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
93
app/models/service_account.rb
Normal file
93
app/models/service_account.rb
Normal file
@@ -0,0 +1,93 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# ServiceAccount model
|
||||
class ServiceAccount < ApplicationRecord
|
||||
UID_PREFIX = 'SI'
|
||||
|
||||
belongs_to :user, foreign_key: "owner_id", optional: true
|
||||
has_many :api_keys, as: :key_holder_account, dependent: :destroy, class_name: 'APIKey'
|
||||
|
||||
validate :role_exists
|
||||
validate :email_n_uid_uniqueness
|
||||
validates :email, email: true, presence: true, uniqueness: true
|
||||
validates :uid, presence: true, uniqueness: true
|
||||
|
||||
scope :active, -> { where(state: 'active') }
|
||||
|
||||
after_update :disable_api_keys
|
||||
before_create :assign_state, if: -> { user.present? }
|
||||
# System will assign user state only if there is no changes of state during update
|
||||
before_update :assign_state, if: -> { user.present? && !state_changed? }
|
||||
before_validation :assign_level, if: -> { user.present? }
|
||||
before_validation :assign_uid
|
||||
before_validation :assign_email
|
||||
|
||||
def active?
|
||||
self.state == 'active'
|
||||
end
|
||||
|
||||
def disable_api_keys
|
||||
if state_previously_changed? && state == 'disabled'
|
||||
api_keys.active.each do |key|
|
||||
key.update(state: 'inactive')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def as_payload
|
||||
as_json(only: %i[uid email role level state])
|
||||
end
|
||||
|
||||
def role_exists
|
||||
return if Permission.pluck(:role).include?(role)
|
||||
|
||||
errors.add(:role, 'doesnt_exist')
|
||||
end
|
||||
|
||||
def email_n_uid_uniqueness
|
||||
errors.add('email_or_uid', 'not_uniq') if User.find_by(email: email) || User.find_by(uid: uid)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_email
|
||||
return unless email.blank?
|
||||
|
||||
name, domain = user.email.split('@')
|
||||
self.email = "#{name}+#{self.uid}@#{domain}"
|
||||
end
|
||||
|
||||
def assign_uid
|
||||
return unless uid.blank?
|
||||
|
||||
self.uid = UIDGenerator.generate(UID_PREFIX)
|
||||
end
|
||||
|
||||
def assign_state
|
||||
self.state = user.state
|
||||
end
|
||||
|
||||
def update_state
|
||||
!state_changed?
|
||||
end
|
||||
|
||||
def assign_level
|
||||
self.level = user.level
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210218135634
|
||||
#
|
||||
# Table name: service_accounts
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# uid :string(255) not null
|
||||
# owner_id :bigint unsigned
|
||||
# email :string(255) not null
|
||||
# role :string(255) default("service_account"), not null
|
||||
# level :integer default(0), not null
|
||||
# state :string(255) default("pending"), not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
129
app/models/treasury.rb
Normal file
129
app/models/treasury.rb
Normal file
@@ -0,0 +1,129 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Treasury model
|
||||
class Treasury < ApplicationRecord
|
||||
|
||||
belongs_to :user
|
||||
|
||||
enum state: { drafted: 0, submitted: 1, processing: 2, verified: 3, rejected: 4, disable: 5 }
|
||||
enum kind: { card: 0, iban: 2 }
|
||||
|
||||
validates :title, uniqueness: { scope: [:user_id, :kind], case_sensitive: false }, allow_blank: true
|
||||
|
||||
validates :data, length: { is: 16 }, if: -> { card? }, allow_blank: false,
|
||||
format: { with: /\A\d+\z/, message: 'Sixteen Integer only. No sign allowed.' }
|
||||
validates :data, uniqueness: true, length: { is: 26 }, if: -> { iban? }, allow_blank: false,
|
||||
format: { with: /\AIR\d+\z/i, message: 'Twenty-four Integer only. No sign allowed.' }
|
||||
|
||||
validate :data, :one_data_verified?
|
||||
|
||||
validates :title, presence: true
|
||||
|
||||
after_commit :start_treasury_kyc_verification, on: %i[create update]
|
||||
before_update :check_at_least_one_verified, if: -> { state == 'disable' }
|
||||
before_destroy :check_at_least_one_verified
|
||||
|
||||
|
||||
scope :active, -> { where.not(state: 'disable') }
|
||||
|
||||
def one_data_verified?
|
||||
errors.add(:data, 'this card is already verified') if Treasury.find_by(data: data, state: 'verified').present?
|
||||
end
|
||||
|
||||
def sub_masked_data
|
||||
data.sub(/(?<=\A.{6})(.*)/) { |match| '*' * match.length } if data
|
||||
end
|
||||
|
||||
# update without callback
|
||||
def ownership!
|
||||
if ownership
|
||||
update_column(:state, 'verified')
|
||||
else
|
||||
update_column(:state, 'rejected')
|
||||
end
|
||||
end
|
||||
|
||||
def ownership
|
||||
# full_name = if kind == 'card'
|
||||
# user.profiles.last.reverse_full_name.delete(' ')
|
||||
# else
|
||||
# user.profiles.last.full_name.delete(' ')
|
||||
# end
|
||||
return unless user.verified_profile.present?
|
||||
|
||||
full_name = user.verified_profile.full_name.delete(' ')
|
||||
owner_name.delete(' ').percent_match(full_name) >= 80
|
||||
end
|
||||
|
||||
def jresult
|
||||
return nil unless result.present?
|
||||
|
||||
JSON.parse(result)
|
||||
end
|
||||
|
||||
def old_owner_name
|
||||
return nil unless jresult.present?
|
||||
|
||||
kind == 'iban' ? "#{jresult.dig('firstName')} #{jresult.dig('lastName')}" : jresult.dig('ownerName')
|
||||
end
|
||||
|
||||
def owner_name
|
||||
return unless jresult.present?
|
||||
|
||||
if kind == 'iban'
|
||||
# JIBIT return array in names of owner
|
||||
names = jresult.dig('ibanInfo', 'owners')&.last
|
||||
"#{names.dig('firstName')} #{names.dig('lastName')}"
|
||||
else
|
||||
jresult.dig('cardInfo', 'ownerName')
|
||||
end
|
||||
end
|
||||
|
||||
def disable
|
||||
update(state: 'disable')
|
||||
end
|
||||
|
||||
def enable
|
||||
update(state: 'submitted')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def start_treasury_kyc_verification
|
||||
::KycService.treasury_label_update(self)
|
||||
end
|
||||
|
||||
def check_at_least_one_verified
|
||||
# return if state_was == 'verified'
|
||||
|
||||
raise 'verified treasury cant be deleted' if state == 'verified'
|
||||
|
||||
verified_treasuries = Treasury.where(user: user, kind: kind, state: 'verified')
|
||||
raise 'at least one verified treasury' if verified_treasuries.present? && verified_treasuries.count <= 1
|
||||
|
||||
# delete Corresponding label if not verified
|
||||
treasury_label = user.labels.find_by(key: kind)
|
||||
treasury_label.destroy unless treasury_label.value == 'verified'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210811064241
|
||||
#
|
||||
# Table name: treasuries
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# user_id :bigint unsigned, not null
|
||||
# title :string(128)
|
||||
# data :string(255) not null
|
||||
# result :text(65535)
|
||||
# state :integer default("drafted"), unsigned, not null
|
||||
# kind :integer default("card"), unsigned, not null
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_treasuries_on_user_id (user_id)
|
||||
#
|
||||
287
app/models/user.rb
Normal file
287
app/models/user.rb
Normal file
@@ -0,0 +1,287 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# User model
|
||||
class User < ApplicationRecord
|
||||
acts_as_eventable prefix: 'user', on: %i[create update]
|
||||
|
||||
include ModelCaching
|
||||
CHANGE_PASS_ATTEMPTS = 3
|
||||
has_secure_password
|
||||
|
||||
has_many :profiles, dependent: :destroy
|
||||
has_many :phones, dependent: :destroy
|
||||
has_many :data_storages, dependent: :destroy
|
||||
has_many :comments, dependent: :destroy
|
||||
has_many :documents, dependent: :destroy
|
||||
has_many :labels, dependent: :destroy
|
||||
has_many :activities, dependent: :destroy
|
||||
has_many :service_accounts, dependent: :destroy, foreign_key: 'owner_id'
|
||||
has_many :api_keys, dependent: :destroy, as: :key_holder_account, class_name: 'APIKey'
|
||||
has_many :treasuries, dependent: :destroy
|
||||
|
||||
validates_length_of :data, maximum: 1024
|
||||
validate :role_exists
|
||||
validate :referral_exists
|
||||
validates :data, data_is_json: true
|
||||
validates :email, email: true, presence: true, uniqueness: true
|
||||
validates :uid, presence: true, uniqueness: true
|
||||
validates :password, presence: true, if: :should_validate?
|
||||
validate :validate_pass!
|
||||
|
||||
scope :active, -> { where(state: 'active') }
|
||||
scope :with_pending_or_replaced_docs, -> { self.joins(:labels).where(labels:
|
||||
{ key: 'document', value: ['pending', 'replaced'], scope: 'private' }) }
|
||||
|
||||
before_validation :assign_uid
|
||||
before_validation :generate_password, on: :create
|
||||
after_update :disable_api_keys
|
||||
after_update :disable_service_accounts
|
||||
before_save :notfiy_new_level, if: :level_changed?
|
||||
before_save :notify_changed_otp, if: :otp_changed?
|
||||
|
||||
def notfiy_new_level
|
||||
::AMQP::Queue.enqueue_event("private" , self.uid, 'kyc', {msg: "new level is #{level}"})
|
||||
end
|
||||
|
||||
def notify_changed_otp
|
||||
::AMQP::Queue.enqueue_event("private" , self.uid, 'kyc', {msg: "otp state is #{otp}"})
|
||||
end
|
||||
|
||||
def generate_password
|
||||
self.password = SecureRandom.base64(30) unless password
|
||||
end
|
||||
|
||||
def validate_pass!
|
||||
return unless (new_record? && password.present?) || password.present?
|
||||
|
||||
validation_result = PasswordStrengthChecker.validate!(password)
|
||||
errors.add(:password, validation_result) unless validation_result == 'strong'
|
||||
end
|
||||
|
||||
def disable_service_accounts
|
||||
if state != 'active'
|
||||
service_accounts.each do |account|
|
||||
account.update(state: state)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def disable_api_keys
|
||||
if otp_previously_changed? && otp == false || state_previously_changed? && state != 'active'
|
||||
service_accounts.each do |service_account|
|
||||
service_account.api_keys.active.each do |key|
|
||||
key.update(state: 'inactive')
|
||||
end
|
||||
end
|
||||
api_keys.active.each do |key|
|
||||
key.update(state: 'inactive')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def active?
|
||||
self.state == 'active'
|
||||
end
|
||||
|
||||
def superadmin?
|
||||
self.role == 'superadmin'
|
||||
end
|
||||
|
||||
def role_exists
|
||||
return if Permission.pluck(:role).include?(role)
|
||||
|
||||
errors.add(:role, 'doesnt_exist')
|
||||
end
|
||||
|
||||
# Check if refferal exist for assignment
|
||||
def referral_exists
|
||||
errors.add(:referral_id, 'doesnt_exist') if referral_id.present? && User.find_by(id: referral_id).blank?
|
||||
end
|
||||
|
||||
def referral_uid
|
||||
user = User.find_by(id: referral_id)
|
||||
return if user.nil?
|
||||
|
||||
user.uid
|
||||
end
|
||||
|
||||
def role
|
||||
super.inquiry
|
||||
end
|
||||
|
||||
def should_validate?
|
||||
new_record? || password.present?
|
||||
end
|
||||
|
||||
def kyc_info
|
||||
user_points = 0
|
||||
last_level = 0
|
||||
tags = labels.with_private_scope.map { |l| [l.key, l.value].join ':' }
|
||||
levels = Level.all.order(id: :asc)
|
||||
levels.each do |lvl|
|
||||
# lvl.value always == 'verified'
|
||||
# so just verified label accepted
|
||||
break unless tags.include?(lvl.key + ':' + lvl.value)
|
||||
|
||||
user_points += lvl.id
|
||||
last_level = lvl.id
|
||||
end
|
||||
{ points: user_points.to_i, step: last_level.to_i, level: ::Level.what_level(user_points).to_i }
|
||||
end
|
||||
|
||||
def update_level
|
||||
update(level: Level.what_level(kyc_info.dig(:points))) unless level == kyc_info.dig(:level)
|
||||
end
|
||||
|
||||
def update_state
|
||||
@resulting_state = 'pending'
|
||||
|
||||
# check if user has all required labels for activation
|
||||
@resulting_state = 'active' if labels_include?(BarongConfig.list['activation_requirements'])
|
||||
|
||||
# FIXME BarongConfig should be a feature of Barong::App
|
||||
BarongConfig.list['state_triggers']&.each do |state, triggers|
|
||||
triggers.each { |trigger|
|
||||
# #TODO please fix it
|
||||
# if my key level is bank : user become banned
|
||||
labels.pluck(:key).each { |label| @resulting_state = state if label.start_with?(trigger) }
|
||||
}
|
||||
end
|
||||
update(state: @resulting_state) if @resulting_state != self.state
|
||||
end
|
||||
|
||||
# check if given key: values hash is a subset of private user labels
|
||||
def labels_include?(labels_hash)
|
||||
labels_hash <= private_labels_to_hash
|
||||
end
|
||||
|
||||
# Select all key-value pairs from user labels with private scope, merge in one hash
|
||||
def private_labels_to_hash
|
||||
key_value_arr = self.labels.with_private_scope.map do
|
||||
|l| { l.key => l.value }
|
||||
end
|
||||
key_value_hash = key_value_arr.inject(:merge)
|
||||
key_value_hash || {}
|
||||
end
|
||||
|
||||
def as_json_for_event_api
|
||||
{
|
||||
uid: uid,
|
||||
email: email,
|
||||
role: role,
|
||||
level: level,
|
||||
otp: otp,
|
||||
state: state,
|
||||
referral_uid: referral_uid,
|
||||
created_at: format_iso8601_time(created_at),
|
||||
updated_at: format_iso8601_time(updated_at)
|
||||
}
|
||||
end
|
||||
|
||||
def as_payload
|
||||
referral_uid = self.class.find_by(referral_id: referral_id).uid if referral_id.present?
|
||||
as_json(only: %i[uid email role level state otp]).merge(
|
||||
'referral_uid' => referral_uid,
|
||||
'last_change_pass' => change_pass_time,
|
||||
'cards' => cards&.pluck('data'),
|
||||
'ibans' => ibans&.pluck('data')
|
||||
)
|
||||
end
|
||||
|
||||
def language
|
||||
if data.blank?
|
||||
Barong::App.config.default_language.upcase
|
||||
else
|
||||
JSON.parse(data)['language'].upcase || Barong::App.config.default_language.upcase
|
||||
end
|
||||
end
|
||||
|
||||
def verified_profile
|
||||
profiles&.find_by(state: 'verified')
|
||||
end
|
||||
|
||||
def submitted_profile
|
||||
profiles&.find_by(state: 'submitted')
|
||||
end
|
||||
|
||||
def drafted_profile
|
||||
profiles&.where(state: 'drafted').last
|
||||
end
|
||||
|
||||
def front_icard
|
||||
documents.where(doc_type: 'Identity card', doc_category: 'front_side')&.last
|
||||
end
|
||||
|
||||
def selfie
|
||||
documents.where(doc_type: 'Selfie', doc_category: 'front_side')&.last
|
||||
end
|
||||
|
||||
def cards
|
||||
treasuries.where(kind: :card, state: :verified)
|
||||
end
|
||||
|
||||
def ibans
|
||||
treasuries.where(kind: :iban, state: :verified)
|
||||
end
|
||||
|
||||
def submitted_treasuries
|
||||
treasuries&.where(state: 'submitted')
|
||||
end
|
||||
|
||||
def last_password_activity
|
||||
activities.where(action: ['password reset', 'password change'], result: 'succeed', topic: 'password')
|
||||
.order('created_at DESC')&.last
|
||||
end
|
||||
|
||||
def change_pass_time
|
||||
last_password_activity&.created_at
|
||||
end
|
||||
|
||||
def access_phone
|
||||
phones.where(step: 'access_phone', category: 'mobile')&.last
|
||||
end
|
||||
|
||||
def mobile(state = nil)
|
||||
return phones.where(category: 'mobile').last unless state.present?
|
||||
|
||||
phones.where(category: 'mobile', state: state)&.last
|
||||
end
|
||||
|
||||
def landline
|
||||
return phones.where(category: 'landline').last unless step.present?
|
||||
|
||||
phones.where(category: 'landline', step: step).last
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assign_uid
|
||||
return unless uid.blank?
|
||||
|
||||
self.uid = UIDGenerator.generate(Barong::App.config.uid_prefix)
|
||||
end
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
# Schema version: 20210425062048
|
||||
#
|
||||
# Table name: users
|
||||
#
|
||||
# id :bigint not null, primary key
|
||||
# uid :string(255) not null
|
||||
# email :string(255) not null
|
||||
# password_digest :string(255) not null
|
||||
# role :string(255) default("member"), not null
|
||||
# data :text(65535)
|
||||
# level :integer default(0), not null
|
||||
# otp :boolean default(FALSE)
|
||||
# state :string(255) default("pending"), not null
|
||||
# referral_id :bigint
|
||||
# created_at :datetime not null
|
||||
# updated_at :datetime not null
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_users_on_email (email) UNIQUE
|
||||
# index_users_on_uid (uid) UNIQUE
|
||||
#
|
||||
Reference in New Issue
Block a user