Initial commit

This commit is contained in:
Yaser
2026-08-13 19:56:46 +03:30
commit de1d57a67b
474 changed files with 43185 additions and 0 deletions

18
app/models/ability.rb Normal file
View 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
View 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)
#

View 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
View 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
#

View File

@@ -0,0 +1,6 @@
class ApplicationRecord < ActiveRecord::Base
include Iso8601TimeFormat
self.abstract_class = true
end

32
app/models/city.rb Normal file
View 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
View 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)
#

View File

View 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

View 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

View 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

View 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

View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
#

View 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
View 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
View 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
#