Initial commit
This commit is contained in:
50
app/api/v2/resource/addresses.rb
Normal file
50
app/api/v2/resource/addresses.rb
Normal file
@@ -0,0 +1,50 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
# Addresses API
|
||||
class Addresses < Grape::API
|
||||
desc 'Documents related routes'
|
||||
resource :addresses do
|
||||
desc 'Upload a new address approval document for current user(this is what ui using now)',
|
||||
success: { code: 201, message: 'New address approval document was uploaded' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :country,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'country name'
|
||||
requires :address,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'address'
|
||||
requires :upload,
|
||||
desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
requires :city,
|
||||
allow_blank: true,
|
||||
desc: 'city name'
|
||||
requires :postcode, type: String, desc: 'Any additional key: value pairs in json string format'
|
||||
end
|
||||
|
||||
post do
|
||||
identificator = SecureRandom.hex(16)
|
||||
params[:upload].each do |file|
|
||||
doc = current_user.documents.new(upload: file, identificator: identificator, doc_type: 'Poa', doc_category: 'front_side')
|
||||
|
||||
code_error!(doc.errors.details, 422) unless doc.save
|
||||
end
|
||||
|
||||
status 201
|
||||
|
||||
rescue Excon::Error => e
|
||||
Rails.logger.error e
|
||||
error!('Connection error', 422)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
159
app/api/v2/resource/api_keys.rb
Normal file
159
app/api/v2/resource/api_keys.rb
Normal file
@@ -0,0 +1,159 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
# Responsible for CRUD for api keys
|
||||
class APIKeys < Grape::API
|
||||
helpers ::API::V2::NamedParams
|
||||
helpers do
|
||||
def skip_api_key_2fa?
|
||||
ENV['BARONG_SKIP_API_KEY_2FA'] == 'true'
|
||||
end
|
||||
|
||||
def otp_protected!
|
||||
return if skip_api_key_2fa?
|
||||
|
||||
unless current_user.otp
|
||||
error!({ errors: ['resource.api_key.2fa_disabled'] }, 400)
|
||||
end
|
||||
error!({ errors: ['resource.api_key.missing_totp'] }, 422) unless params[:totp_code].present?
|
||||
|
||||
return if TOTPService.validate?(current_user.uid, params[:totp_code])
|
||||
|
||||
error!({ errors: ['resource.api_key.invalid_totp'] }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
resource :api_keys do
|
||||
desc 'Create an api key',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
requires :algorithm,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'API key algorithm'
|
||||
optional :scope,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Comma separated scopes'
|
||||
optional :totp_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post do
|
||||
otp_protected!
|
||||
declared_params = declared(params, include_missing: false)
|
||||
.except(:totp_code)
|
||||
.merge(scope: params[:scope]&.split(','))
|
||||
.merge(secret: SecureRandom.hex(16))
|
||||
|
||||
api_key = current_user.api_keys.new(declared_params)
|
||||
|
||||
APIKey.transaction do
|
||||
raise ActiveRecord::Rollback unless api_key.save
|
||||
rescue Vault::VaultError
|
||||
api_key.errors.add(:api_key, 'could_not_save_secret')
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
if api_key.errors.any?
|
||||
code_error!(api_key.errors.details, 422)
|
||||
end
|
||||
|
||||
present api_key, with: Entities::APIKey
|
||||
end
|
||||
|
||||
desc 'Updates an api key',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
requires :kid,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'API key kid'
|
||||
optional :scope,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Comma separated scopes'
|
||||
optional :state,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'State of API Key. "active" state means key is active and can be used for auth'
|
||||
optional :totp_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
patch ':kid' do
|
||||
otp_protected!
|
||||
declared_params = declared(params, include_missing: false)
|
||||
.except(:totp_code)
|
||||
.merge(scope: params[:scope]&.split(','))
|
||||
api_key = current_user.api_keys.find_by!(kid: params[:kid])
|
||||
|
||||
unless api_key.update(declared_params)
|
||||
code_error!(api_key.errors.details, 422)
|
||||
end
|
||||
|
||||
present api_key, with: Entities::APIKey, except: [:secret]
|
||||
end
|
||||
|
||||
desc 'Delete an api key',
|
||||
success: { code: 204, message: 'Succefully deleted' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
params do
|
||||
requires :kid,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'API key kid'
|
||||
optional :totp_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
delete ':kid' do
|
||||
otp_protected!
|
||||
api_key = current_user.api_keys.find_by!(kid: params[:kid])
|
||||
api_key.destroy
|
||||
status 204
|
||||
end
|
||||
|
||||
desc 'List all api keys for current account.',
|
||||
failure: [
|
||||
{ code: 400, message: 'Require 2FA and totp code' },
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
optional :ordering,
|
||||
values: { value: -> (p){ %w[asc desc].include?(p) }, message: 'resource.api_key.invalid_ordering' },
|
||||
default: 'asc',
|
||||
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
|
||||
optional :order_by,
|
||||
values: { value: -> (p){ APIKey.new.attributes.keys.include?(p) }, message: 'resource.api_key.invalid_attribute' },
|
||||
default: 'id',
|
||||
desc: 'Name of the field, which result will be ordered by.'
|
||||
use :pagination_filters
|
||||
end
|
||||
get do
|
||||
current_user.api_keys.order(params[:order_by] => params[:ordering]).tap { |q| present paginate(q), with: Entities::APIKey, except: [:secret] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
36
app/api/v2/resource/base.rb
Normal file
36
app/api/v2/resource/base.rb
Normal file
@@ -0,0 +1,36 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_dependency 'barong/middleware/jwt_authenticator'
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
class Base < Grape::API
|
||||
use Barong::Middleware::JWTAuthenticator, \
|
||||
pubkey: Rails.configuration.x.keystore.public_key
|
||||
|
||||
helpers API::V2::Resource::Utils
|
||||
|
||||
do_not_route_options!
|
||||
|
||||
mount Resource::Addresses
|
||||
mount Resource::Users
|
||||
mount Resource::Labels
|
||||
mount Resource::Profiles
|
||||
mount Resource::Documents
|
||||
mount Resource::Phones
|
||||
mount Resource::Otp
|
||||
mount Resource::APIKeys
|
||||
mount Resource::DataStorage
|
||||
mount Resource::ServiceAccounts
|
||||
|
||||
add_swagger_documentation security_definitions: {
|
||||
'BearerToken': {
|
||||
description: 'Bearer Token authentication',
|
||||
type: 'basic',
|
||||
name: 'Authorization',
|
||||
in: 'header'
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
39
app/api/v2/resource/data_storage.rb
Normal file
39
app/api/v2/resource/data_storage.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module V2
|
||||
module Resource
|
||||
# data storage C functionality
|
||||
class DataStorage < Grape::API
|
||||
resource :data_storage do
|
||||
desc 'Create data storage',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 201, message: 'Data Storage was created' }
|
||||
params do
|
||||
requires :title,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Storage title'
|
||||
requires :data,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Storage data'
|
||||
end
|
||||
post do
|
||||
declared_params = declared(params)
|
||||
|
||||
data_storage = current_user.data_storages.new(declared_params)
|
||||
|
||||
code_error!(data_storage.errors.details, 422) unless data_storage.save
|
||||
|
||||
current_user.labels.create(key: data_storage.title, value: 'recorded', scope: 'private')
|
||||
status 201
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
78
app/api/v2/resource/documents.rb
Normal file
78
app/api/v2/resource/documents.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
# Dpcuments API
|
||||
class Documents < Grape::API
|
||||
desc 'Documents related routes'
|
||||
resource :documents do
|
||||
desc 'Return current user documents list',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: Entities::Document
|
||||
get do
|
||||
present current_user.documents, with: Entities::Document, only: %i[upload doc_type doc_number doc_expire]
|
||||
end
|
||||
|
||||
desc 'Upload a new document for current user',
|
||||
success: { code: 201, message: 'Document is uploaded' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :doc_type,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Document type'
|
||||
requires :doc_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Document number'
|
||||
requires :upload,
|
||||
desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
optional :doc_expire,
|
||||
type: { value: Date, message: "resource.documents.expire_not_a_date" },
|
||||
allow_blank: true,
|
||||
desc: 'Document expiration date'
|
||||
optional :doc_category,
|
||||
type: String,
|
||||
values: { value: -> (p){ %w[front_side back_side selfie].include?(p) }, message: 'resource.documents.doc_category' },
|
||||
desc: 'Category of the submitted document - front/back/selfie etc.'
|
||||
optional :identificator, type: String, desc: 'Identificator for documents to be supplied together'
|
||||
optional :metadata, type: String, desc: 'Any additional key: value pairs in json string format'
|
||||
end
|
||||
|
||||
post do
|
||||
if Barong::App.config.required_docs_expire
|
||||
error!({ errors: ['resource.documents.invalid_format'] }, 422) unless /\A\d{4}\-\d{2}\-\d{2}\z/.match?(params[:doc_expire].to_s)
|
||||
|
||||
error!({ errors: ['resource.documents.already_expired'] }, 422) if params[:doc_expire] < DateTime.now.to_date
|
||||
end
|
||||
|
||||
unless current_user.documents.count <= Barong::App.config.doc_num_limit
|
||||
error!({ errors: ['resource.documents.limit_reached'] }, 400)
|
||||
end
|
||||
|
||||
unless current_user.documents.count + params[:upload].length <= Barong::App.config.doc_num_limit
|
||||
error!({ errors: ['resource.documents.limit_will_be_reached'] }, 400)
|
||||
end
|
||||
params[:identificator] = SecureRandom.hex(16) unless params[:identificator]
|
||||
|
||||
params[:upload].each do |file|
|
||||
doc = current_user.documents.new(params.except(:upload).merge(upload: file))
|
||||
code_error!(doc.errors.details, 422) unless doc.save
|
||||
end
|
||||
|
||||
status 201
|
||||
|
||||
rescue Excon::Error => e
|
||||
Rails.logger.error e
|
||||
error!('Connection error', 422)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
127
app/api/v2/resource/labels.rb
Normal file
127
app/api/v2/resource/labels.rb
Normal file
@@ -0,0 +1,127 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
module V2
|
||||
module Resource
|
||||
# Responsible for CRUD for labes
|
||||
class Labels < Grape::API
|
||||
resource :labels do
|
||||
desc 'List all labels for current user.',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: Entities::Label
|
||||
params do
|
||||
optional :ordering,
|
||||
type: String,
|
||||
values: { value: %w(asc desc) },
|
||||
default: 'asc',
|
||||
desc: 'If set, returned labels sorted in specific order, default to "asc".'
|
||||
end
|
||||
get do
|
||||
labels = current_user.labels.order(created_at: params[:ordering])
|
||||
present labels, with: Entities::Label
|
||||
end
|
||||
|
||||
desc 'Return a label by key.',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
],
|
||||
success: Entities::Label
|
||||
params do
|
||||
requires :key,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label key.'
|
||||
end
|
||||
route_param :key do
|
||||
get do
|
||||
label = current_user.labels.find_by!(key: params[:key])
|
||||
present label, with: Entities::Label
|
||||
end
|
||||
end
|
||||
|
||||
desc "Create a label with 'public' scope.",
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: Entities::Label
|
||||
params do
|
||||
requires :key,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label key.'
|
||||
requires :value,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label value.'
|
||||
end
|
||||
post do
|
||||
label =
|
||||
current_user.labels.new(
|
||||
key: params[:key],
|
||||
value: params[:value],
|
||||
scope: 'public'
|
||||
)
|
||||
if label.save
|
||||
present label, with: Entities::Label
|
||||
else
|
||||
code_error!(label.errors.details, 422)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Update a label with 'public' scope.",
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: Entities::Label
|
||||
params do
|
||||
requires :key,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label key.'
|
||||
requires :value,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label value.'
|
||||
end
|
||||
patch ':key' do
|
||||
label = current_user.labels.find_by!(key: params[:key])
|
||||
return error!({ errors: ['resource.labels.private'] }, 400) if label.private?
|
||||
|
||||
label.update(value: params[:value])
|
||||
present label, with: Entities::Label
|
||||
end
|
||||
|
||||
desc "Delete a label with 'public' scope.",
|
||||
success: { code: 204, message: 'Succefully deleted' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
params do
|
||||
requires :key,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Label key.'
|
||||
end
|
||||
delete ':key' do
|
||||
label = current_user.labels.find_by!(key: params[:key])
|
||||
return error!({ errors: ['resource.labels.private'] }, 400) if label.private?
|
||||
|
||||
label.destroy
|
||||
status 204
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
220
app/api/v2/resource/otp.rb
Normal file
220
app/api/v2/resource/otp.rb
Normal file
@@ -0,0 +1,220 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
# TOTP functionality API
|
||||
class Otp < Grape::API
|
||||
helpers do
|
||||
def otp_error!(options = {})
|
||||
options[:topic] = 'otp'
|
||||
record_error!(options)
|
||||
end
|
||||
end
|
||||
|
||||
resource :otp do
|
||||
desc 'Generate qr code for 2FA (first step for enabling)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has been enabled for this account' },
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: { code: 200, message: 'QR code was generated' }
|
||||
post '/generate_qrcode' do
|
||||
if current_user.otp
|
||||
otp_error!(reason: '2FA has been already enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'request QR code for 2FA', error_text: 'already_enabled')
|
||||
end
|
||||
|
||||
if current_user.read_cache('enable_otp')
|
||||
otp_error!(reason: '2FA has been already sent email for this account', error_code: 400,
|
||||
user: current_user.id, action: 'request QR code for 2FA', error_text: 'already_sent')
|
||||
end
|
||||
|
||||
activity_record(user: current_user.id, action: 'request QR code for 2FA', result: 'succeed', topic: 'otp')
|
||||
TOTPService.create(current_user.uid, current_user.email)
|
||||
end
|
||||
|
||||
desc 'enter google authenticator code and send authorization code by email(second step for enabling)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has been enabled for this account or code is missing' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: '2FA was enabled' }
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
|
||||
post '/enable' do
|
||||
if current_user.otp
|
||||
otp_error!(reason: '2FA has been already enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'already_enabled')
|
||||
end
|
||||
|
||||
unless TOTPService.validate?(current_user.uid, declared(params)[:code])
|
||||
otp_error!(reason: 'OTP code is invalid', error_code: 422,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'invalid')
|
||||
end
|
||||
|
||||
if current_user.read_cache('enable_otp')
|
||||
otp_error!(reason: '2FA has been already sent email for this account', error_code: 400,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'already_enabled')
|
||||
end
|
||||
|
||||
# unless current_user.update(otp: true)
|
||||
# # FIXME active record validation
|
||||
# otp_error!(reason: current_user.errors.full_messages.to_sentence, error_code: 422,
|
||||
# user: current_user.id, action: 'enable 2FA')
|
||||
# end
|
||||
|
||||
# current_user.labels.create(key: :otp, value: :enabled, scope: :private) unless current_user.labels.find_by(key: :otp, scope: :private)
|
||||
# activity_record(user: current_user.id, action: 'enable 2FA', result: 'succeed', topic: 'otp')
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, 'enable-otp')
|
||||
current_user.write_cache('enable_otp', 'true', 120)
|
||||
200
|
||||
end
|
||||
|
||||
desc 'enter authorization key and enable 2fa(third step for enabling)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has been enabled for this account or code is missing' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: '2FA was enabled' }
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from email'
|
||||
end
|
||||
|
||||
post '/enable_2fa' do
|
||||
if current_user.otp
|
||||
otp_error!(reason: '2FA has been already enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'already_enabled')
|
||||
end
|
||||
|
||||
unless current_user.read_cache("enable_otp")
|
||||
otp_error!(reason: '2FA hasnt been enable', error_code: 400,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'doesnt get email')
|
||||
end
|
||||
|
||||
unless TOTPServiceAction.new('enable-otp').validate?(current_user.uid, declared(params)[:code])
|
||||
otp_error!(reason: 'OTP code is invalid', error_code: 422,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'invalid')
|
||||
end
|
||||
|
||||
unless current_user.update(otp: true)
|
||||
# FIXME active record validation
|
||||
otp_error!(reason: current_user.errors.full_messages.to_sentence, error_code: 422,
|
||||
user: current_user.id, action: 'enable 2FA')
|
||||
end
|
||||
|
||||
current_user.labels.create(key: :otp, value: :enabled, scope: :private) unless current_user.labels.find_by(key: :otp, scope: :private)
|
||||
activity_record(user: current_user.id, action: 'enable 2FA', result: 'succeed', topic: 'otp')
|
||||
|
||||
|
||||
# publish_confirmation_code(current_user,Barong::App.config.domain, action)
|
||||
current_user.delete_cache("enable_otp")
|
||||
200
|
||||
end
|
||||
|
||||
desc 'Disable 2FA request and send authorization code(first step disabling)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has not been enabled for this account or code is missing' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: '2FA was disabled' }
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post '/disable' do
|
||||
unless current_user.otp
|
||||
otp_error!(reason: '2FA has not been enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'disable 2FA', error_text: 'not_enabled')
|
||||
end
|
||||
|
||||
unless TOTPService.validate?(current_user.uid, declared(params)[:code])
|
||||
otp_error!(reason: 'OTP code is invalid', error_code: 422,
|
||||
user: current_user.id, action: 'disable 2FA', error_text: 'invalid')
|
||||
end
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, 'disable-otp')
|
||||
current_user.write_cache('disable_otp', 'true', 120)
|
||||
status 200
|
||||
end
|
||||
|
||||
desc 'enter authorization code that send by email (second step for disabling 2FA)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has been enabled for this account or code is missing' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: '2FA was disabled' }
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post '/disable_email' do
|
||||
unless current_user.otp
|
||||
otp_error!(reason: '2FA has not been enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'disable 2FA', error_text: 'not_enabled')
|
||||
end
|
||||
|
||||
unless current_user.read_cache("disable_otp")
|
||||
otp_error!(reason: '2FA hasnt been disable', error_code: 400,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'doesnt get email')
|
||||
end
|
||||
|
||||
unless TOTPServiceAction.new('disable-otp').validate?(current_user.uid, declared(params)[:code])
|
||||
otp_error!(reason: 'OTP code is invalid', error_code: 422,
|
||||
user: current_user.id, action: 'enable 2FA', error_text: 'invalid')
|
||||
end
|
||||
|
||||
unless current_user.update(otp: false)
|
||||
otp_error!(reason: current_user.errors.full_messages.to_sentence, error_code: 422,
|
||||
user: current_user.id, action: 'disable 2FA')
|
||||
end
|
||||
|
||||
current_user.labels.find_by(key: :otp, scope: :private).delete if current_user.labels.find_by(key: :otp, scope: :private)
|
||||
activity_record(user: current_user.id, action: 'disable 2FA', result: 'succeed', topic: 'otp')
|
||||
|
||||
status 200
|
||||
end
|
||||
desc 'Verify 2FA code(for checking 2fa)',
|
||||
failure: [
|
||||
{ code: 400, message: '2FA has not been enabled for this account or code is missing' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: '2FA was verified' }
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post '/verify' do
|
||||
unless current_user.otp
|
||||
otp_error!(reason: '2FA has not been enabled for this account', error_code: 400,
|
||||
user: current_user.id, action: 'verify 2FA code', error_text: 'already_enabled')
|
||||
end
|
||||
|
||||
unless TOTPService.validate?(current_user.uid, declared(params)[:code])
|
||||
otp_error!(reason: 'OTP code is invalid', error_code: 422,
|
||||
user: current_user.id, action: 'verify 2FA code', error_text: 'invalid')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
267
app/api/v2/resource/phones.rb
Normal file
267
app/api/v2/resource/phones.rb
Normal file
@@ -0,0 +1,267 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
class Phones < Grape::API
|
||||
rescue_from(Twilio::REST::RestError) do |error|
|
||||
Rails.logger.error "Twilio Client Error: #{error.message}"
|
||||
error!({ errors: [twilio_dictionary_error(error.code)] }, 422)
|
||||
end
|
||||
|
||||
helpers do
|
||||
def validate_phone!(phone_number, landline = nil)
|
||||
phone_number = Phone.international(phone_number)
|
||||
|
||||
error!({ errors: ['resource.phone.invalid_num'] }, 400) \
|
||||
unless Phone.valid?(phone_number)
|
||||
|
||||
error!({ errors: ['resource.phone.number_exist'] }, 400) \
|
||||
if Phone.verified.find_by_number(phone_number) && landline.blank?
|
||||
|
||||
# check phone is landline
|
||||
error!({ errors: ['resource.phone.number_not_landline'] }, 400) \
|
||||
if landline.present? && phone_number[2] == '9'
|
||||
end
|
||||
end
|
||||
|
||||
desc 'mobile related routes'
|
||||
resource :mobiles do
|
||||
desc 'Returns list of user\'s phones',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
],
|
||||
success: Entities::Phone
|
||||
get do
|
||||
present current_user.phones, with: Entities::Phone
|
||||
end
|
||||
|
||||
desc 'Add new mobile',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: 'New phone was added' }
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Phone number with country code(in iran +98)'
|
||||
optional :channel,
|
||||
type: String,
|
||||
default: 'sms',
|
||||
values: { value: -> { Phone::TWILIO_CHANNELS }, message: 'resource.phone.invalid_channel'},
|
||||
desc: 'The verification method to use(this param is for legacy structure must be sms)'
|
||||
end
|
||||
post do
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 2
|
||||
|
||||
error!({ errors: ['resource.phone.code_doesnt_expired'] }, 422) if current_user.read_cache('register-mobile')
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number])
|
||||
phone_number = Phone.international(declared_params[:phone_number])
|
||||
# label = current_user.labels.find_by(key: 'access_phone', value: 'verified')
|
||||
# error!({ errors: ['resource.phone.exists'] }, 422) if label.present?
|
||||
if current_user.mobile('pending').present?
|
||||
current_user.mobile('pending').update(number: phone_number)
|
||||
else
|
||||
current_user.phones.create(number: phone_number, category: 'mobile')
|
||||
end
|
||||
# code_error!(phone.errors.details, 422) if phone.errors.any?
|
||||
error!({ errors: ['resource.phone.cant_send_message'] }, 422) unless KaveNegarSmsService.send_confirmation(current_user)
|
||||
status 200
|
||||
end
|
||||
|
||||
desc 'Resend activation code for mobile sms',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: 'Activation code was resend' }
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Phone number with country code( in iran +98 )'
|
||||
optional :channel,
|
||||
type: String,
|
||||
default: 'sms',
|
||||
values: { value: -> { Phone::TWILIO_CHANNELS }, message: 'resource.phone.invalid_channel'},
|
||||
desc: 'The verification method to use(this param is for legacy structure must be sms)'
|
||||
end
|
||||
# TODO you can delete this api and use above
|
||||
post '/send_code' do
|
||||
error!({ errors: ['resource.phone.code_doesnt_expired'] }, 422) if current_user.read_cache('register-mobile')
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number])
|
||||
|
||||
phone_number = Phone.international(declared_params[:phone_number])
|
||||
phone = current_user.phones.find_by_number(phone_number)
|
||||
error!({ errors: ['resource.phone.doesnt_exist'] }, 404) unless phone
|
||||
|
||||
KaveNegarSmsService.send_confirmation(current_user)
|
||||
status 200
|
||||
end
|
||||
|
||||
desc 'Verify a mobile(enter code and verify number)',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
],
|
||||
success: API::V2::Entities::UserWithFullInfo
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'phone number with country code(in iran with +98)'
|
||||
requires :verification_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Verification code from sms(this param is for legacy structure must be sms)'
|
||||
end
|
||||
post '/verify' do
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number])
|
||||
error!({ errors: ['resource.phone.doesnt_exist'] }, 404) unless current_user.mobile('pending')
|
||||
|
||||
unless TOTPServiceAction.new('register-mobile').validate?(current_user.uid, declared(params)[:verification_code])
|
||||
error!({ errors: ['resource.phone.verification_invalid'] }, 400)
|
||||
end
|
||||
current_user.delete_cache('register-mobile')
|
||||
pending_mobile = current_user.mobile('pending')
|
||||
last_verified_mobile = current_user.mobile('verified')
|
||||
if last_verified_mobile&.step == 'owner_phone'
|
||||
if pending_mobile.check_owner_ship
|
||||
last_verified_mobile.update(state: 'replaced')
|
||||
pending_mobile.update(validated_at: Time.current, state: 'verified', step: 'owner_phone')
|
||||
else
|
||||
pending_mobile.update(state: 'rejected')
|
||||
end
|
||||
elsif last_verified_mobile&.step == 'access_phone'
|
||||
last_verified_mobile.update(state: 'replaced')
|
||||
pending_mobile.update(validated_at: Time.current, step: 'access_phone', state: 'verified')
|
||||
else
|
||||
pending_mobile.update(validated_at: Time.current, step: 'access_phone', state: 'verified')
|
||||
end
|
||||
|
||||
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
desc 'landline related routes'
|
||||
resource :phones do
|
||||
|
||||
desc 'Add new phone',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: 'New phone was added' }
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Phone number with country code(in iran +98)'
|
||||
optional :channel,
|
||||
type: String,
|
||||
default: 'call',
|
||||
values: { value: -> { Phone::TWILIO_CHANNELS }, message: 'resource.phone.invalid_channel'},
|
||||
desc: 'The verification method to use'
|
||||
end
|
||||
post do
|
||||
error!({ errors: ['resource.phone.code_doesnt_expired'] }, 422) if current_user.read_cache('register-phone')
|
||||
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 6
|
||||
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number], true)
|
||||
phone_number = Phone.international(declared_params[:phone_number])
|
||||
phone = current_user.phones.find_by(category: :landline, step: 'telephone', state: 'pending')
|
||||
label = current_user.labels.find_by(key: 'telephone', value: 'verified')
|
||||
# TODO can edit for updating feature
|
||||
error!({ errors: ['resource.phone.exists'] }, 422) if label.present?
|
||||
|
||||
if phone.present?
|
||||
phone.update(number: phone_number)
|
||||
else
|
||||
current_user.phones.create(number: phone_number, category: :landline, step: 'telephone')
|
||||
end
|
||||
# code_error!(phone.errors.details, 422) if phone.errors.any?
|
||||
KaveNegarSmsService.send_call_confirmation(current_user)
|
||||
end
|
||||
|
||||
desc 'Resend activation code in call',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: { code: 200, message: 'Activation code was resend' }
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Phone number with country code'
|
||||
optional :channel,
|
||||
type: String,
|
||||
default: 'call',
|
||||
values: { value: -> { Phone::TWILIO_CHANNELS }, message: 'resource.phone.invalid_channel'},
|
||||
desc: 'The verification method to use'
|
||||
end
|
||||
# TODO you can delete this api and use above
|
||||
post '/send_code' do
|
||||
error!({ errors: ['resource.phone.code_doesnt_expired'] }, 422) if current_user.read_cache('register-phone')
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number], true)
|
||||
|
||||
phone_number = Phone.international(declared_params[:phone_number])
|
||||
phone = current_user.phones.find_by_number(phone_number)
|
||||
error!({ errors: ['resource.phone.doesnt_exist'] }, 404) unless phone
|
||||
|
||||
KaveNegarSmsService.send_call_confirmation(current_user)
|
||||
end
|
||||
|
||||
|
||||
desc 'Verify a phone (enter code and verify number)',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
],
|
||||
success: API::V2::Entities::UserWithFullInfo
|
||||
params do
|
||||
requires :phone_number,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Phone number with country code(in iran with +98)'
|
||||
requires :verification_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Verification code from call'
|
||||
end
|
||||
post '/verify' do
|
||||
declared_params = declared(params)
|
||||
validate_phone!(declared_params[:phone_number], true)
|
||||
|
||||
# phone_number = Phone.international(declared_params[:phone_number])
|
||||
phone = current_user.phones.find_by(step: 'telephone', category: 'landline')
|
||||
error!({ errors: ['resource.phone.doesnt_exist'] }, 404) unless phone
|
||||
|
||||
error!({ errors: ['resource.phone.verification_invalid'] }, 404) unless TOTPServiceAction.new('register-phone').validate?(current_user.uid, declared(params)[:verification_code])
|
||||
current_user.delete_cache('register-phone')
|
||||
phone.update(validated_at: Time.current, state: 'verified')
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
255
app/api/v2/resource/profiles.rb
Normal file
255
app/api/v2/resource/profiles.rb
Normal file
@@ -0,0 +1,255 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
# CR functionality over profiles table
|
||||
class Profiles < Grape::API
|
||||
helpers do
|
||||
def profile_param_keys
|
||||
%w[first_name last_name dob address
|
||||
postcode city country metadata national_code].freeze
|
||||
end
|
||||
|
||||
def treasury_param_keys
|
||||
%w[title data kind].freeze
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Profile related routes'
|
||||
resource :profiles do
|
||||
desc 'Return profiles of current resource owner',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'User has no profile' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
get '/me' do
|
||||
present current_user.profiles, with: API::V2::Entities::Profile
|
||||
end
|
||||
|
||||
# personal info - kyc level one step one
|
||||
desc 'Create Level one - step one - personal info in profiles',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 409, message: 'Profile already exists' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
params do
|
||||
requires :first_name, type: String, desc: 'First Name'
|
||||
requires :last_name, type: String, desc: 'Last Name'
|
||||
requires :dob, type: Date, desc: 'Date of Birth'
|
||||
requires :national_code, type: String, desc: 'national code'
|
||||
requires :upload, desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
optional :confirm, type: Boolean, default: true, desc: 'Profile confirmation'
|
||||
end
|
||||
|
||||
post do
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 1
|
||||
|
||||
declared_params = declared(params.slice(*profile_param_keys), include_missing: false)
|
||||
declared_params.merge!(state: 'submitted')
|
||||
# declared_params.merge!(state: 'submitted') if params['confirm']
|
||||
ActiveRecord::Base.transaction do
|
||||
profile = current_user.profiles.create(declared_params)
|
||||
code_error!(profile.errors.details, 422) if profile.errors.any?
|
||||
|
||||
doc = current_user.documents.new(upload: params[:upload],
|
||||
identificator: SecureRandom.hex(16),
|
||||
doc_type: 'Identity card',
|
||||
doc_category: 'front_side')
|
||||
code_error!(doc.errors.details, 422) unless doc.save
|
||||
|
||||
present profile, with: API::V2::Entities::Profile
|
||||
end
|
||||
end
|
||||
|
||||
desc 'Update a profile for current_user',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
params do
|
||||
requires :first_name, type: String, desc: 'First Name'
|
||||
requires :last_name, type: String, desc: 'Last Name'
|
||||
requires :dob, type: Date, desc: 'Date of Birth'
|
||||
requires :national_code, type: String, desc: 'national code'
|
||||
requires :upload, desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
optional :confirm, type: Boolean, default: true, desc: 'Profile confirmation'
|
||||
end
|
||||
|
||||
put do
|
||||
target_profile = current_user.submitted_profile
|
||||
error!({ errors: ['resource.profile.doesnt_exist_or_not_editable'] }, 404) if target_profile.nil?
|
||||
|
||||
declared_params = declared(params.slice(*profile_param_keys), include_missing: false)
|
||||
# declared_params.merge!(state: 'submitted') if params['confirm']
|
||||
declared_params.merge!(state: 'submitted')
|
||||
ActiveRecord::Base.transaction do
|
||||
code_error!(target_profile.errors.details, 422) unless target_profile.update(declared_params)
|
||||
document = current_user.front_icard
|
||||
document.update!(upload: params[:upload]) if document.present?
|
||||
end
|
||||
present target_profile, with: API::V2::Entities::Profile
|
||||
end
|
||||
|
||||
# card bank info - kyc level one step three
|
||||
resource :treasury do
|
||||
# one request add iban just
|
||||
desc 'Create Level one - step three - info bank ',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 409, message: 'Bank info already exists' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
params do
|
||||
requires :data, type: String, desc: 'treasury number'
|
||||
requires :kind, type: String, desc: 'iban or card'
|
||||
optional :title, type: String, desc: 'treasury name'
|
||||
optional :confirm, type: Boolean, default: true, desc: 'Treasury confirmation'
|
||||
end
|
||||
|
||||
post do
|
||||
error!({ errors: ['resource.verified.profile.doesnt_exist'] }, 404) unless current_user.verified_profile.present?
|
||||
|
||||
declared_params = declared(params.slice(*treasury_param_keys), include_missing: false)
|
||||
# declared_params.merge!(state: 'submitted') if params['confirm'].present?
|
||||
declared_params.merge!(state: 'submitted')
|
||||
error!({ errors: ['treasury.kind.not_exist'] }, 404) if Treasury.kinds.keys.exclude?(declared_params['kind'])
|
||||
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 3 && declared_params['kind'].to_s == 'card'
|
||||
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 4 && declared_params['kind'].to_s == 'iban'
|
||||
|
||||
treasury = current_user.treasuries.create(declared_params)
|
||||
code_error!(treasury.errors.details, 422) if treasury.errors.any?
|
||||
|
||||
KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::TreasuryWorker.perform_async(current_user.verified_profile.id)
|
||||
present treasury, with: API::V2::Entities::Treasury
|
||||
end
|
||||
|
||||
desc 'Delete a treasury',
|
||||
success: { code: 204, message: 'Successfully deleted' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
delete ':id' do
|
||||
treasury = Treasury.find_by!(id: params[:id], user_id: current_user.id)
|
||||
treasury.destroy
|
||||
code_error!(treasury.errors.details, 422) if treasury.errors.any?
|
||||
status 204
|
||||
end
|
||||
|
||||
desc 'Return list of treasuries',
|
||||
failure: [
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'User has no treasuries' }
|
||||
],
|
||||
success: API::V2::Entities::Treasury
|
||||
get '/list' do
|
||||
present current_user.treasuries, with: API::V2::Entities::Treasury
|
||||
end
|
||||
end
|
||||
|
||||
resource :selfie do
|
||||
# submit selfie - kyc level two step three
|
||||
desc 'Create Level two - step three - selfie',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 409, message: 'Selfie already exists' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
params do
|
||||
requires :upload, desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
optional :confirm, type: Boolean, default: true, desc: 'Profile confirmation'
|
||||
end
|
||||
|
||||
post do
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 7
|
||||
|
||||
target_profile = current_user.verified_profile
|
||||
error!({ errors: ['resource.verified.profile.doesnt_exist'] }, 404) if target_profile.nil?
|
||||
|
||||
label = current_user.labels.find_by(key: 'selfie', value: 'verified')
|
||||
error!({ errors: ['resource.verified.selfie.already_exist'] }, 404) if label.present?
|
||||
|
||||
doc = current_user.documents.find_by(doc_type: 'Selfie', state: 'pending')
|
||||
|
||||
if doc.present?
|
||||
doc.update(upload: params[:upload])
|
||||
else
|
||||
doc = current_user.documents.new(upload: params[:upload],
|
||||
identificator: SecureRandom.hex(16),
|
||||
doc_type: 'Selfie',
|
||||
doc_category: 'front_side')
|
||||
code_error!(doc.errors.details, 422) unless doc.save
|
||||
end
|
||||
|
||||
status 201
|
||||
end
|
||||
end
|
||||
|
||||
resource :address do
|
||||
desc 'Adding Address',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'needed data doesnt exist' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: API::V2::Entities::Profile
|
||||
params do
|
||||
requires :city, type: String, desc: 'city name'
|
||||
requires :province, type: String, desc: 'province name'
|
||||
requires :address, type: String, desc: 'address'
|
||||
requires :postcode, type: String, desc: 'post code'
|
||||
requires :upload, desc: 'Array of Rack::Multipart::UploadedFile'
|
||||
end
|
||||
|
||||
post do
|
||||
error!({ errors: ['resource.address.already_exist'] }, 422) if current_user.documents.find_by(doc_type: 'Poa', state: 'verified').present?
|
||||
|
||||
error!({ errors: ['kyc_follow_steps'] }, 403) if current_user.kyc_info.dig(:step) < 5
|
||||
|
||||
declared_params = declared(params)
|
||||
profile = current_user.verified_profile
|
||||
# TODO change city and province to id instead of string
|
||||
province = Province.find_by(name: declared_params['province'])
|
||||
error!({ errors: ['resource.profile.province_doesnt_exist'] }, 404) if province.nil?
|
||||
|
||||
city = province.cities.find_by(name: declared_params['city'])
|
||||
error!({ errors: ['resource.profile.province_doesnt_exist'] }, 404) if city.nil?
|
||||
|
||||
doc = current_user.documents.find_by(doc_type: 'Poa', state: 'pending')
|
||||
# declared_params.merge!(state: 'submitted') if params['confirm']
|
||||
ActiveRecord::Base.transaction do
|
||||
profile.update(city: city, address: declared_params['address'], postcode: declared_params['postcode'])
|
||||
|
||||
code_error!(profile.errors.details, 422) if profile.errors.any?
|
||||
if doc.present?
|
||||
doc.update(upload: params[:upload])
|
||||
else
|
||||
doc = current_user.documents.new(upload: params[:upload],
|
||||
identificator: SecureRandom.hex(16),
|
||||
doc_type: 'Poa',
|
||||
doc_category: 'front_side')
|
||||
end
|
||||
code_error!(doc.errors.details, 422) unless doc.save
|
||||
|
||||
present profile, with: API::V2::Entities::Profile
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
186
app/api/v2/resource/service_accounts.rb
Normal file
186
app/api/v2/resource/service_accounts.rb
Normal file
@@ -0,0 +1,186 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
class ServiceAccounts < Grape::API
|
||||
helpers ::API::V2::NamedParams
|
||||
helpers do
|
||||
def otp_protected!
|
||||
unless current_user.otp
|
||||
error!({ errors: ['resource.service_accounts.2fa_disabled'] }, 400)
|
||||
end
|
||||
error!({ errors: ['resource.service_accounts.missing_totp'] }, 422) unless params[:totp_code].present?
|
||||
|
||||
return if TOTPService.validate?(current_user.uid, params[:totp_code])
|
||||
|
||||
error!({ errors: ['resource.service_accounts.invalid_totp'] }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
resource :service_accounts do
|
||||
desc 'List all service accounts for current user.',
|
||||
security: [{ "BearerToken": [] }],
|
||||
failure: [
|
||||
{ code: 400, message: 'Require 2FA and totp code' },
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
]
|
||||
params do
|
||||
end
|
||||
get do
|
||||
current_user.service_accounts
|
||||
end
|
||||
|
||||
resource :api_keys do
|
||||
desc 'List all api keys for specific service account.',
|
||||
failure: [
|
||||
{ code: 400, message: 'Require 2FA and totp code' },
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
optional :ordering,
|
||||
values: { value: -> (p){ %w[asc desc].include?(p) }, message: 'resource.service_accounts.invalid_ordering' },
|
||||
default: 'asc',
|
||||
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
|
||||
optional :order_by,
|
||||
values: { value: -> (p){ APIKey.new.attributes.keys.include?(p) }, message: 'resource.service_accounts.invalid_attribute' },
|
||||
default: 'id',
|
||||
desc: 'Name of the field, which result will be ordered by.'
|
||||
use :pagination_filters
|
||||
requires :service_account_uid, type: { value: String, message: 'resource.service_account.non_string_service_account_uid' }
|
||||
end
|
||||
get do
|
||||
target_service_account = current_user.service_accounts.find_by(uid: params[:service_account_uid])
|
||||
|
||||
error!({ errors: ['resource.service_account.doesnt_exist'] }, 404) if target_service_account.nil?
|
||||
|
||||
target_service_account.api_keys.order(params[:order_by] => params[:ordering]).tap { |q| present paginate(q), with: Entities::APIKey, except: [:secret] }
|
||||
end
|
||||
|
||||
desc 'Create api key for specific service account.',
|
||||
failure: [
|
||||
{ code: 400, message: 'Require 2FA and totp code' },
|
||||
{ code: 401, message: 'Invalid bearer token' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
requires :service_account_uid, type: { value: String, message: 'resource.service_account.non_string_service_account_uid' }
|
||||
requires :algorithm,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Service account algorithm'
|
||||
optional :scope,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Comma separated scopes'
|
||||
requires :totp_code,
|
||||
type: String,
|
||||
message: 'resource.service_accounts.missing_totp',
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
post do
|
||||
target_service_account = current_user.service_accounts.find_by(uid: params[:service_account_uid])
|
||||
|
||||
error!({ errors: ['resource.service_account.doesnt_exist'] }, 404) if target_service_account.nil?
|
||||
|
||||
otp_protected!
|
||||
declared_params = declared(params, include_missing: false)
|
||||
.except(:totp_code, :service_account_uid)
|
||||
.merge(scope: params[:scope]&.split(','))
|
||||
.merge(secret: SecureRandom.hex(16))
|
||||
|
||||
|
||||
api_key = target_service_account.api_keys.new(declared_params)
|
||||
|
||||
APIKey.transaction do
|
||||
raise ActiveRecord::Rollback unless api_key.save
|
||||
rescue Vault::VaultError
|
||||
api_key.errors.add(:api_key, 'could_not_save_secret')
|
||||
raise ActiveRecord::Rollback
|
||||
end
|
||||
|
||||
if api_key.errors.any?
|
||||
code_error!(api_key.errors.details, 422)
|
||||
end
|
||||
|
||||
present api_key, with: Entities::APIKey
|
||||
end
|
||||
|
||||
desc 'Delete an api key for specific service account',
|
||||
success: { code: 204, message: 'Succefully deleted' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' }
|
||||
]
|
||||
params do
|
||||
requires :service_account_uid, type: { value: String, message: 'resource.service_account.non_string_service_account_uid' }
|
||||
requires :kid,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Service account kid'
|
||||
requires :totp_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
delete ':kid' do
|
||||
target_service_account = current_user.service_accounts.find_by(uid: params[:service_account_uid])
|
||||
|
||||
error!({ errors: ['resource.service_account.doesnt_exist'] }, 404) if target_service_account.nil?
|
||||
|
||||
otp_protected!
|
||||
api_key = target_service_account.api_keys.find_by!(kid: params[:kid])
|
||||
api_key.destroy
|
||||
status 204
|
||||
end
|
||||
|
||||
desc 'Updates an api key',
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 401, message: 'Invalid bearer token' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
],
|
||||
success: Entities::APIKey
|
||||
params do
|
||||
requires :service_account_uid, type: { value: String, message: 'resource.service_account.non_string_service_account_uid' }
|
||||
requires :kid,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Service account kid'
|
||||
optional :scope,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Comma separated scopes'
|
||||
optional :state,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'State of API Key. "active" state means key is active and can be used for auth'
|
||||
requires :totp_code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Code from Google Authenticator'
|
||||
end
|
||||
put ':kid' do
|
||||
target_service_account = current_user.service_accounts.find_by(uid: params[:service_account_uid])
|
||||
|
||||
error!({ errors: ['resource.service_account.doesnt_exist'] }, 404) if target_service_account.nil?
|
||||
|
||||
otp_protected!
|
||||
declared_params = declared(params, include_missing: false)
|
||||
.except(:totp_code).except(:service_account_uid)
|
||||
.merge(scope: params[:scope]&.split(','))
|
||||
api_key = target_service_account.api_keys.find_by!(kid: params[:kid])
|
||||
unless api_key.update(declared_params)
|
||||
code_error!(api_key.errors.details, 422)
|
||||
end
|
||||
|
||||
present api_key, with: Entities::APIKey, except: [:secret]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
210
app/api/v2/resource/users.rb
Normal file
210
app/api/v2/resource/users.rb
Normal file
@@ -0,0 +1,210 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
class Users < Grape::API
|
||||
helpers ::API::V2::NamedParams
|
||||
helpers do
|
||||
def password_error!(options = {})
|
||||
options[:topic] = 'password'
|
||||
record_error!(options)
|
||||
end
|
||||
|
||||
def validate_topic!(topic)
|
||||
unless %w[all session otp password account].include?(topic)
|
||||
error!({ errors: ['resource.user.wrong_topic'] }, 422)
|
||||
end
|
||||
end
|
||||
|
||||
def verify_otp!
|
||||
error!({ errors: ['resource.user.missing_otp_code'] }, 422) if params[:otp_code].nil?
|
||||
|
||||
error!({ errors: ['resource.user.empty_otp_code'] }, 422) if params[:otp_code].empty?
|
||||
|
||||
unless TOTPService.validate?(current_user.uid, params[:otp_code])
|
||||
error!({ errors: ['resource.user.invalid_otp'] }, 422)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
resource :users do
|
||||
desc 'Returns current user',
|
||||
success: API::V2::Entities::UserWithFullInfo
|
||||
get '/me' do
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo
|
||||
end
|
||||
|
||||
desc 'Updates current user data field',
|
||||
success: API::V2::Entities::UserWithFullInfo
|
||||
params do
|
||||
requires :data, type: String, allow_blank: false, desc: 'Any additional key: value pairs in json string format'
|
||||
end
|
||||
put '/me' do
|
||||
code_error!(current_user.errors.details, 422) unless current_user.update(data: params[:data])
|
||||
|
||||
present current_user, with: API::V2::Entities::UserWithFullInfo
|
||||
end
|
||||
|
||||
desc 'Blocks current user',
|
||||
success: { code: 200, message: 'Current user was blocked' }
|
||||
params do
|
||||
requires :password, type: String, allow_blank: false, desc: 'Account password'
|
||||
optional :otp_code, type: String, allow_blank: false, desc: 'Code from Google Authenticator'
|
||||
end
|
||||
delete '/me' do
|
||||
error!({ errors: ['resource.user.invalid_password'] }, 422) unless password_valid?(params[:password])
|
||||
|
||||
verify_otp! if current_user.otp
|
||||
|
||||
current_user.labels.create(key: 'delete', value: 'by_user', scope: 'private')
|
||||
EventAPI.notify(
|
||||
'system.user.account.deleted',
|
||||
record: { user: current_user.as_json_for_event_api }
|
||||
)
|
||||
|
||||
status(200)
|
||||
end
|
||||
|
||||
desc 'Returns user activity',
|
||||
success: Entities::Activity
|
||||
params do
|
||||
requires :topic,
|
||||
type: String,
|
||||
allow_blank: { value: false, message: 'resource.user.empty_topic' },
|
||||
desc: 'Topic of user activity. Allowed: [all, password, session, otp]'
|
||||
optional :time_from,
|
||||
type: { value: Integer, message: 'resource.user.non_integer_time_from' },
|
||||
allow_blank: { value: false, message: 'resource.user.empty_time_from' },
|
||||
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
|
||||
'If set, only activities created after the time will be returned.'
|
||||
optional :time_to,
|
||||
type: { value: Integer, message: 'resource.user.non_integer_time_to' },
|
||||
allow_blank: { value: false, message: 'resource.user.empty_time_to' },
|
||||
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
|
||||
'If set, only activities created before the time will be returned.'
|
||||
optional :result,
|
||||
type: { value: String, message: 'resource.user.non_string_result' },
|
||||
allow_blank: { value: false, message: 'resource.user.empty_result' },
|
||||
desc: 'Result of user activity. Allowed: [succeed, failed, denied]'
|
||||
use :pagination_filters
|
||||
end
|
||||
get '/activity/:topic' do
|
||||
validate_topic!(params[:topic])
|
||||
data = current_user.activities.order('id DESC')
|
||||
data = data.where(topic: params[:topic]) if params[:topic] != 'all'
|
||||
data = data.tap { |q| q.where!('created_at >= ?', Time.at(params[:time_from])) if params[:time_from] }
|
||||
.tap { |q| q.where!('created_at < ?', Time.at(params[:time_to])) if params[:time_to] }
|
||||
.tap { |q| q.where!(result: params[:result]) if params[:result] }
|
||||
|
||||
error!({ errors: ['resource.user.no_activity'] }, 422) unless data.present?
|
||||
|
||||
present paginate(data), with: Entities::Activity
|
||||
end
|
||||
|
||||
# change password
|
||||
desc 'Sets new account password(change password)',
|
||||
success: { code: 201, message: 'Code sent for confirm password' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :old_password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Previous account password'
|
||||
requires :new_password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User password'
|
||||
requires :confirm_password,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'User password'
|
||||
end
|
||||
put '/password' do
|
||||
# if user inters too many wrong password, we logout him
|
||||
attempt = current_user.read_cache('attempt_change_password') || 0
|
||||
# TODO: numeric must be config that set by admin
|
||||
if attempt >= User::CHANGE_PASS_ATTEMPTS
|
||||
activity_record(user: current_user.id, action: 'many_change_password_attempts', result: 'succeed', topic: 'session')
|
||||
current_user.delete_cache('attempt_change_password')
|
||||
request.session.destroy
|
||||
password_error!(reason: 'Too many attempts', error_code: 422,
|
||||
user: current_user.id, action: 'password change', error_text: 'many_change_password_attempts')
|
||||
end
|
||||
|
||||
unless params[:new_password] == params[:confirm_password]
|
||||
password_error!(reason: 'New passwords don\'t match',
|
||||
error_code: 422, user: current_user.id, action: 'password change', error_text: 'doesnt_match')
|
||||
end
|
||||
|
||||
unless PasswordStrengthChecker.validate!(params[:new_password]) == 'strong'
|
||||
password_error!(reason: 'Previous password is not strong',
|
||||
error_code: 400, user: current_user.id, action: 'password change', error_text: 'password_is_not_strong')
|
||||
end
|
||||
|
||||
unless password_valid?(params[:old_password])
|
||||
current_user.write_cache('attempt_change_password', attempt + 1, 86_400)
|
||||
password_error!(reason: 'Previous password is not correct',
|
||||
error_code: 400, user: current_user.id, action: 'password change', error_text: 'prev_pass_not_correct')
|
||||
end
|
||||
|
||||
if params[:old_password] == params[:new_password]
|
||||
password_error!(reason: 'New password cant be the same, as old one',
|
||||
error_code: 400, user: current_user.id, action: 'password change', error_text: 'no_change_provided')
|
||||
end
|
||||
|
||||
publish_confirmation_code(current_user, Barong::App.config.domain, 'change-password')
|
||||
|
||||
current_user.write_cache('change_password', params[:new_password], 300)
|
||||
status 201
|
||||
end
|
||||
|
||||
desc 'Confirm account password(enter authorization code)',
|
||||
success: { code: 201, message: 'Changes password' },
|
||||
failure: [
|
||||
{ code: 400, message: 'Required params are empty' },
|
||||
{ code: 404, message: 'Record is not found' },
|
||||
{ code: 422, message: 'Validation errors' }
|
||||
]
|
||||
params do
|
||||
requires :code,
|
||||
type: String,
|
||||
allow_blank: false,
|
||||
desc: 'Totp Code from email'
|
||||
end
|
||||
|
||||
post '/confirm_password' do
|
||||
|
||||
temp_password = current_user.read_cache('change_password')
|
||||
|
||||
error!({ errors: ['resource.password.email_expired'] }, 422) if temp_password.blank?
|
||||
|
||||
unless TOTPServiceAction.new('change-password').validate?(current_user.uid, declared(params)[:code])
|
||||
error!({ errors: ['resource.password.confirm_code'] }, 422)
|
||||
end
|
||||
|
||||
unless current_user.update(password: temp_password)
|
||||
error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json
|
||||
activity_record(user: current_user.id, action: 'password change',
|
||||
result: 'failed', topic: 'password', data: error_note)
|
||||
code_error!(current_user.errors.details, 422)
|
||||
end
|
||||
|
||||
activity_record(user: current_user.id, action: 'password change', result: 'succeed', topic: 'password')
|
||||
|
||||
EventAPI.notify('system.user.password.change',
|
||||
record: {
|
||||
user: current_user.as_json_for_event_api,
|
||||
domain: Barong::App.config.domain
|
||||
})
|
||||
|
||||
current_user.delete_cache('change_password')
|
||||
status 201
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
64
app/api/v2/resource/utils.rb
Normal file
64
app/api/v2/resource/utils.rb
Normal file
@@ -0,0 +1,64 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module API::V2
|
||||
module Resource
|
||||
module Utils
|
||||
def current_user
|
||||
if env[:current_payload].has_key?(:uid)
|
||||
@_current_user ||= User.find_by!(uid: env[:current_payload][:uid])
|
||||
else
|
||||
raise(Peatio::Auth::Error, 'Middleware Error')
|
||||
end
|
||||
end
|
||||
|
||||
def record_error!(options = {})
|
||||
options[:data] = { reason: options[:reason] }.to_json
|
||||
options[:result] = 'failed'
|
||||
activity_record(options.except(:reason, :error_code))
|
||||
error!({ errors: ['resource.' + options[:topic] + '.' + options[:error_text]] }, options[:error_code])
|
||||
end
|
||||
|
||||
def twilio_dictionary_error(code)
|
||||
user_error = 'resource.phone.' + {
|
||||
21_612 => 'num_not_reachable',
|
||||
21_614 => 'num_not_valid',
|
||||
21_618 => 'sms_content_invalid',
|
||||
21_610 => 'unsubscribed_recipient'
|
||||
}[code]
|
||||
|
||||
user_error || 'resource.phone.twilio_unexpected'
|
||||
end
|
||||
|
||||
def activity_record(options = {})
|
||||
params = {
|
||||
category: 'user',
|
||||
user_id: options[:user],
|
||||
user_ip: remote_ip,
|
||||
user_agent: request.env['HTTP_USER_AGENT'],
|
||||
topic: options[:topic],
|
||||
action: options[:action],
|
||||
result: options[:result],
|
||||
data: options[:data]
|
||||
}
|
||||
Activity.create(params)
|
||||
end
|
||||
|
||||
def password_valid?(password)
|
||||
true if current_user == current_user.try(:authenticate, password)
|
||||
end
|
||||
|
||||
def publish_confirmation_code(user, domain, action)
|
||||
totp = TOTPServiceAction.new(action)
|
||||
totp.create(user.uid, user.email)
|
||||
EventAPI.notify(
|
||||
action,
|
||||
record: {
|
||||
user: user.as_json_for_event_api,
|
||||
domain: domain,
|
||||
code: totp.read_code(user.uid)
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user