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

9
app/api/base.rb Normal file
View File

@@ -0,0 +1,9 @@
module API
class Base < Grape::API
PREFIX = '/api'
cascade false
mount API::V2::Base => API::V2::Base::API_VERSION
end
end

View File

@@ -0,0 +1,17 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over abilities
class Abilities < Grape::API
namespace :abilities do
desc 'Get all roles and admin_permissions of barong cancan.'
get do
Ability.admin_permissions[current_user.role] || {}
end
end
end
end
end
end

View File

@@ -0,0 +1,61 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over activities table
class Activities < Grape::API
resource :activities do
helpers ::API::V2::NamedParams
helpers ::API::V2::Admin::NamedParams
helpers do
def permitted_search_params(params)
params[:range] = 'created'
params.slice(:action, :uid, :email, :topic, :from, :to, :range, :target_uid).merge(with_user: true, ordered: true)
end
end
desc 'Returns array of activities as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::ActivityWithUser
params do
use :activity_attributes
use :timeperiod_filters
use :pagination_filters
end
get do
admin_authorize! :read, Activity
activities = API::V2::Queries::ActivityFilter.new(Activity.where(category: 'user')).call(permitted_search_params(params))
present paginate(activities), with: API::V2::Admin::Entities::ActivityWithUser
end
desc 'Returns array of activities as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::AdminActivity
params do
use :activity_attributes
use :timeperiod_filters
use :pagination_filters
optional :target_uid,
type: { value: String, message: 'admin.activity.non_string_target_uid' }
optional :range,
type: String,
values: { value: -> (p){ %w[created].include?(p) }, message: 'admin.activity.invalid_range' },
default: 'created'
end
get '/admin' do
admin_authorize! :read, Activity
activities = API::V2::Queries::ActivityFilter.new(Activity.where(category: 'admin')).call(permitted_search_params(params))
present paginate(activities), with: API::V2::Admin::Entities::AdminActivity
end
end
end
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over user api keys
class APIKeys < Grape::API
resource :api_keys do
helpers ::API::V2::NamedParams
helpers ::API::V2::Admin::NamedParams
desc 'List all api keys for selected account.',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::APIKey
params do
requires :uid, type: String, allow_blank: false, desc: 'user uniq id'
optional :ordering,
values: { value: -> (p){ %w[asc desc].include?(p) }, message: 'api_keys.ordering.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: 'api_keys.ordering.invalid_attribute' },
default: 'id',
desc: 'Name of the field, which result will be ordered by.'
use :pagination_filters
end
get do
admin_authorize! :read, APIKey
target_user = User.find_by(uid: params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
target_user.api_keys.order(params[:order_by] => params[:ordering]).tap { |q| present paginate(q), with: API::V2::Entities::APIKey, except: [:secret] }
end
end
end
end
end
end

62
app/api/v2/admin/base.rb Normal file
View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
require_dependency 'barong/middleware/jwt_authenticator'
module API::V2
module Admin
class Base < Grape::API
PREFIX = '/admin'
use Barong::Middleware::JWTAuthenticator, \
pubkey: Rails.configuration.x.keystore.public_key
cascade false
format :json
content_type :json, 'application/json'
default_format :json
helpers API::V2::Resource::Utils
do_not_route_options!
mount Admin::Users
mount Admin::APIKeys
mount Admin::Permissions
mount Admin::Activities
mount Admin::Metrics
mount Admin::Restrictions
mount Admin::Profiles
mount Admin::Levels
mount Admin::Abilities
add_swagger_documentation base_path: File.join(API::Base::PREFIX, API::V2::Base::API_VERSION, 'barong', PREFIX),
add_base_path: true,
mount_path: '/swagger',
api_version: API::V2::Base::API_VERSION,
doc_version: Barong::Application::GIT_TAG,
info: {
title: 'Barong',
description: 'RESTful AdminAPI for barong OAuth server'
},
security_definitions: {
'BearerToken': {
description: 'Bearer Token authentication',
type: 'basic',
name: 'Authorization',
in: 'header'
}
},
models: [
API::V2::Admin::Entities::ActivityWithUser,
API::V2::Admin::Entities::AdminActivity,
API::V2::Admin::Entities::Document,
API::V2::Admin::Entities::Phone,
API::V2::Admin::Entities::Profile,
API::V2::Admin::Entities::UserWithKYC,
API::V2::Admin::Entities::UserWithProfile,
API::V2::Entities::APIKey
]
end
end
end

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class ActivityWithUser < API::V2::Entities::Base
expose :user_ip,
documentation: {
type: 'String',
desc: 'User IP'
}
expose :user_agent,
documentation: {
type: 'String',
desc: 'User Browser Agent'
}
expose :topic,
documentation: {
type: 'String',
desc: 'Defined topic (session, adjustments) or general by default'
}
expose :action,
documentation: {
type: 'String',
desc: "API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method"
}
expose :result,
documentation: {
type: 'String',
desc: 'Status of API response: succeed, failed, denied'
}
expose :data,
documentation: {
type: 'String',
desc: 'Parameters which was sent to specific API endpoint'
}
expose :user, using: API::V2::Entities::User
with_options(format_with: :iso_timestamp) do
expose :created_at
end
end
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class AdminActivity < API::V2::Entities::Base
expose :user_ip,
documentation: {
type: 'String',
desc: 'User IP'
}
expose :user_agent,
documentation: {
type: 'String',
desc: 'User Browser Agent'
}
expose :topic,
documentation: {
type: 'String',
desc: 'Defined topic (session, adjustments) or general by default'
}
expose :action,
documentation: {
type: 'String',
desc: "API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method"
}
expose :result,
documentation: {
type: 'String',
desc: 'Status of API response: succeed, failed, denied'
}
expose :data,
documentation: {
type: 'String',
desc: 'Parameters which was sent to specific API endpoint'
}
expose :user, as: :admin, using: API::V2::Entities::User
expose :target, as: :target, using: API::V2::Entities::User
with_options(format_with: :iso_timestamp) do
expose :created_at
end
end
end
end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module API::V2
module Admin
module Entities
class Document < API::V2::Entities::Document
expose :doc_number,
documentation: {
type: 'String',
desc: 'document number: AB123123 type'
}
end
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class Phone < API::V2::Entities::Phone
expose :number,
documentation: {
type: 'String',
desc: 'Phone number'
}
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class Profile < API::V2::Entities::Profile
expose :first_name,
documentation: {
type: 'String',
desc: 'first name'
}
expose :last_name,
documentation: {
type: 'String',
desc: 'Last name'
}
expose :national_code,
documentation: {
type: 'String',
desc: 'National Code'
}
expose :dob,
documentation: {
type: 'Date',
desc: 'Birth date'
}
end
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class UserWithKYC < API::V2::Entities::UserWithKYC
expose :profiles, using: Entities::Profile
# expose :phones, using: Entities::Phone
expose :documents, using: Entities::Document
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module API::V2::Admin
module Entities
class UserWithProfile < API::V2::Entities::UserWithProfile
expose :profiles, using: Entities::Profile
end
end
end

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over levels table
class Levels < Grape::API
resource :levels do
desc 'Returns array of permissions as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::Level
get do
admin_authorize! :read, Level
present ::Level.all, with: API::V2::Entities::Level
end
desc 'Change vip level',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Entities::User
params do
requires :uid, type: String
requires :level,
values: { value: -> (p){ %w[2 3].include?(p) }, message: 'user.level.invalid' },
default: '3',
desc: 'change level from vip'
end
put do
admin_authorize! :update, Profile
target_user = User.find_by(uid: params[:uid])
return error!({ errors: ['admin.users.doesnt_exist'] }, 404) if target_user.nil?
error!({ errors: ['admin.superadmin_change'] }, 422) if target_user.superadmin? && !current_user.superadmin?
unless target_user.update(declared(params.except(:uid), include_missing: false))
code_error!(target_user.errors.details, 422)
end
present user, with: API::V2::Entities::User
end
end
end
end
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Metrics functionality
class Metrics < Grape::API
helpers do
def permitted_search_params(params)
params.slice(:created_from, :created_to, :topic, :action, :result).merge(with_user: false)
end
end
resource :metrics do
desc 'Returns main statistic in the given time period',
failure: [
{ code: 401, message: 'Invalid bearer token' }
]
params do
optional :created_from
optional :created_to
end
get do
admin_authorize! :read, User
result = {}
signup = API::V2::Queries::ActivityFilter.new(Activity.all).call(
permitted_search_params(params.merge(topic: 'account', action: 'signup', result: 'succeed'))
)
sucessful_login = API::V2::Queries::ActivityFilter.new(Activity.all).call(
permitted_search_params(params.merge(topic: 'session', action: 'login', result: 'succeed'))
)
failed_login = API::V2::Queries::ActivityFilter.new(Activity.all).call(
permitted_search_params(params.merge(topic: 'session', action: 'login', result: 'failed'))
)
result[:signups] = signup.group('date(created_at)').size
result[:sucessful_logins] = sucessful_login.group('date(created_at)').size
result[:failed_logins] = failed_login.group('date(created_at)').size
result[:pending_applications] = Label.where({ key: 'document', value: 'pending', scope: 'private' }).count
present result
end
end
end
end
end
end

View File

@@ -0,0 +1,35 @@
# frozen_string_literal: true
module API
module V2
module Admin
module NamedParams
extend ::Grape::API::Helpers
params :pagination_filters do
optional :page,
type: { value: Integer, message: 'non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
optional :limit,
type: { value: Integer, message: 'non_integer_limit' },
values: { value: 1..100, message: 'invalid_limit' },
default: 100,
desc: 'Number of users per page (defaults to 100, maximum is 100).'
end
params :activity_attributes do
optional :topic,
type: { value: String, message: 'admin.activity.non_string_topic' }
optional :action,
type: { value: String, message: 'admin.activity.non_string_action' }
optional :uid,
type: { value: String, message: 'admin.activity.non_string_uid' }
optional :email,
type: { value: String, message: 'admin.activity.non_string_email' }
end
end
end
end
end

View File

@@ -0,0 +1,146 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over permissions table
class Permissions < Grape::API
resource :permissions do
helpers ::API::V2::NamedParams
helpers do
def validate_params!(params)
unless %w(get post delete put head patch all).include?(params[:verb].downcase)
error!({ errors: ['admin.permissions.invalid_verb'] }, 422)
end
error!({ errors: ['admin.permissions.invalid_action'] }, 422) unless %w(accept drop audit).include?(params[:action].downcase)
end
end
desc 'Returns array of permissions as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::Permission
params do
use :pagination_filters
end
get do
admin_authorize! :read, Permission
present paginate(Permission.all), with: API::V2::Entities::Permission
end
desc 'Create permission',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Permission was created' }
params do
requires :role,
type: String,
allow_blank: false
requires :verb,
type: String,
allow_blank: false
requires :path,
type: String,
allow_blank: false
requires :action,
type: String,
allow_blank: false
optional :topic,
type: String,
allow_blank: false
end
post do
admin_authorize! :create, Permission
validate_params!(params)
declared_params = declared(params, include_missing: false)
error!({ errors: ['admin.permission.role_doesnt_exist'] }, 422) if Permission.where(role: params[:role]).empty?
permission = Permission.new(declared_params)
code_error!(permission.errors.details, 422) unless permission.save
# clear cached permissions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('permissions')
status 200
end
desc 'Deletes permission',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Permission was deleted' }
params do
requires :id,
type: Integer,
allow_blank: false,
desc: 'permission id'
end
delete do
admin_authorize! :destroy, Permission
target_permission = Permission.find_by(id: params[:id])
error!({ errors: ['admin.permission.doesnt_exist'] }, 404) if target_permission.nil?
target_permission.destroy
# clear cached permissions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('permissions')
status 200
end
desc 'Update Permission',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Permission was updated' }
params do
requires :id,
type: Integer,
allow_blank: false,
desc: 'Permission id'
optional :role,
type: String,
allow_blank: false,
desc: 'permission field - role'
optional :verb,
type: String,
allow_blank: false,
desc: 'permission field - request verb'
optional :path,
type: String,
allow_blank: false,
desc: 'permission field - request path'
optional :action,
type: String,
allow_blank: false
optional :topic,
type: String,
allow_blank: false
end
put do
admin_authorize! :update, Permission
target_permission = Permission.find_by(id: params[:id])
error!({ errors: ['admin.permission.doesnt_exist'] }, 404) if target_permission.nil?
unless target_permission.update(declared(params, include_missing: false))
code_error!(target_permission.errors.details, 422)
end
# clear cached permissions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('permissions')
status 200
end
end
end
end
end
end

View File

@@ -0,0 +1,148 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over profiles table
class Profiles < Grape::API
resource :profiles do
helpers ::API::V2::NamedParams
desc 'Return all profiles',
failure: [
{ code: 401, message: 'Invalid bearer token' },
],
success: API::V2::Admin::Entities::Profile
params do
use :pagination_filters
end
get do
admin_authorize! :read, Profile
present paginate(Profile.all), with: API::V2::Admin::Entities::Profile
end
desc "Verify user's profile",
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Admin::Entities::Profile
params do
requires :uid, type: String
requires :state, type: String
end
put do
admin_authorize! :update, Profile
target_profile = User.find_by(uid: params[:uid])&.submitted_profile
return error!({ errors: ['admin.profiles.doesnt_exist_or_not_editable'] }, 404) if target_profile.nil?
if target_profile.user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.profiles.superadmin_change'] }, 422)
end
if Barong::App.config.profile_double_verification && target_profile.author \
&& target_profile.author == current_user.uid && !BarongConfig.list['profile_verification_roles']&.include?(current_user.role)
error!({ errors: ['admin.profiles.second_admin_approval'] }, 422)
end
unless target_profile.update(declared(params.except(:uid), include_missing: false))
code_error!(target_profile.errors.details, 422)
end
present target_profile, with: API::V2::Admin::Entities::Profile
end
desc 'Create a profile for user',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Admin::Entities::Profile
params do
requires :uid, type: String
optional :first_name, type: String
optional :last_name, type: String
optional :dob, type: Date
optional :address, type: String
optional :postcode, type: String
optional :city, type: String
optional :country, type: String
optional :metadata, type: String, desc: 'Any additional key: value pairs in json string format'
end
post do
target_user = User.find_by(uid: params[:uid])
declared_params = declared(params.except(:uid), include_missing: false)
declared_params.merge!(state: 'submitted', author: current_user.uid)
profile = target_user.profiles.create(declared_params)
code_error!(profile.errors.details, 422) if profile.errors.any?
present profile, with: API::V2::Admin::Entities::Profile
status 201
end
desc 'verifying labels by admin',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 404, message: 'doesnt exist' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Admin::Entities::Profile
params do
requires :user_uid, type: String
requires :label_key, type: String
requires :label_value, type: String
end
put '/label' do
admin_authorize! :update, Label
target_user = User.find_by(uid: params[:user_uid])
error!({ errors: ['admin.label.user_doesnt_exist'] }, 404) unless target_user
label = target_user.labels.find_by(key: params[:label_key])
error!({ errors: ['admin.label.label_doesnt_exist'] }, 404) unless label
label.update(value: params['label_value'])
code_error!(label.errors.details, 422) if label.errors.any?
# present label, with: API::V2::Admin::Entities::Profile
status 201
end
# bank info
resource :treasury do
desc 'Return list of treasuries',
failure: [
{ code: 401, message: 'Invalid bearer token' },
{ code: 404, message: 'User has no treasuries' }
],
success: API::V2::Entities::Treasury
params do
requires :user_uid, type: String
optional :kind, type: String
end
get '/list' do
target_user = User.find_by(uid: params[:user_uid])
error!({ errors: ['admin.treasury.user_doesnt_exist'] }, 404) unless target_user
treasury_list = target_user.treasuries
treasury_list = treasury_list.where(kind: params[:kind]) if params[:kind].present?
present treasury_list, with: API::V2::Entities::Treasury
end
end
end
end
end
end
end

View File

@@ -0,0 +1,177 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over restrictions table
class Restrictions < Grape::API
resource :restrictions do
helpers ::API::V2::NamedParams
desc 'Returns array of restrictions as a paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::Restriction
params do
optional :scope,
allow_blank: false,
values: { value: -> { Restriction::SCOPES }, message: 'admin.restriction.invalid_scope'}
optional :category,
allow_blank: false,
values: { value: -> { Restriction::CATEGORIES }, message: 'admin.restriction.invalid_category'}
optional :range,
type: String,
values: { value: ->(p) { %w[created updated].include?(p) }, message: 'admin.restriction.invalid_range' },
default: 'created'
use :pagination_filters
end
get do
admin_authorize! :read, Restriction
restrictions = Restriction.all
restrictions = params[:category] ? restrictions.where(category: params[:category]) : restrictions
restrictions = params[:scope] ? restrictions.where(scope: params[:scope]) : restrictions
restrictions = params[:to] ? restrictions.where("#{params[:range]}_at <= ?", Time.at(params[:to].to_i)) : restrictions
restrictions = params[:from] ? restrictions.where("#{params[:range]}_at >= ?", Time.at(params[:from].to_i)) : restrictions
present paginate(restrictions), with: API::V2::Entities::Restriction
end
desc 'Create whitelink',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Created whitelink' }
params do
optional :expire_time,
allow_blank: false,
default: 1,
values: { value: 1..30, message: 'invalid_expire' },
type: Integer,
desc: 'link will be active for (Time.now + expire_time in following range)'
optional :range,
allow_blank: false,
default: 'day',
values: { value: ->(p) { %w[day hour].include?(p) }, message: 'invalid_range' },
type: String,
desc: 'In combination with expire_time gives full controll over token expiration'
end
post '/whitelink' do
admin_authorize! :create, Restriction
whitelink_token = Digest::SHA256.hexdigest(SecureRandom.hex(10))
expires_in = params[:range] == 'day' ? params[:expire_time].days : params[:expire_time].hours
Rails.cache.write(whitelink_token, 'active', expires_in: expires_in)
response = { whitelink_token: whitelink_token }
present response
end
desc 'Create restriction',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Restriction was created' }
params do
requires :scope,
allow_blank: false,
values: { value: -> { Restriction::SCOPES }, message: 'admin.restriction.invalid_scope'}
requires :value,
allow_blank: false
requires :category,
type: String,
values: { value: -> { Restriction::CATEGORIES }, message: 'admin.restriction.invalid_category'},
allow_blank: false
optional :state,
default: 'enabled',
allow_blank: false,
values: { value: -> { Restriction::STATES }, message: 'admin.restriction.invalid_state' }
optional :code,
type: Integer,
allow_blank: false
end
post do
admin_authorize! :create, Restriction
restriction = Restriction.new(declared(params, include_missing: false))
code_error!(restriction.errors.details, 422) unless restriction.save
# clear cached restrictions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('restrictions')
status 200
end
desc 'Update restriction',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Restriction was updated' }
params do
requires :id,
type: Integer,
allow_blank: false,
desc: 'Restriction id'
optional :scope,
allow_blank: false,
values: { value: -> { Restriction::SCOPES }, message: 'admin.restriction.invalid_scope' }
optional :category,
type: String,
values: { value: -> { Restriction::CATEGORIES }, message: 'admin.restriction.invalid_category'},
allow_blank: false
optional :value,
allow_blank: false
optional :state,
allow_blank: false,
values: { value: -> { Restriction::STATES }, message: 'admin.restriction.invalid_state' }
optional :code,
type: Integer,
allow_blank: false
end
put do
admin_authorize! :update, Restriction
target_restriction = Restriction.find_by(id: params[:id])
error!({ errors: ['admin.restriction.doesnt_exist'] }, 404) if target_restriction.nil?
unless target_restriction.update(declared(params, include_missing: false))
code_error!(target_restriction.errors.details, 422)
end
# clear cached restrictions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('restrictions')
status 200
end
desc 'Delete restriction',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Restriction was deleted' }
params do
requires :id,
type: Integer,
allow_blank: false,
desc: 'Restriction id'
end
delete do
admin_authorize! :destroy, Restriction
target_restriction = Restriction.find_by(id: params[:id])
error!({ errors: ['admin.restriction.doesnt_exist'] }, 404) if target_restriction.nil?
target_restriction.destroy
# clear cached restrictions, so they will be freshly refetched on the next call to /auth
Rails.cache.delete('restrictions')
status 200
end
end
end
end
end
end

699
app/api/v2/admin/users.rb Normal file
View File

@@ -0,0 +1,699 @@
# frozen_string_literal: true
module API
module V2
module Admin
# Admin functionality over users table
class Users < Grape::API
resource :users do
helpers ::API::V2::NamedParams
helpers do
def permitted_search_params(params)
params.slice(:uid, :email, :role, :first_name, :last_name, :country, :level, :state, :from, :to, :range)
end
def search(field, value)
error!({ errors: ['admin.user.non_user_field'] }, 422) unless User.attribute_names.include?(field)
User.where("#{field}": value).order('email ASC')
end
end
desc 'Returns array of users as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::User
params do
optional :extended,
type: { value: Boolean, message: 'admin.user.non_boolean_extended' },
default: false,
desc: 'When true endpoint returns full information about users'
optional :uid,
type: String
optional :email,
type: String
optional :role,
type: String
optional :country,
type: String
optional :level,
type: Integer
optional :state,
type: String
optional :range,
type: String,
values: { value: -> (p){ %w[created updated].include?(p) }, message: 'admin.user.invalid_range' },
default: 'created'
optional :ordering,
values: { value: -> (p){ %w[asc desc].include?(p) }, message: 'user.ordering.invalid_ordering' },
default: 'asc',
desc: 'If set, returned values will be sorted in specific order, defaults to \'asc\'.'
optional :order_by,
values: { value: -> (p){ User.new.attributes.keys.include?(p) }, message: 'user.ordering.invalid_attribute' },
default: 'id',
desc: 'Name of the field, which result will be ordered by.'
use :timeperiod_filters
use :pagination_filters
end
get do
admin_authorize! :read, User
entity = params[:extended] ? API::V2::Admin::Entities::UserWithProfile : API::V2::Entities::User
users = API::V2::Queries::UserFilter.new(User.all.order(params[:order_by] => params[:ordering])).call(params).uniq
present paginate(users), with: entity
end
desc 'Update user attributes',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'User attributes were updated' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
optional :state,
type: String,
allow_blank: false,
desc: 'user state'
optional :otp,
type: Boolean,
allow_blank: false,
desc: 'user 2fa status'
exactly_one_of :state, :otp, message: 'admin.user.one_of_state_otp'
end
post '/update' do
admin_authorize! :update, User
target_user = User.find_by_uid(params[:uid])
# Ruby Hash returns array on keys and values
update_param_key = params.except(:uid).keys.first
update_param_value = params.except(:uid).values.first
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
error!({ errors: ['admin.user.update_himself'] }, 422) if target_user.uid == current_user.uid
if update_param_key == 'otp' && update_param_value == true
error!({ errors: ['admin.user.enable_2fa'] }, 422)
end
if update_param_value == target_user[update_param_key]
error!({ errors: ["admin.user.#{update_param_key}_no_change"] }, 422)
end
unless target_user.update(update_param_key => update_param_value)
code_error!(target_user.errors.details, 422)
end
target_user.labels.find_by(key: :otp, scope: :private).delete if target_user.labels.find_by(key: :otp, scope: :private) && update_param_key == 'otp'
status 200
end
desc 'Update user role',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'User role was created' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :role,
type: String,
allow_blank: false,
desc: 'user role'
end
post '/role' do
admin_authorize! :update, User
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
error!({ errors: ['admin.user.update_himself'] }, 422) if target_user.uid == current_user.uid
if params[:role] == target_user.role
error!({ errors: ["admin.user.role_no_change"] }, 422)
end
unless target_user.update(role: params[:role])
code_error!(target_user.errors.details, 422)
end
status 200
end
desc 'Update user attributes',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'User attributes were created' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
optional :email,
type: String,
allow_blank: false,
desc: 'User Email'
optional :state,
type: String,
allow_blank: false,
desc: 'user state'
optional :otp,
type: Boolean,
allow_blank: false,
desc: 'user 2fa status'
exactly_one_of :state, :otp, :email, message: 'admin.user.one_of_state_otp_email'
end
put do
admin_authorize! :update, User
target_user = User.find_by_uid(params[:uid])
# Ruby Hash returns array on keys and values
update_param_key = params.except(:uid).keys.first
update_param_value = params.except(:uid).values.first
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
error!({ errors: ['admin.user.update_himself'] }, 422) if target_user.uid == current_user.uid
if update_param_key == 'email' && !current_user.superadmin?
error!({ errors: ['superadmin.user.update_email'] }, 422)
end
if update_param_key == 'otp' && update_param_value == true
error!({ errors: ['admin.user.enable_2fa'] }, 422)
end
if update_param_value == target_user[update_param_key]
error!({ errors: ["admin.user.#{update_param_key}_no_change"] }, 422)
end
unless target_user.update(update_param_key => update_param_value)
code_error!(target_user.errors.details, 422)
end
target_user.labels.find_by(key: :otp, scope: :private).delete if target_user.labels.find_by(key: :otp, scope: :private) && update_param_key == 'otp'
status 200
end
desc 'Returns array of users with pending or replaced documents as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::User
params do
optional :extended,
type: { value: Boolean, message: 'admin.user.non_boolean_extended' },
default: false,
desc: 'When true endpoint returns full information about users'
optional :uid,
type: String
optional :email,
type: String
optional :role,
type: String
optional :first_name,
type: String
optional :last_name,
type: String
optional :country,
type: String
optional :level,
type: Integer
optional :state,
type: String
optional :range,
type: String,
values: { value: ->(p) { %w[created updated].include?(p) }, message: 'admin.user.invalid_range' },
default: 'created'
use :timeperiod_filters
use :pagination_filters
end
get '/documents/pending' do
admin_authorize! :read, User
users_with_pending_or_replaced_docs = User.with_pending_or_replaced_docs.order('labels.updated_at ASC')
users = API::V2::Queries::UserFilter.new(users_with_pending_or_replaced_docs).call(params)
entity = params[:extended] ? API::V2::Admin::Entities::UserWithKYC : API::V2::Entities::User
present paginate(users), with: entity
end
desc 'Returns user documents',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 404, message: 'doesnt exist' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Entities::Document
params do
requires :user_uid, type: String
optional :doc_type, type: String
optional :doc_category, type: String
optional :doc_state, type: String
end
get '/documents' do
admin_authorize! :read, User
target_user = User.find_by(uid: params[:user_uid])
error!({ errors: ['admin.document.user_doesnt_exist'] }, 404) unless target_user
documents = target_user.documents
documents = documents.where(doc_type: params[:doc_type]) if params[:doc_type].present?
documents = documents.where(doc_category: params[:doc_category]) if params[:doc_category].present?
documents = documents.where(state: params[:doc_state]) if params[:doc_state].present?
error!({ errors: ['admin.document.document_doesnt_exist'] }, 404) unless documents
present documents, with: API::V2::Entities::Document
status 200
end
desc 'change documents by admin',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 401, message: 'Invalid bearer token' },
{ code: 404, message: 'doesnt exist' },
{ code: 422, message: 'Validation errors' }
],
success: API::V2::Admin::Entities::Profile
params do
requires :user_uid, type: String
requires :doc_type, type: String
requires :doc_state, type: String
end
put '/documents' do
admin_authorize! :update, Document
target_user = User.find_by(uid: params[:user_uid])
error!({ errors: ['admin.document.user_doesnt_exist'] }, 404) unless target_user
document = target_user.documents.find_by(doc_type: params[:doc_type])
error!({ errors: ['admin.document.document_doesnt_exist'] }, 404) unless document
document.update(state: params['doc_state'])
code_error!(document.errors.details, 422) if document.errors.any?
present document, with: API::V2::Admin::Entities::Document
status 200
end
namespace :labels do
desc 'Returns existing labels keys and values',
failure: [
{ code: 401, message: 'Invalid bearer token' }
]
params do
end
get '/list' do
admin_authorize! :read, User
labels = Label.where(scope: 'private').group(:key, :value).size
present labels
end
desc 'Returns array of users as paginated collection',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Entities::User
params do
requires :key, type: String, desc: 'Label key'
requires :value, type: String, desc: 'Label value'
use :pagination_filters
end
get do
admin_authorize! :read, User
users = User.joins(:labels).where(labels: { key: params[:key], value: params[:value] })
present paginate(users), with: API::V2::Entities::User
end
desc 'Add label for user',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Label was created' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :key,
type: String,
allow_blank: false,
desc: 'label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
requires :value,
type: String,
allow_blank: false,
desc: 'label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.'
optional :description,
type: String,
allow_blank: false,
desc: 'label description. [A-Za-z0-9_-] should be used. max - 255 characters.'
optional :scope, type: String, desc: "Label scope: 'public' or 'private'. Default is public", allow_blank: false
end
post do
admin_authorize! :create, Label
declared_params = declared(params, include_missing: false)
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
declared_params[:user_id] = target_user.id
label = Label.new(declared_params.except(:uid))
code_error!(label.errors.details, 422) unless label.save
status 200
end
desc 'Update user label value',
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: 'Label was updated' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :key,
type: String,
allow_blank: false,
desc: 'Label key.'
requires :scope,
type: String,
allow_blank: false,
desc: 'label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
requires :value,
type: String,
allow_blank: false,
desc: 'Label value.'
optional :description,
type: String,
allow_blank: false,
desc: 'label description. [A-Za-z0-9_-] should be used. max - 255 characters.'
optional :replace,
type: { value: Boolean, message: 'admin.user.non_boolean_replace' },
default: true,
desc: 'When true label will be created if not exist'
end
post '/update' do
admin_authorize! :update, Label
declared_params = declared(params, include_missing: false)
target_user = User.find_by_uid(declared_params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
label = Label.find_by_key_and_user_id_and_scope(declared_params[:key], target_user.id, declared_params[:scope])
if label.nil?
if declared_params[:replace]
label = Label.create(
user_id: target_user.id,
key: declared_params[:key],
value: declared_params[:value],
scope: declared_params[:scope],
description: declared_params[:description]
)
else
error!({ errors: ['admin.label.doesnt_exist'] }, 404)
end
else
label.update({ value: params[:value], description: params[:description] })
end
code_error!(label.errors.details, 422) if label.errors.any?
status 200
end
desc 'Update user label 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: { code: 200, message: 'Label was updated' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :key,
type: String,
allow_blank: false,
desc: 'Label key.'
requires :scope,
type: String,
allow_blank: false,
desc: 'label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
optional :description,
type: String,
allow_blank: false,
desc: 'label description. [A-Za-z0-9_-] should be used. max - 255 characters.'
requires :value,
type: String,
allow_blank: false,
desc: 'Label value.'
end
put do
admin_authorize! :update, Label
declared_params = declared(params, include_missing: false)
target_user = User.find_by_uid(declared_params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
label = Label.find_by_key_and_user_id_and_scope(declared_params[:key], target_user.id, declared_params[:scope])
error!({ errors: ['admin.label.doesnt_exist'] }, 404) if label.nil?
unless label.update({ value: params[:value], description: params[:description] }.compact)
code_error!(label.errors.details, 422)
end
status 200
end
desc 'Deletes label for user',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: { code: 200, message: 'Label was deleted' }
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :key,
type: String,
allow_blank: false,
desc: 'label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
requires :scope,
type: String,
allow_blank: false,
desc: 'label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
end
delete do
admin_authorize! :destroy, Label
declared_params = declared(params, include_missing: false)
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
if target_user.superadmin? && !current_user.superadmin?
error!({ errors: ['admin.user.superadmin_change'] }, 422)
end
label = Label.find_by_key_and_user_id_and_scope(declared_params[:key], target_user.id, declared_params[:scope])
error!({ errors: ['admin.label.doesnt_exist'] }, 404) if label.nil?
label.destroy
status 200
end
end
desc 'Returns user info',
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::UserWithKYC
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
end
get '/:uid' do
admin_authorize! :read, User
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
present target_user, with: API::V2::Admin::Entities::UserWithKYC
end
desc "Deletes user's data storage record",
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::UserWithKYC
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :title,
type: String,
allow_blank: false,
desc: 'data storage uniq title'
end
delete '/data_storage' do
admin_authorize! :destroy, User
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
storage = target_user.data_storages.find_by_title(params[:title])
error!({ errors: ['admin.storage.doesnt_exist'] }, 404) if storage.nil?
target_user.labels.find_by(key: storage.title, scope: 'private')
storage.destroy
present target_user, with: API::V2::Admin::Entities::UserWithKYC
end
namespace :comments do
desc "Adds new user's comment",
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::UserWithKYC
params do
requires :uid,
type: String,
allow_blank: false,
desc: 'user uniq id'
requires :title,
type: String,
values: { value: -> (v){ v.length <= 64 }, message: 'admin.comments.title_too_long'},
allow_blank: false,
desc: 'comment uniq title'
requires :data,
type: String,
values: { value: -> (v){ v.length <= 65535 }, message: 'admin.comments.data_too_long'},
allow_blank: false,
desc: 'comment data'
end
post do
target_user = User.find_by_uid(params[:uid])
error!({ errors: ['admin.user.doesnt_exist'] }, 404) if target_user.nil?
comment = Comment.new(user_id: target_user.id,
data: params[:data],
title: params[:title],
author_uid: current_user[:uid])
code_error!(data_storage.errors.details, 422) unless comment.save
present target_user, with: API::V2::Admin::Entities::UserWithKYC
end
desc "Edit user's comment",
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::UserWithKYC
params do
requires :id,
type: Integer,
desc: 'comment uniq id'
optional :title,
type: String,
values: { value: -> (v){ v.length <= 64 }, message: 'admin.comments.title_too_long'},
allow_blank: false,
desc: 'comment title'
optional :data,
type: String,
values: { value: -> (v){ v.length <= 65535 }, message: 'admin.comments.data_too_long'},
allow_blank: false,
desc: 'comment data'
end
put do
comment = Comment.find(params[:id])
error!({ errors: ['admin.comment.doesnt_exist'] }, 404) if comment.nil?
code_error!(comment.errors.details, 422) unless comment.update(params.slice(:data, :title))
present comment.user, with: API::V2::Admin::Entities::UserWithKYC
end
desc "Delete user's comment",
failure: [
{ code: 401, message: 'Invalid bearer token' }
],
success: API::V2::Admin::Entities::UserWithKYC
params do
requires :id,
type: Integer,
desc: 'comment uniq id'
end
delete do
comment = Comment.find(params[:id])
error!({ errors: ['admin.comment.doesnt_exist'] }, 404) if comment.nil?
code_error!(comment.errors.details, 422) unless comment.destroy
present comment.user, with: API::V2::Admin::Entities::UserWithKYC
end
end
end
end
end
end
end

73
app/api/v2/base.rb Normal file
View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
require_dependency 'v2/validations'
require_dependency 'v2/exception_handlers'
module API::V2
# Base api configuration for V2 module
class Base < Grape::API
API_VERSION = 'v2'
cascade false
logger Rails.logger.dup
if Rails.env.production?
logger.formatter = GrapeLogging::Formatters::Json.new
else
logger.formatter = GrapeLogging::Formatters::Rails.new
end
use GrapeLogging::Middleware::RequestLogger,
logger: logger,
log_level: (Rails.env.production?)? :warn : :debug,
include: [GrapeLogging::Loggers::Response.new,
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
GrapeLogging::Loggers::RequestHeaders.new]
helpers API::V2::Utils
format :json
content_type :json, 'application/json'
default_format :json
include ExceptionHandlers
mount Identity::Base => '/identity'
mount Public::Base => '/public'
mount Resource::Base => '/resource'
add_swagger_documentation base_path: File.join(API::Base::PREFIX, API_VERSION, 'barong'),
add_base_path: true,
info: {
title: 'Barong',
description: 'RESTful API for barong OAuth server'
},
models: [
API::V2::Entities::Level,
API::V2::Entities::Label,
API::V2::Entities::APIKey,
API::V2::Entities::Profile,
API::V2::Entities::User,
API::V2::Entities::UserWithProfile,
API::V2::Entities::UserWithKYC,
API::V2::Entities::UserWithFullInfo,
API::V2::Entities::Phone,
API::V2::Entities::Activity,
API::V2::Entities::ServiceAccounts,
API::V2::Entities::Document,
API::V2::Entities::DataStorage,
API::V2::Entities::Comment,
API::V2::Entities::AdminLabelView
],
api_version: API_VERSION,
doc_version: Barong::Application::GIT_TAG,
mount_path: '/swagger'
mount Management::Base => '/management'
mount Admin::Base => '/admin'
route :any, '*path' do
error! 'Route is not found', 404
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
module API::V2
module Entities
class Activity < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'Activity ID'
}
expose :user_ip,
documentation: {
type: 'String',
desc: 'User IP'
}
expose :user_agent,
documentation: {
type: 'String',
desc: 'User Browser Agent'
}
expose :topic,
documentation: {
type: 'String',
desc: 'Defined topic (session, adjustments) or general by default'
}
expose :action,
documentation: {
type: 'String',
desc: "API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method"
}
expose :result,
documentation: {
type: 'String',
desc: 'Status of API response: succeed, failed, denied'
}
expose :data,
documentation: {
type: 'String',
desc: 'Parameters which was sent to specific API endpoint'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
end
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module API::V2
module Entities
class AdminLabelView < API::V2::Entities::Base
expose :key,
documentation: {
type: 'String',
desc: 'Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
}
expose :value,
documentation: {
type: 'String',
desc: 'Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.'
}
expose :scope,
documentation: {
type: 'String',
desc: "Label scope: 'public' or 'private'"
}
expose :description,
documentation: {
type: 'String',
desc: "Label desc: json string with any additional information"
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
module API::V2
module Entities
class APIKey < API::V2::Entities::Base
expose :kid,
documentation: {
type: 'String',
desc: 'JWT public key'
}
expose :algorithm,
documentation: {
type: 'String',
desc: 'Cryptographic hash function type'
}
expose :scope,
documentation: {
type: 'String',
desc: 'Serialized array of scopes'
}
expose :state,
documentation: {
type: 'String',
desc: 'active/non-active state of key'
}
expose :secret,
documentation: {
type: 'String',
desc: 'Api key secret'
},
if: ->(api_key) { api_key.hmac? }
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,12 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
module Entities
class Base < Grape::Entity
format_with(:iso_timestamp) { |t| t.iso8601 if t }
end
end
end
end

View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true
module API
module V2
module Entities
# comment retrieval entity
class Comment < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'Comment id'
}
expose :author_uid,
documentation: {
type: 'String',
desc: 'Comment author UID'
}
expose :title,
documentation: {
type: 'String',
desc: 'Comment title'
}
expose :data,
documentation: {
type: 'String',
desc: 'Comment plain text'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module API
module V2
module Entities
# data storage retrieval entity
class DataStorage < API::V2::Entities::Base
expose :title,
documentation: {
type: 'String',
desc: 'Any additional data title'
}
expose :data,
documentation: {
type: 'String',
desc: 'Any additional data json key:value pairs'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module API
module V2
module Entities
# Return user document with related info
class Document < API::V2::Entities::Base
expose :upload,
documentation: {
type: 'String',
desc: 'File Upload'
}
expose :url,
documentation: {
type: 'String',
desc: 'File url'
} do |document|
document&.upload&.url
end
expose :doc_type,
documentation: {
type: 'String',
desc: 'Document type: passport, driver license, utility bill, identity card, institutional, address, residental'
}
expose :doc_number,
documentation: {
type: 'String',
desc: 'Submasked document number: AB123123 type'
} do |document|
Barong::App.config.api_data_masking_enabled ? document.sub_masked_doc_number : document.doc_number
end
expose :doc_expire,
documentation: {
type: 'String',
desc: 'Expire date of uploaded documents'
}
expose :metadata,
documentation: {
type: 'String',
desc: 'Any additional stored data'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module API::V2
module Entities
class Label < API::V2::Entities::Base
expose :key,
documentation: {
type: 'String',
desc: 'Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.'
}
expose :value,
documentation: {
type: 'String',
desc: 'Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.'
}
expose :scope,
documentation: {
type: 'String',
desc: "Label scope: 'public' or 'private'"
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
module API
module V2
module Entities
class Level < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'Level identifier, level number'
}
expose :key,
documentation: {
type: 'String',
desc: 'Label key. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.'
}
expose :value,
documentation: {
type: 'String',
desc: 'Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.'
}
end
end
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module API
module V2
module Entities
class Permission < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'Permission id'
}
expose :action,
documentation: {
type: 'String',
desc: 'Permission action: accept (allow access (drop access), audit (record activity)'
}
expose :role,
documentation: {
type: 'String',
desc: 'Permission user role'
}
expose :verb,
documentation: {
type: 'String',
desc: 'Permission verb: put, post, delete, get'
}
expose :path,
documentation: {
type: 'String',
desc: 'API path'
}
expose :topic,
documentation: {
type: 'String',
desc: 'Permission topic: general, session etc'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,30 @@
# frozen_string_literal: true
module API
module V2
module Entities
# Phone request response
class Phone < API::V2::Entities::Base
expose :country,
documentation: {
type: 'String',
desc: 'Phone country'
}
expose :number,
documentation: {
type: 'String',
desc: 'Submasked phone number'
} do |phone|
Barong::App.config.api_data_masking_enabled ? phone.sub_masked_number : phone.number
end
expose :validated_at,
documentation: {
type: 'Datetime',
desc: 'Phone validation date'
}
end
end
end
end

View File

@@ -0,0 +1,85 @@
# frozen_string_literal: true
module API::V2
module Entities
class Profile < API::V2::Entities::Base
expose :first_name,
documentation: {
type: 'String',
desc: 'First Name'
}
expose :last_name,
documentation: {
type: 'String',
desc: 'Submasked last name'
} do |profile|
Barong::App.config.api_data_masking_enabled ? profile.sub_masked_last_name : profile.last_name
end
expose :national_code,
documentation: {
type: 'String',
desc: 'Submasked national code'
} do |profile|
Barong::App.config.api_data_masking_enabled ? profile.sub_masked_national_code : profile.national_code
end
expose :dob,
documentation: {
type: 'Date',
desc: 'Submasked birth date'
} do |profile|
Barong::App.config.api_data_masking_enabled ? profile.sub_masked_dob : profile.dob
end
expose :address,
documentation: {
type: 'String',
desc: 'Address'
}
expose :postcode,
documentation: {
type: 'String',
desc: 'Address Postcode'
}
expose :city,
documentation: {
type: 'String',
desc: 'City name'
}
expose :country,
documentation: {
type: 'String',
desc: 'Country name'
}
expose :state,
documentation: {
type: 'String',
desc: 'Profile state: drafted, submitted, verified, rejected'
}
expose :metadata,
documentation: {
type: 'Hash',
desc: 'Profile additional fields'
}
expose :upload,
documentation: {
type: 'String',
desc: 'Address Postcode'
} do |profile|
end
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
module API::V2
module Entities
class Restriction < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'Restriction id'
}
expose :category,
documentation: {
type: 'String',
desc: 'Restriction categories: blacklist, maintenance, whitelist, blocklogin'
}
expose :scope,
documentation: {
type: 'String',
desc: 'Restriction scopes: continent, country, ip, ip_subnet, all'
}
expose :value,
documentation: {
type: 'String',
desc: 'Restriction value: IP address, country abbreviation, all'
}
expose :code,
documentation: {
type: 'Integer',
desc: "Restriction codes: #{::Restriction::DEFAULT_CODES}"
}
expose :state,
documentation: {
type: 'String',
desc: 'Restriction states: disabled, enabled'
}
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
module API::V2
module Entities
class ServiceAccounts < API::V2::Entities::Base
expose :email,
documentation: {
type: 'String',
desc: 'User Email'
}
expose :uid,
documentation: {
type: 'String',
desc: 'User UID'
}
expose :role,
documentation: {
type: 'String',
desc: 'Service Account Role'
}
expose :level,
documentation: {
type: 'Integer',
desc: 'User Level'
}
expose :state,
documentation: {
type: 'String',
desc: 'Service Account State: active, disabled'
}
expose :user, using: Entities::User
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
module API::V2
module Entities
class Treasury < API::V2::Entities::Base
expose :id,
documentation: {
type: 'Integer',
desc: 'id of record'
}
expose :title,
documentation: {
type: 'String',
desc: 'title'
}
expose :state,
documentation: {
type: 'String',
desc: 'state'
}
expose :kind,
documentation: {
type: 'String',
desc: 'kind'
}
expose :data,
documentation: {
type: 'String',
desc: 'Submasked data'
} do |treasury|
Barong::App.config.api_data_masking_enabled ? treasury.sub_masked_data : treasury.data
end
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,60 @@
# frozen_string_literal: true
module API
module V2
module Entities
# Basic user info
class User < API::V2::Entities::Base
expose :email,
documentation: {
type: 'String',
desc: 'User Email'
}
expose :uid,
documentation: {
type: 'String',
desc: 'User UID'
}
expose :role,
documentation: {
type: 'String',
desc: 'User role'
}
expose :level,
documentation: {
type: 'Integer',
desc: 'User level'
}
expose :otp,
documentation: {
type: 'Boolean',
desc: 'is 2FA enabled for account'
}
expose :state,
documentation: {
type: 'String',
desc: 'User state: active, pending, inactive'
}
expose :referral_uid,
documentation: {
type: 'String',
desc: 'UID of referrer'
} do |user|
user.referral_uid
end
expose :data,
documentation: {
type: 'String',
desc: 'Additional phone and profile info'
}
end
end
end
end

View File

@@ -0,0 +1,80 @@
# frozen_string_literal: true
module API
module V2
module Entities
# User information containing profile, labels and documents
class UserWithFullInfo < API::V2::Entities::Base
expose :email,
documentation: {
type: 'String',
desc: 'User Email'
}
expose :uid,
documentation: {
type: 'String',
desc: 'User UID'
}
expose :role,
documentation: {
type: 'String',
desc: 'User role'
}
expose :level,
documentation: {
type: 'Integer',
desc: 'User level'
}
expose :otp,
documentation: {
type: 'Boolean',
desc: 'is 2FA enabled for account'
}
expose :state,
documentation: {
type: 'String',
desc: 'User state: active, pending, inactive'
}
expose :referral_uid,
documentation: {
type: 'String',
desc: 'UID of referrer'
} do |user|
user.referral_uid
end
expose :data,
documentation: {
type: 'String',
desc: 'Additional phone and profile info'
}
expose :csrf_token,
documentation: {
type: 'String',
desc: 'Сsrf protection token'
},
if: ->(_, options) { options[:csrf_token] } do |_user, options|
options[:csrf_token]
end
expose :labels, using: Entities::Label
expose :phones, using: Entities::Phone
expose :profiles, using: Entities::Profile
expose :data_storages, using: Entities::DataStorage
# activities, as sensitive and potentialy too big data should be queried separately
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
module API
module V2
module Entities
# User information containing profile, labels and documents
class UserWithKYC < API::V2::Entities::Base
expose :email,
documentation: {
type: 'String',
desc: 'User Email'
}
expose :uid,
documentation: {
type: 'String',
desc: 'User UID'
}
expose :role,
documentation: {
type: 'String',
desc: 'User role'
}
expose :level,
documentation: {
type: 'Integer',
desc: 'User level'
}
expose :otp,
documentation: {
type: 'Boolean',
desc: 'is 2FA enabled for account'
}
expose :state,
documentation: {
type: 'String',
desc: 'User state: active, pending, inactive'
}
expose :referral_uid,
documentation: {
type: 'String',
desc: 'UID of referrer'
} do |user|
user.referral_uid
end
expose :data,
documentation: {
type: 'String',
desc: 'Additional phone and profile info'
}
expose :profiles, using: Entities::Profile
expose :labels, using: Entities::AdminLabelView
# expose :phones, using: Entities::Phone
# expose :documents, using: Entities::Document
# expose :data_storages, using: Entities::DataStorage
# expose :comments, using: Entities::Comment
# activities, as sensitive and potentialy too big data should be queried separately
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end
end

View File

@@ -0,0 +1,64 @@
# frozen_string_literal: true
module API::V2
module Entities
class UserWithProfile < API::V2::Entities::Base
expose :email,
documentation: {
type: 'String',
desc: 'User Email'
}
expose :uid,
documentation: {
type: 'String',
desc: 'User UID'
}
expose :role,
documentation: {
type: 'String',
desc: 'User role'
}
expose :level,
documentation: {
type: 'Integer',
desc: 'User level'
}
expose :otp,
documentation: {
type: 'Boolean',
desc: 'is 2FA enabled for account'
}
expose :state,
documentation: {
type: 'String',
desc: 'User state: active, pending, inactive'
}
expose :referral_uid,
documentation: {
type: 'String',
desc: 'UID of referrer'
} do |user|
user.referral_uid
end
expose :data,
documentation: {
type: 'String',
desc: 'Additional phone and profile info'
}
expose :profiles, using: Entities::Profile
with_options(format_with: :iso_timestamp) do
expose :created_at
expose :updated_at
end
end
end
end

View File

@@ -0,0 +1,45 @@
# encoding: UTF-8
# frozen_string_literal: true
module API
module V2
# Overrides standard AR and grape validation errors
module ExceptionHandlers
def self.included(base)
base.instance_eval do
rescue_from Grape::Exceptions::ValidationErrors do |e|
errors_array = e.full_messages.map do |err|
err.split.last
end
error!({ errors: errors_array }, 422)
end
rescue_from ActiveRecord::RecordNotFound do |_e|
error!({ errors: ['record.not_found'] }, 404)
end
rescue_from Peatio::Auth::Error do |e|
# report_exception(e)
error!({ errors: ['jwt.decode_and_verify'] }, 401)
end
rescue_from(JWT::DecodeError) do |error|
# expired for "Signature has expired" - expired token
# segments for "Not enough or too many segments" - wrong token
error!({ errors: ["jwt.decode_and_verify.#{error.message.split.last}"] }, 422)
end
# Known Vault Error from TOTPService.with_human_error
rescue_from(TOTPService::Error) do |error|
error!({ errors: ['totp.error'] }, 422)
end
rescue_from :all do |e|
Rails.logger.error "#{e.message}\n#{e.backtrace[0..5].join("\n")}"
error!({ errors: ['server.internal_error'] }, 500)
end
end
end
end
end
end

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
module API
module V2
module Identity
# base api configurations for module
class Base < Grape::API
helpers API::V2::Identity::Utils
do_not_route_options!
mount Identity::General
mount Identity::Sessions
mount Identity::Users
end
end
end
end

View File

@@ -0,0 +1,48 @@
# frozen_string_literal: true
module API::V2
module Identity
class General < Grape::API
desc 'Password strength testing'
params do
requires :password, type: String, desc: 'User password'
end
post '/password/validate' do
{ entropy: PasswordStrengthChecker.calculate_entropy(params[:password]) }
end
desc 'Test connectivity'
get '/ping' do
{ ping: 'pong' }
end
desc 'Get server current unix timestamp.'
get '/time' do
ts = ::Time.now.to_i
{ time: ts }
end
desc 'Get barong version'
get '/version' do
{
git_tag: Barong::Application::GIT_TAG,
git_sha: Barong::Application::GIT_SHA,
build_date: DateTime.rfc3339(Barong::Application::BUILD_DATE),
version: Barong::Application::VERSION
}
end
desc 'Get barong configurations'
get '/configs' do
{
session_expire_time: Barong::App.config.session_expire_time,
captcha_type: Barong::App.config.captcha,
captcha_id: (Barong::App.config.recaptcha_site_key if Barong::App.config.captcha == 'recaptcha'),
phone_verification_type: Barong::App.config.phone_verification,
password_min_entropy: Barong::App.config.password_min_entropy,
password_regexp: Barong::App.config.password_regexp
}.compact
end
end
end
end

View File

@@ -0,0 +1,176 @@
# frozen_string_literal: true
require_dependency 'barong/jwt'
module API::V2
module Identity
class Sessions < Grape::API
helpers do
def get_user(email)
user = User.find_by(email: email)
error!({ errors: ['identity.session.invalid_params'] }, 401) unless user
if user.state == 'banned'
login_error!(reason: 'Your account is banned', error_code: 401,
user: user.id, action: 'login', result: 'failed', error_text: 'banned')
end
if user.state == 'deleted'
login_error!(reason: 'Your account is deleted', error_code: 401,
user: user.id, action: 'login', result: 'failed', error_text: 'deleted')
end
# if user is not active or pending, then return 401
unless user.state.in?(%w[active pending])
login_error!(reason: 'Your account is not active', error_code: 401,
user: user.id, action: 'login', result: 'failed', error_text: 'not_active')
end
user
end
end
desc 'Session related routes'
resource :sessions do
desc 'Start a new session for every LogIn',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
]
params do
requires :email
requires :password
optional :captcha_response,
types: { value: [String, Hash], message: 'identity.session.invalid_captcha_format' },
desc: 'Response from captcha widget'
optional :otp_code,
type: String,
desc: 'Code from Google Authenticator'
end
post do
verify_captcha!(response: params['captcha_response'], endpoint: 'session_create')
declared_params = declared(params, include_missing: false)
user = get_user(declared_params[:email])
error!({ errors: ['identity.session.not_active'] }, 401) unless user.state == 'active'
unless user.authenticate(declared_params[:password])
publish_session_failed(user)
login_error!(reason: 'Invalid Email or Password', error_code: 401, user: user.id,
action: 'login', result: 'failed', error_text: 'invalid_params')
end
unless user.otp
activity_record(user: user.id, action: 'login', result: 'succeed', topic: 'session')
csrf_token = open_session(user)
publish_session_create(user)
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
return status 200
end
error!({ errors: ['identity.session.missing_otp'] }, 401) if declared_params[:otp_code].blank?
unless TOTPService.validate?(user.uid, declared_params[:otp_code])
login_error!(reason: 'OTP code is invalid', error_code: 403,
user: user.id, action: 'login::2fa', result: 'failed', error_text: 'invalid_otp')
end
activity_record(user: user.id, action: 'login::2fa', result: 'succeed', topic: 'session')
csrf_token = open_session(user)
publish_session_create(user)
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
status(200)
end
desc 'Destroy current session for LogOut',
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
],
success: { code: 200, message: 'Session was destroyed' }
delete do
user = User.find_by(uid: session[:uid])
error!({ errors: ['identity.session.not_found'] }, 404) unless user
activity_record(user: user.id, action: 'logout', result: 'succeed', topic: 'session')
session.destroy
status(200)
end
desc 'Auth0 authentication by id_token',
success: { code: 200, message: 'User authenticated' },
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' }
]
params do
requires :id_token,
type: String,
allow_blank: false,
desc: 'ID Token'
end
post '/auth0' do
begin
# Decode ID token to get user info
claims = Barong::Auth0::JWT.verify(params[:id_token]).first
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless claims.key?('email')
user = User.find_by(email: claims['email'])
# If there is no user in platform and user email verified from id_token
# system will create user
if user.blank? && claims['email_verified']
user = User.create!(email: claims['email'], state: 'active')
user.labels.create!(scope: 'private', key: 'email', value: 'verified')
elsif claims['email_verified'] == false
error!({ errors: ['identity.session.auth0.invalid_params'] }, 401) unless user
end
activity_record(user: user.id, action: 'login', result: 'succeed', topic: 'session')
csrf_token = open_session(user)
publish_session_create(user)
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
rescue StandardError => e
report_exception(e)
error!({ errors: ['identity.session.auth0.invalid_params'] }, 422)
end
end
desc 'Resend confirmations code(authorization code)',
success: { code: 201, message: 'Generated verification code' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :data,
type: String,
allow_blank: false,
desc: 'Account email or Telephone number'
requires :action,
type: String,
allow_blank: false,
desc: 'for what need auth code'
optional :channel,
type: String,
allow_blank: true,
default: 'email',
desc: 'channel that send in'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/resend' do
current_user = User.find_by_email(params[:data])
return status 201 if current_user.nil?
publish_confirmation_code(current_user, Barong::App.config.domain, params[:action])
status 201
end
end
end
end
end

View File

@@ -0,0 +1,405 @@
# frozen_string_literal: true
require_dependency 'barong/jwt'
module API::V2
module Identity
class Users < Grape::API
helpers do
def parse_refid!
error!({ errors: ['identity.user.invalid_referral_format'] }, 422) unless params[:refid].start_with?(Barong::App.config.uid_prefix.upcase)
user = User.find_by_uid(params[:refid])
error!({ errors: ['identity.user.referral_doesnt_exist'] }, 422) if user.nil?
user.id
end
end
desc 'User related routes'
resource :users do
desc 'Creates new whitelist restriction',
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
],
success: { code: 200, message: 'Whitelist restriction was created' }
params do
requires :whitelink_token,
type: String,
allow_blank: false
end
post '/access' do
if Rails.cache.read(params[:whitelink_token]) == 'active'
restriction = Restriction.new(
category: 'whitelist',
scope: 'ip',
value: remote_ip,
state: 'enabled'
)
code_error!(restriction.errors.details, 422) unless restriction.save
Rails.cache.delete('restrictions')
else
error!({ errors: ['identity.user.access.invalid_token'] }, 422)
end
end
desc 'Creates new user (sign up)',
success: API::V2::Entities::UserWithFullInfo,
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :email,
type: String,
allow_blank: false,
desc: 'User Email'
requires :password,
type: String,
allow_blank: false,
desc: 'User Password'
optional :refid,
type: String,
desc: 'Referral uid'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
optional :data,
type: String,
desc: 'Any additional key: value pairs in json string format'
end
post do
verify_captcha!(response: params['captcha_response'], endpoint: 'user_create')
declared_params = declared(params, include_missing: false)
user_params = declared_params.slice('email', 'password', 'data')
user_params[:referral_id] = parse_refid! unless params[:refid].nil?
user = User.find_by(email: user_params[:email])
error!({ errors: ['identity.user.active_or_banned'] }, 422) if user.present? && %w[active ban].include?(user.state)
if user.present?
code_error!(user.errors.details, 422) unless user.update(user_params)
else
user = User.new(user_params)
code_error!(user.errors.details, 422) unless user.save
end
activity_record(user: user.id, action: 'signup', result: 'succeed', topic: 'account')
# Creates superadmin user in first platform registration
if Barong::App.config.first_registration_superadmin && User.count == 1
user.update(role: 'superadmin', state: 'active')
user.labels.create(key: 'email', value: 'verified', scope: 'private')
else
publish_confirmation_code(user, Barong::App.config.domain, 'sign-up')
user.write_cache('register_email', 'true', 3600)
end
csrf_token = open_session(user)
present user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
end
desc 'Register Geetest captcha'
get '/register_geetest' do
CaptchaService::GeetestVerifier.new.register
end
namespace :email do
desc 'Send confirmations instructions (code in email)',
success: { code: 201, message: 'Generated verification code' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :email,
type: String,
allow_blank: false,
desc: 'Account email'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/generate_email_code' do
verify_captcha!(response: params['captcha_response'], endpoint: 'email_confirmation')
current_user = User.find_by_email(params[:email])
return status 201 if current_user.nil? || current_user.active?
publish_confirmation_code(current_user, Barong::App.config.domain, 'sign-up')
status 201
end
desc 'Confirms an account by token (one-time link in email)',
success: API::V2::Entities::UserWithFullInfo,
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :token,
type: String,
allow_blank: false,
desc: 'Token from email'
end
post '/confirm_code' do
payload = codec.decode_and_verify(
params[:token],
pub_key: Barong::App.config.keystore.public_key,
sub: 'confirmation'
)
current_user = User.find_by_email(payload[:email])
if current_user.nil? || current_user.active?
error!({ errors: ['identity.user.active_or_doesnt_exist'] }, 422)
end
token_uniq?(payload[:jti])
current_user.labels.create!(key: 'email', value: 'verified', scope: 'private')
csrf_token = open_session(current_user)
EventAPI.notify('system.user.email.confirmed',
record: {
user: current_user.as_json_for_event_api,
domain: Barong::App.config.domain
})
present current_user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
end
desc 'Confirms an account by Authorization code (verify user email and achieve first label)',
success: API::V2::Entities::UserWithFullInfo,
failure: [
{ code: 400, message: 'Required code are missing' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :code,
type: String,
allow_blank: false,
desc: 'Code from email'
requires :email,
type: String,
allow_blank: false,
desc: 'user email'
end
post '/confirm_email' do
current_user = User.find_by_email(params[:email])
if current_user.nil? || current_user.active?
error!({ errors: ['identity.user.active_or_doesnt_exist'] }, 422)
end
unless TOTPServiceAction.new('sign-up').validate?(current_user.uid, declared(params)[:code])
error!({ errors: ['identity.user.code_invalid'] }, 422)
end
error!({ errors: ['identity.user.code_invalid'] }, 422) unless current_user.read_cache('register_email')
current_user.labels.create!(key: 'email', value: 'verified', scope: 'private')
csrf_token = open_session(current_user)
EventAPI.notify('system.user.email.confirmed',
record: {
user: current_user.as_json_for_event_api,
domain: Barong::App.config.domain
})
present current_user, with: API::V2::Entities::UserWithFullInfo, csrf_token: csrf_token
end
end
# forgot reset password
namespace :password do
desc 'Send password reset instructions(forget password)',
success: { code: 201, message: 'Generated password reset code' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' },
{ code: 404, message: 'User doesn\'t exist'}
]
params do
requires :email,
type: String,
message: 'identity.user.missing_email',
allow_blank: false,
desc: 'Account email'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/generate_code' do
verify_captcha!(response: params['captcha_response'], endpoint: 'password_reset')
current_user = User.find_by_email(params[:email])
return status 404 if current_user.nil?
activity_record(user: current_user.id, action: 'request password reset', result: 'succeed', topic: 'password')
publish_confirmation_code(current_user, Barong::App.config.domain, 'reset-password')
status 201
end
desc 'Validate reset code or set a new password via one-time email link',
success: { code: 201, message: 'reset code is ok' },
failure: [
{ code: 400, message: 'Required params are missing' },
{ code: 422, message: 'Validation errors' },
{ code: 404, message: 'User doesn\'t exist'}
]
params do
optional :email,
type: String,
allow_blank: false,
desc: 'Account email'
optional :code,
type: String,
allow_blank: false,
desc: 'Confirm code'
optional :reset_password_token,
type: String,
allow_blank: false,
desc: 'Token from email'
optional :password,
type: String,
allow_blank: false,
desc: 'User new password'
optional :confirm_password,
type: String,
allow_blank: false,
desc: 'User new password'
optional :captcha_response,
types: [String, Hash],
desc: 'Response from captcha widget'
end
post '/confirm_code' do
token_reset_request = params[:reset_password_token].present? ||
params[:password].present? ||
params[:confirm_password].present?
if token_reset_request
error!({ errors: ['identity.user.missing_pass_token', 'identity.user.empty_reset_password_token'] }, 422) if params[:reset_password_token].blank?
error!({ errors: ['identity.user.missing_password', 'identity.user.empty_password'] }, 422) if params[:password].blank?
error!({ errors: ['identity.user.missing_confirm_password', 'identity.user.empty_confirm_password'] }, 422) if params[:confirm_password].blank?
error!({ errors: ['identity.user.passwords_doesnt_match'] }, 422) unless params[:password] == params[:confirm_password]
payload = codec.decode_and_verify(
params[:reset_password_token],
pub_key: Barong::App.config.keystore.public_key,
sub: 'reset'
)
if Rails.cache.read("reset_password_#{payload[:email]}") != payload[:reset_token] ||
Rails.cache.read(payload[:jti]) == 'utilized'
error!({ errors: ['identity.user.utilized_token'] }, 422)
end
current_user = User.find_by_email(payload[:email])
return status 404 if current_user.nil?
unless PasswordStrengthChecker.validate!(params[:password]) == 'strong'
error!({ errors: ["resource.password.#{PasswordStrengthChecker.validate!(params[:password])}"] }, 422)
end
unless current_user.update(password: params[:password])
error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json
activity_record(user: current_user.id, action: 'password reset',
result: 'failed', topic: 'password', data: error_note)
code_error!(current_user.errors.details, 422)
end
Rails.cache.delete("reset_password_#{payload[:email]}")
Rails.cache.write(payload[:jti], 'utilized', expires_in: Barong::App.config.jwt_expire_time.seconds)
activity_record(user: current_user.id, action: 'password reset', result: 'succeed', topic: 'password')
EventAPI.notify('system.user.password.reset',
record: {
user: current_user.as_json_for_event_api,
domain: Barong::App.config.domain
})
status 201
else
error!({ errors: ['identity.user.missing_email', 'identity.user.empty_email'] }, 422) if params[:email].blank?
error!({ errors: ['identity.user.missing_code', 'identity.user.empty_code'] }, 422) if params[:code].blank?
verify_captcha!(response: params['captcha_response'], endpoint: 'password_reset')
current_user = User.find_by_email(params[:email])
return status 404 if current_user.nil?
totp = TOTPServiceAction.new('reset-password')
error!({ errors: ['resource.totp.code'] }, 422) unless totp.safe_validate?(current_user.uid, declared(params)[:code])
status 200
end
end
# reset forgot password
desc 'Sets new account password(for forgot password)',
success: { code: 201, message: 'Resets password' },
failure: [
{ code: 400, message: 'Required params are empty' },
{ code: 404, message: 'Record is not found' },
{ code: 422, message: 'Validation errors' }
]
params do
requires :email,
type: String,
message: 'identity.user.email',
allow_blank: false,
desc: 'user email'
requires :code,
type: String,
message: 'identity.user.missing_pass_token',
allow_blank: false,
desc: 'code from email again'
requires :password,
type: String,
message: 'identity.user.missing_password',
allow_blank: false,
desc: 'User new password'
requires :confirm_password,
type: String,
message: 'identity.user.missing_confirm_password',
allow_blank: false,
desc: 'User new password'
end
post '/reset' do
current_user = User.find_by_email(params[:email])
return status 404 if current_user.nil?
totp = TOTPServiceAction.new('reset-password')
error!({ errors: ['resource.totp.code'] }, 422) unless totp.safe_validate?(current_user.uid, declared(params)[:code])
unless params[:password] == params[:confirm_password]
error!({ errors: ['identity.user.passwords_doesnt_match'] }, 422)
end
unless PasswordStrengthChecker.validate!(params[:password]) == 'strong'
error!({ errors: ["resource.password.#{PasswordStrengthChecker.validate!(temp_password)}"] }, 422)
end
unless current_user.update(password: params[:password])
error_note = { reason: current_user.errors.full_messages.to_sentence }.to_json
activity_record(user: current_user.id, action: 'password reset',
result: 'failed', topic: 'password', data: error_note)
code_error!(current_user.errors.details, 422)
end
totp.validate?(current_user.uid, declared(params)[:code])
activity_record(user: current_user.id, action: 'password reset', result: 'succeed', topic: 'password')
EventAPI.notify('system.user.password.reset',
record: {
user: current_user.as_json_for_event_api,
domain: Barong::App.config.domain
})
status 201
end
end
end
end
end
end

View File

@@ -0,0 +1,161 @@
# frozen_string_literal: true
module API::V2
module Identity
module Utils
def session
request.session
end
def codec
@_codec ||= Barong::JWT.new(key: Barong::App.config.keystore.private_key)
end
def open_session(user)
csrf_token = SecureRandom.hex(10)
session.merge!(
"uid": user.uid,
"user_ip": remote_ip,
"user_agent": request.env['HTTP_USER_AGENT'],
"expire_time": Time.now.to_i + Barong::App.config.session_expire_time,
"csrf_token": csrf_token
)
csrf_token
end
def verify_captcha!(response:, endpoint:, error_statuses: [400, 422])
# by default we protect user_create session_create password_reset email_confirmation endpoints
return unless BarongConfig.list['captcha_protected_endpoints']&.include?(endpoint)
case Barong::App.config.captcha
when 'recaptcha'
recaptcha(response: response)
when 'geetest'
geetest(response: response)
end
end
def recaptcha(response:, error_statuses: [400, 422])
error!({ errors: ['identity.captcha.required'] }, error_statuses.first) if response.blank?
captcha_error_message = 'identity.captcha.verification_failed'
if CaptchaService::RecaptchaVerifier.new(request: request).response_valid?(skip_remote_ip: true, response: response)
return
end
error!({ errors: [captcha_error_message] }, error_statuses.last)
rescue StandardError
error!({ errors: [captcha_error_message] }, error_statuses.last)
end
def geetest(response:, error_statuses: [400, 422])
error!({ errors: ['identity.captcha.required'] }, error_statuses.first) if response.blank?
geetest_error_message = 'identity.captcha.verification_failed'
validate_geetest_response(response: response)
return if CaptchaService::GeetestVerifier.new.validate(response)
error!({ errors: [geetest_error_message] }, error_statuses.last)
rescue StandardError
error!({ errors: [geetest_error_message] }, error_statuses.last)
end
def validate_geetest_response(response:)
unless (response['geetest_challenge'].is_a? String) &&
(response['geetest_validate'].is_a? String) &&
(response['geetest_seccode'].is_a? String)
error!({ errors: ['identity.captcha.mandatory_fields'] }, 400)
end
end
def login_error!(options = {})
options[:data] = { reason: options[:reason] }.to_json
options[:topic] = 'session'
activity_record(options.except(:reason, :error_code, :error_text))
error!({ errors: ['identity.session.' + options[:error_text]] }, options[:error_code])
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 token_uniq?(jti)
error!({ errors: ['identity.user.utilized_token'] }, 422) if Rails.cache.read(jti) == 'utilized'
Rails.cache.write(jti, 'utilized', expires_in: Barong::App.config.jwt_expire_time.seconds)
end
def publish_confirmation(user, domain)
token = codec.encode(sub: 'confirmation', email: user.email, uid: user.uid)
EventAPI.notify(
'system.user.email.confirmation.token',
record: {
user: user.as_json_for_event_api,
domain: domain,
token: token
}
)
end
def publish_confirmation_code(user, domain, action)
totp = TOTPServiceAction.new(action)
totp.create(user.uid, user.email)
record = {
user: user.as_json_for_event_api,
domain: domain,
code: totp.read_code(user.uid)
}
case action
when 'sign-up'
record[:token] = codec.encode(sub: 'confirmation', email: user.email, uid: user.uid)
when 'reset-password'
reset_token = SecureRandom.hex(10)
Rails.cache.write(
"reset_password_#{user.email}",
reset_token,
expires_in: Barong::App.config.jwt_expire_time.seconds
)
record[:token] = codec.encode(
sub: 'reset',
email: user.email,
uid: user.uid,
reset_token: reset_token
)
end
EventAPI.notify(action, record: record)
end
def publish_session_create(user)
EventAPI.notify('system.session.create',
record: {
user: user.as_json_for_event_api,
user_ip: remote_ip,
user_agent: request.env['HTTP_USER_AGENT']
})
end
def publish_session_failed(user)
EventAPI.notify('system.session.failed',
record: {
user: user.as_json_for_event_api,
user_ip: remote_ip,
user_agent: request.env['HTTP_USER_AGENT']
})
end
end
end
end

View File

@@ -0,0 +1,110 @@
# frozen_string_literal: true
module API::V2
module Management
class APIKeys < Grape::API
resource :api_keys do
desc 'Create an api key for service account' do
@settings[:scope] = :write_apikeys
success API::V2::Entities::APIKey
end
params do
requires :algorithm,
type: String,
allow_blank: false,
desc: 'API key algorithm'
requires :uid,
type: String,
allow_blank: false,
desc: 'User UID or Service Account UID'
optional :scopes,
type: String,
allow_blank: false,
desc: 'Comma separated scopes'
end
post do
if params[:uid].start_with?(Barong::App.config.uid_prefix)
error!({ error: 'disabled_management_endpoint' }, 422) unless Barong::App.config.mgn_api_keys_user
key_holder = User.find_by(uid: params[:uid])
error!({ error: 'user_doesnt_exist' }, 422) unless key_holder
elsif params[:uid].start_with?(ServiceAccount::UID_PREFIX)
error!({ error: 'disabled_management_endpoint' }, 422) unless Barong::App.config.mgn_api_keys_sa
key_holder = ServiceAccount.find_by(uid: params[:uid])
error!({ error: 'service_account_doesnt_exist' }, 422) unless key_holder
else
error!({ error: 'uid_prefix_doesnt_exist'}, 422)
end
declared_params = declared(params, include_missing: false)
.except(:uid, :sa_uid, :scopes)
.merge(scope: params[:scopes]&.split(','))
.merge(secret: SecureRandom.hex(16))
api_key = key_holder.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
code_error!(api_key.errors.details, 422) if api_key.errors.any?
present api_key, with: API::V2::Entities::APIKey
end
desc 'Updates an api key for service account' do
@settings[:scope] = :write_apikeys
success API::V2::Entities::APIKey
end
params do
requires :kid,
type: String,
allow_blank: false,
desc: 'API key kid'
requires :uid,
type: String,
allow_blank: false,
desc: 'Service Account UID'
optional :scopes,
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'
end
post '/update' do
if params[:uid].start_with?(Barong::App.config.uid_prefix)
error!({ error: 'disabled_management_endpoint' }, 422) unless Barong::App.config.mgn_api_keys_user
key_holder = User.find_by(uid: params[:uid])
error!({ error: 'user_doesnt_exist' }, 422) unless key_holder
elsif params[:uid].start_with?(ServiceAccount::UID_PREFIX)
error!({ error: 'disabled_management_endpoint' }, 422) unless Barong::App.config.mgn_api_keys_sa
key_holder = ServiceAccount.find_by(uid: params[:uid])
error!({ error: 'service_account_doesnt_exist' }, 422) unless key_holder
else
error!({ error: 'uid_prefix_doesnt_exist'}, 422)
end
declared_params = declared(params, include_missing: false)
.except(:uid, :scopes)
.merge(scope: params[:scopes]&.split(','))
api_key = key_holder.api_keys.find_by(kid: params[:kid])
error!({ error: 'api_key_doesnt_exist' }, 422) unless api_key
code_error!(api_key.errors.details, 422) unless api_key.update(declared_params)
present api_key, with: API::V2::Entities::APIKey, except: [:secret]
end
end
end
end
end

View File

@@ -0,0 +1,58 @@
module API::V2
module Management
class Base < Grape::API
PREFIX = '/management'
do_not_route_options!
rescue_from(API::V2::Management::Exceptions::Base) { |e| error!(e.message, e.status, e.headers) }
rescue_from(Grape::Exceptions::ValidationErrors) { |e| error!(e.message, 422) }
rescue_from(ActiveRecord::RecordNotFound) { error!('Record is not found', 404) }
# Known Vault Error from TOTPService.with_human_error
rescue_from(TOTPService::Error) do |error|
error!(error.message, 422)
end
use API::V2::Management::JWTAuthenticationMiddleware
mount API::V2::Management::Labels
mount API::V2::Management::Users
mount API::V2::Management::Profiles
mount API::V2::Management::Phones
mount API::V2::Management::Tools
mount API::V2::Management::Otp
mount API::V2::Management::Documents
mount API::V2::Management::ServiceAccounts
mount API::V2::Management::APIKeys
add_swagger_documentation base_path: File.join(API::Base::PREFIX, API::V2::Base::API_VERSION, 'barong', PREFIX),
info: {
title: 'Barong',
description: 'Management API for barong OAuth server'
},
mount_path: '/swagger',
security_definitions: {
'SecurityScope': {
description: 'JWT should have signature keychains',
type: 'basic',
name: 'Authorization'
}
},
models: [
API::V2::Entities::Label,
API::V2::Entities::APIKey,
API::V2::Entities::UserWithFullInfo,
API::V2::Entities::User,
API::V2::Management::Entities::Profile,
API::V2::Management::Entities::Phone,
API::V2::Management::Entities::Document,
API::V2::Management::Entities::UserWithProfile,
API::V2::Management::Entities::UserWithKYC,
API::V2::Management::Entities::APIKey,
],
api_version: API::V2::Base::API_VERSION,
doc_version: Barong::Application::GIT_TAG,
add_base_path: true
end
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
module API::V2
module Management
# Documents server-to-server API
class Documents < Grape::API
desc 'Documents related routes'
resource :documents do
helpers do
def parse_file_data(upload, name, ext)
decoded_file = Base64.strict_decode64(upload)
file = Tempfile.new([name, ext])
file.binmode
file.write decoded_file
return file
end
end
desc 'Push documents to barong DB' do
@settings[:scope] = :write_documents
end
params do
requires :uid, type: String, allow_blank: false, desc: 'User uid'
requires :doc_type,
type: String,
allow_blank: false,
desc: 'Document type'
requires :doc_number,
type: String,
allow_blank: false,
desc: 'Document number'
requires :filename,
type: String,
allow_blank: false,
desc: 'Document name'
requires :file_ext,
type: String,
allow_blank: false,
desc: 'Document file extension'
requires :upload,
type: String,
desc: 'Base64 encoded document'
optional :doc_expire,
type: { value: Date, message: 'management.documents.expire_not_a_date' },
allow_blank: true,
desc: 'Document expiration date'
optional :update_labels,
type: { value: Boolean, message: 'management.documents.update_labels_invalid' },
default: true,
desc: 'If set to false, user label will not be created/updated'
optional :metadata,
type: String,
desc: 'Any additional key: value pairs in json string format'
end
post do
user = User.find_by(uid: params[:uid])
error!(errors: ['user doesnt exist']) unless user
file = parse_file_data(params[:upload], params[:filename], params[:file_ext])
doc = user.documents.new(declared(params).except(:upload, :uid, :filename, :file_ext).merge(upload: file))
error!(doc.errors.full_messages.to_sentence, 422) unless doc.save
status 201
end
end
end
end
end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
module API::V2::Management
module Entities
class Document < API::V2::Entities::Document
expose :doc_number,
documentation: {
type: 'String', desc: 'Document number: AB123123 type'
}
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module API::V2::Management
module Entities
class Phone < API::V2::Entities::Phone
expose :number,
documentation: {
type: 'String',
desc: 'Phone Number'
}
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module API::V2::Management
module Entities
class Profile < API::V2::Entities::Profile
expose :last_name,
documentation: {
type: 'String',
desc: 'Last name'
}
expose :dob,
documentation: {
type: 'Date',
desc: 'Birth date'
}
end
end
end

View File

@@ -0,0 +1,11 @@
# frozen_string_literal: true
module API::V2::Management
module Entities
class UserWithKYC < API::V2::Entities::UserWithKYC
expose :profiles, using: Entities::Profile
expose :phones, using: Entities::Phone
expose :documents, using: Entities::Document
end
end
end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module API::V2::Management
module Entities
class UserWithProfile < API::V2::Entities::UserWithProfile
expose :profiles, using: Entities::Profile
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
module API::V2
module Management
module Exceptions
class Authentication < Base
def status
@options.fetch(:status, 401)
end
end
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module API::V2
module Management
module Exceptions
class Base < StandardError
def initialize(message:, **options)
@options = options
super(message)
end
def headers
@options.fetch(:headers, {})
end
def status
@options.fetch(:status)
end
end
end
end
end

View File

@@ -0,0 +1,93 @@
# frozen_string_literal: true
require 'stringio'
module API::V2
module Management
class JWTAuthenticationMiddleware < Grape::Middleware::Base
extend Memoist
mattr_accessor :security_configuration
def before
return if request.path == '/api/v2/management/swagger'
check_request_method!
check_query_parameters!
check_content_type!
payload = check_jwt!(jwt)
env['rack.input'] = StringIO.new(payload.fetch(:data, {}).to_json)
end
private
def request
Grape::Request.new(env)
end
memoize :request
def jwt
JSON.parse(request.body.read)
rescue StandardError => e
raise Exceptions::Authentication, \
message: 'Couldn\'t parse JWT.',
debug_message: e.inspect,
status: 400
end
memoize :jwt
def check_request_method!
return if request.post? || request.put?
raise Exceptions::Authentication, \
message: 'Only POST and PUT verbs are allowed.',
status: 405
end
def check_query_parameters!
return if request.GET.empty?
raise Exceptions::Authentication, \
message: 'Query parameters are not allowed.',
status: 400
end
def check_content_type!
return if request.content_type == 'application/json'
raise Exceptions::Authentication, \
message: 'Only JSON body is accepted.',
status: 400
end
def check_jwt!(jwt)
begin
scope = security_configuration.fetch(:scopes).fetch(security_scope)
keychain = security_configuration
.fetch(:keychain)
.slice(*scope.fetch(:permitted_signers))
.each_with_object({}) { |(k, v), memo| memo[k] = v.fetch(:value) }
result = JWT::Multisig.verify_jwt(jwt, keychain, security_configuration.fetch(:jwt, {}))
rescue StandardError => e
Rails.logger.error "ManagementAPI check_jwt error: #{e.inspect}"
raise Exceptions::Authentication, \
message: 'Failed to verify JWT.',
debug_message: e.inspect,
status: 401
end
unless (scope.fetch(:mandatory_signers) - result[:verified]).empty?
raise Exceptions::Authentication, \
message: 'Not enough signatures for the action.',
status: 401
end
result[:payload]
end
def security_scope
request.env['api.endpoint'].options.fetch(:route_options).fetch(:scope)
end
end
end
end

View File

@@ -0,0 +1,132 @@
# frozen_string_literal: true
module API::V2
module Management
# Labels-related API
class Labels < Grape::API
helpers ::API::V2::NamedParams
helpers do
def user
@user ||= User.find_by!(uid: params[:user_uid])
end
def permitted_search_params(params)
params.slice(:key, :value, :from, :to, :range, :scope)
end
end
desc 'Label related routes'
resource :labels do
desc 'Get all labels assigned to users' do
@settings[:scope] = :read_users
success API::V2::Entities::User
end
params do
requires :key, type: String, allow_blank: false, desc: 'Label key.'
optional :value, type: String, allow_blank: false, desc: 'Label value.'
optional :scope, type: String, allow_blank: false, desc: 'Label scope.'
optional :extended,
type: { value: Boolean, message: 'Non boolean extended value' },
default: false,
desc: 'When true endpoint returns full information about users'
optional :range,
type: String,
values: { value: ->(p) { %w[created updated].include?(p) }, message: 'Invalid range' },
default: 'created'
use :pagination_filters
end
post '/filter/users' do
entity = params[:extended] ? API::V2::Entities::UserWithProfile : API::V2::Entities::User
users = API::V2::Queries::UserWithLabelFilter.new(User.all).call(permitted_search_params(params))
present paginate(users), with: entity
status 200
end
desc 'Get user collection filtered on label attributes' do
@settings[:scope] = :read_labels
success API::V2::Entities::AdminLabelView
end
params do
requires :user_uid, type: String, allow_blank: false, desc: 'User uid'
end
post '/list' do
present user.labels, with: API::V2::Entities::AdminLabelView
end
desc "Create a label with 'private' scope and assigns to users" do
@settings[:scope] = :write_labels
success API::V2::Entities::Label
end
params do
requires :user_uid, type: String, allow_blank: false, desc: 'User uid'
requires :key, type: String, allow_blank: false, desc: 'Label key.'
requires :value, type: String, allow_blank: false, desc: 'Label value.'
optional :description, type: String, allow_blank: false, desc: 'Label desc.'
end
post do
label = user.labels.create(key: params[:key],
value: params[:value],
description: params[:description],
scope: 'private')
if label.errors.any?
error!(label.errors.as_json(full_messages: true), 422)
end
present label, with: API::V2::Entities::Label
end
desc "Update a label with 'private' scope" do
@settings[:scope] = :write_labels
success API::V2::Entities::Label
end
params do
requires :user_uid, type: String, allow_blank: false, desc: 'User uid'
requires :key, type: String, allow_blank: false, desc: 'Label key.'
requires :value, type: String, allow_blank: false, desc: 'Label value.'
optional :description, type: String, allow_blank: false, desc: 'Label desc.'
optional :replace, type: Boolean, default: true, desc: 'When true label will be created if not exist'
end
put do
label = user.labels.find_by(key: params[:key], scope: 'private')
if label.nil?
if params[:replace]
label = Label.create(
user_id: user.id,
key: params[:key],
value: params[:value],
description: params[:description],
scope: params[:scope] || 'private'
)
else
error!({ error: 'label doesnt exist' }, 404)
end
else
label.update({ value: params[:value], description: params[:description] }.compact)
end
error!(label.errors.as_json(full_messages: true), 422) if label.errors.any?
present label, with: API::V2::Entities::Label
end
desc "Delete a label with 'private' scope" do
@settings[:scope] = :write_labels
end
params do
requires :user_uid, type: String, allow_blank: false, desc: 'User uid'
requires :key, type: String, allow_blank: false, desc: 'Label key.'
end
post '/delete' do
user.labels.find_by!(key: params[:key], scope: 'private').destroy
status 204
end
end
end
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
module API
module V2
module Management
class Otp < Grape::API
helpers do
def sign_request(jwt)
JWT::Multisig.add_jws(jwt, :barong, Barong::App.config.keystore.private_key, 'RS256')
rescue StandardError => e
error!("JWT is invalid by the reason \"#{e.message}\"", 422)
end
end
desc 'OTP related routes'
resource :otp do
desc 'Sign request with barong signature' do
@settings[:scope] = :otp_sign
end
params do
requires :user_uid, type: String, allow_blank: false, desc: 'Account UID'
requires :otp_code, type: String, allow_blank: false, desc: 'Code from Google Authenticator'
requires :jwt, type: Hash, allow_blank: false, desc: 'RFC 7516 jwt with applogic signature'
end
post '/sign' do
declared_params = declared(params)
user = User.active.find_by!(uid: declared_params[:user_uid])
error!('Account has not enabled 2FA', 422) unless user.otp
unless TOTPService.validate?(user.uid, declared_params[:otp_code])
error!('OTP code is invalid', 422)
end
sign_request(declared_params[:jwt])
end
end
end
end
end
end

View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
module API::V2
module Management
class Phones < Grape::API
helpers do
def validate_phone!(phone_number)
error!('management.phone.invalid_num', 400) unless Phone.valid?(phone_number)
error!('management.phone.number_exist', 400) if Phone.verified.find_by_number(phone_number)
end
end
desc 'Phones related routes'
resource :phones do
desc 'Get user phone numbers' do
@settings[:scope] = :read_phones
success API::V2::Management::Entities::Phone
end
params do
requires :uid, type: String, desc: 'User uid', allow_blank: false
end
post '/get' do
user = User.find_by(uid: params[:uid])
error!('user.doesnt_exist', 422) unless user
present user.phones, with: API::V2::Management::Entities::Phone
end
desc 'Create phone number for user' do
@settings[:scope] = :write_phones
success API::V2::Management::Entities::Phone
end
params do
requires :uid, type: String, desc: 'User uid', allow_blank: false
requires :number, type: String, desc: 'User phone number', allow_blank: false
end
post do
user = User.find_by(uid: params[:uid])
error!('user.doesnt_exist', 422) unless user
phone_number = Phone.international(params[:number])
validate_phone!(phone_number)
error!('management.phone.exists', 400) if user.phones.find_by_number(phone_number)
phone = user.phones.create(number: params[:number], validated_at: Time.now)
error!(phone.errors.full_messages, 422) if phone.errors.any?
present phone, with: API::V2::Management::Entities::Phone
end
desc 'Delete phone number for user' do
@settings[:scope] = :write_phones
success API::V2::Management::Entities::Phone
end
params do
requires :uid, type: String, desc: 'User uid', allow_blank: false
requires :number, type: String, desc: 'User phone number', allow_blank: false
end
post '/delete' do
user = User.find_by(uid: params[:uid])
error!('user.doesnt_exist', 422) unless user
phone_number = Phone.international(params[:number])
phone = user.phones.find_by_number(phone_number) if phone_number.present?
error!('management.phone.doesnt_exists', 422) unless phone
present phone.destroy, with: API::V2::Management::Entities::Phone
end
end
end
end
end

View File

@@ -0,0 +1,47 @@
# frozen_string_literal: true
module API::V2
module Management
# Profiles server-to-server API
class Profiles < Grape::API
desc 'Profiles related routes'
resource :profiles do
helpers do
def profile_param_keys
%w[first_name last_name dob address
postcode city country state metadata].freeze
end
end
desc 'Imports a profile for user' do
@settings[:scope] = :write_users
success API::V2::Management::Entities::UserWithProfile
end
params do
requires :uid, type: String, desc: 'User Uid', allow_blank: false
optional :first_name, type: String, desc: 'First Name', allow_blank: false
optional :last_name, type: String, desc: 'Last Name', allow_blank: false
optional :dob, type: Date, desc: 'Birth date', allow_blank: false
optional :address, type: String, desc: 'Address', allow_blank: false
optional :postcode, type: String, desc: 'Postcode', allow_blank: false
optional :city, type: String, desc: 'City', allow_blank: false
optional :country, type: String, desc: 'Country', allow_blank: false
optional :state, type: String, desc: 'State', allow_blank: false
optional :metadata, type: String, desc: 'Metadata', allow_blank: false
end
post do
user = User.find_by(uid: params[:uid])
error! 'user.doesnt_exist', 422 unless user
profile_params = params.slice(*profile_param_keys)
profile = Profile.new(profile_params.merge(user_id: user.id))
error!(profile.errors.full_messages, 422) unless profile.save
present user, with: API::V2::Management::Entities::UserWithProfile
end
end
end
end
end

View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
module API::V2
module Management
# ServiceAccounts server-to-server API
class ServiceAccounts < Grape::API
helpers ::API::V2::NamedParams
desc 'ServiceAccounts related routes'
resource :service_accounts do
desc 'Get specific service_account information' do
@settings[:scope] = :read_service_accounts
success API::V2::Entities::ServiceAccounts
end
params do
optional :uid, type: String, allow_blank: false, desc: 'service_account uid'
optional :email, type: String, allow_blank: false, desc: 'service_account email'
exactly_one_of :uid, :email
end
post '/get' do
declared_params = declared(params, include_missing: false)
service_acc = ServiceAccount.find_by!(declared_params)
error!('Service account doesnt exist', 422) unless service_acc
present service_acc, with: API::V2::Entities::ServiceAccounts
status 200
end
desc 'Get service_accounts as a paginated collection' do
@settings[:scope] = :read_service_accounts
success API::V2::Entities::ServiceAccounts
end
params do
use :pagination_filters
optional :owner_uid, type: String, allow_blank: false, desc: 'owner uid'
optional :owner_email, type: String, allow_blank: false, desc: 'owner email'
end
post '/list' do
owner = User.find_by(uid: params[:owner_uid]) || User.find_by(email: params[:owner_email]) if params[:owner_uid] || params[:owner_email]
service_accs = owner ? owner.service_accounts : ServiceAccount.all
service_accs.tap { |q| present paginate(q), with: API::V2::Entities::ServiceAccounts }
status 200
end
desc 'Create service_account' do
@settings[:scope] = :write_service_accounts
success API::V2::Entities::ServiceAccounts
end
params do
requires :service_account_role, type: String, allow_blank: false, desc: 'service_account role'
optional :owner_uid, type: String, allow_blank: false, desc: 'owner uid'
optional :service_account_uid, type: String, allow_blank: false, desc: 'service_account uid'
optional :service_account_email, type: String, allow_blank: false, desc: 'service_account email'
end
post '/create' do
owner = User.find_by(uid: params[:owner_uid])
error!('User doesnt exist', 422) unless owner
s_params = { email: params[:service_account_email], uid: params[:service_account_uid], role: params[:service_account_role] }.compact
service_acc = ServiceAccount.new(s_params.merge(user: owner))
error!(service_acc.errors.full_messages, 422) unless service_acc.save
present service_acc, with: API::V2::Entities::ServiceAccounts
status 201
end
desc 'Update service_account' do
@settings[:scope] = :write_service_accounts
success API::V2::Entities::ServiceAccounts
end
params do
requires :uid, type: String, allow_blank: false, desc: 'service_account uid'
optional :owner_uid, type: String, allow_blank: false, desc: 'service_account owner uid'
end
post '/update' do
service_acc = ServiceAccount.find_by(uid: params[:uid])
error!('Service account doesnt exist', 422) unless service_acc
owner = User.find_by(uid: params[:owner_uid])
s_params = { owner_id: owner&.id }.compact
code_error!(service_acc.errors.details, 422) unless service_acc.update(s_params)
present service_acc, with: API::V2::Entities::ServiceAccounts
end
desc 'Delete specific service_account' do
@settings[:scope] = :write_service_accounts
success API::V2::Entities::ServiceAccounts
end
params do
requires :uid, type: String, allow_blank: false, desc: 'service_account uid'
end
post '/delete' do
declared_params = declared(params, include_missing: false)
service_acc = ServiceAccount.find_by!(declared_params)
error!('Service account doesnt exist', 422) unless service_acc
unless service_acc.update(state: 'disabled')
code_error!(service_acc.errors.details, 422)
end
present service_acc, with: API::V2::Entities::ServiceAccounts
status 200
end
end
end
end
end

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module API::V2
module Management
class Tools < Grape::API
desc 'Returns server time in seconds since Unix epoch.' do
@settings[:scope] = :tools
end
post '/timestamp' do
body timestamp: Time.now.to_i
status 200
end
end
end
end

View File

@@ -0,0 +1,168 @@
# frozen_string_literal: true
module API::V2
module Management
class Users < Grape::API
helpers ::API::V2::NamedParams
helpers do
def profile_param_keys
%w[first_name last_name dob address
postcode city country state].freeze
end
def create_user(user_params)
user = User.new(user_params)
user.send :assign_uid
user.save(validate: false)
error!(user.errors.full_messages3, 422) unless user.persisted?
user
end
def all_profile_fields?(params)
profile_param_keys.all? { |key| params[key].present? }
end
def create_phone(user:, number:)
return if number.blank?
phone = user.phones.create(number: number)
error!(phone.errors.full_messages, 422) unless phone.persisted?
phone.update(validated_at: Time.current)
end
end
desc 'Users related routes'
resource :users do
desc 'Get users and profile information' do
@settings[:scope] = :read_users
success API::V2::Management::Entities::UserWithKYC
end
params do
optional :uid, type: String, allow_blank: false, desc: 'User uid'
optional :email, type: String, allow_blank: false, desc: 'User email'
optional :phone_num, type: String, allow_blank: false, desc: 'User phone number'
exactly_one_of :uid, :email, :phone_num
end
post '/get' do
declared_params = declared(params, include_missing: false)
if declared_params.key?(:phone_num)
user = Phone.find_by_number!(declared_params[:phone_num]).user
present user, with: API::V2::Management::Entities::UserWithKYC
return status 201
end
user = User.find_by!(declared_params)
present user, with: API::V2::Management::Entities::UserWithKYC
end
desc 'Returns array of users as collection',
security: [{ "BearerToken": [] }],
failure: [
{ code: 401, message: 'Invalid bearer token' }
] do
@settings[:scope] = :read_users
success API::V2::Entities::User
end
params do
optional :extended,
type: { value: Boolean, message: 'Non boolean extended value' },
default: false,
desc: 'When true endpoint returns full information about users'
optional :range,
type: String,
values: { value: -> (p){ %w[created updated].include?(p) }, message: 'Non positive page' },
default: 'created'
use :timeperiod_filters
use :pagination_filters
end
post '/list' do
entity = params[:extended] ? API::V2::Management::Entities::UserWithProfile : API::V2::Entities::User
users = API::V2::Queries::UserFilter.new(User.all).call(params)
users.tap { |q| present paginate(q), with: entity }
status 200
end
desc 'Creates new user' do
@settings[:scope] = :write_users
success API::V2::Management::Entities::UserWithProfile
end
params do
requires :email, type: String, desc: 'User Email', allow_blank: false
requires :password, type: String, desc: 'User Password', allow_blank: false
optional :referral_uid, type: String, desc: 'Referral uid', allow_blank: false
end
post do
referral = User.find_by_uid(params[:referral_uid]).id if params[:referral_uid]
user = User.create({ email: params[:email], password: params[:password], referral_id: referral }.compact )
error!(user.errors.full_messages, 422) unless user.persisted?
present user, with: API::V2::Management::Entities::UserWithProfile
end
desc 'Updates role and data fields of existing user' do
@settings[:scope] = :write_users
success API::V2::Management::Entities::UserWithProfile
end
params do
requires :uid, type: String, desc: 'User Uid', allow_blank: false
optional :role, type: String, desc: 'User Role', allow_blank: false
optional :data, type: String, desc: 'Any additional key:value pairs in json format', allow_blank: false
at_least_one_of :role, :data
end
post '/update' do
user = User.find_by_uid(params[:uid])
error! 'user.doesnt_exist', 422 unless user
u_params = { data: params[:data], role: params[:role] }.compact
error!(user.errors.full_messages, 422) unless user.update(u_params)
present user, with: API::V2::Management::Entities::UserWithProfile
end
desc 'Imports an existing user' do
@settings[:scope] = :write_users
success API::V2::Management::Entities::UserWithProfile
end
params do
requires :email, type: String,
desc: 'User Email',
allow_blank: false
requires :password_digest, type: String,
desc: 'User Password Hash',
allow_blank: false
optional :referral_uid, type: String, desc: 'Referral uid', allow_blank: false
optional :phone, type: String, allow_blank: false, desc: 'Phone'
optional :first_name, type: String, allow_blank: false, desc: 'First Name'
optional :last_name, type: String, allow_blank: false, desc: 'Last Name'
optional :dob, type: Date, desc: 'Birth date', allow_blank: false
optional :address, type: String, allow_blank: false, desc: 'Address'
optional :postcode, type: String, allow_blank: false, desc: 'Postcode'
optional :city, type: String, allow_blank: false, desc: 'City'
optional :country, type: String, allow_blank: false, desc: 'Country'
optional :state, type: String, allow_blank: false, desc: 'State'
end
post '/import' do
if User.find_by(email: params[:email]).present?
error! 'User already exists by this email', 422
end
referral = User.find_by_uid(params[:referral_uid]).id if params[:referral_uid]
user = create_user({
email: params[:email],
password_digest: params[:password_digest],
referral_id: referral
}.compact)
create_phone(user: user, number: params[:phone])
profile_params = params.slice(*profile_param_keys)
profile = Profile.new(profile_params.merge(user_id: user.id))
error!(profile.errors.full_messages, 422) unless profile.save
present user, with: API::V2::Management::Entities::UserWithProfile
end
end
end
end
end

View File

@@ -0,0 +1,33 @@
# frozen_string_literal: true
module API
module V2
module NamedParams
extend ::Grape::API::Helpers
params :pagination_filters do
optional :page,
type: { value: Integer, message: 'non_integer_page' },
values: { value: -> (p){ p.try(:positive?) }, message: 'non_positive_page'},
default: 1,
desc: 'Page number (defaults to 1).'
optional :limit,
type: { value: Integer, message: 'non_integer_limit' },
values: { value: 1..100, message: 'invalid_limit' },
default: 100,
desc: 'Number of users per page (defaults to 100, maximum is 100).'
end
params :timeperiod_filters do
optional :from,
type: Integer,
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
'If set, only records FROM the time will be retrieved.'
optional :to,
type: Integer,
desc: 'An integer represents the seconds elapsed since Unix epoch.'\
'If set, only records BEFORE the time will be retrieved.'
end
end
end
end

15
app/api/v2/public/base.rb Normal file
View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module V2
module Public
class Base < Grape::API
helpers API::V2::Identity::Utils
do_not_route_options!
mount Public::General
end
end
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
module API::V2
module Public
class General < Grape::API
desc 'KYC callback'
post '/kyc' do
return_status = KycService.kycaid_callback(params)
status return_status
end
desc 'Password strength testing'
params do
requires :password, type: String, desc: 'User password'
end
post '/password/validate' do
{ entropy: PasswordStrengthChecker.calculate_entropy(params[:password]) }
end
desc 'Test connectivity'
get '/ping' do
{ ping: 'pong' }
end
desc 'Get server current unix timestamp.'
get '/time' do
ts = ::Time.now.to_i
{ time: ts }
end
desc 'Get barong version'
get '/version' do
{
git_tag: Barong::Application::GIT_TAG,
git_sha: Barong::Application::GIT_SHA,
build_date: DateTime.rfc3339(Barong::Application::BUILD_DATE),
version: Barong::Application::VERSION
}
end
desc 'Get barong configurations'
get '/configs' do
{
session_expire_time: Barong::App.config.session_expire_time,
captcha_type: Barong::App.config.captcha,
captcha_id: (Barong::App.config.recaptcha_site_key if Barong::App.config.captcha == 'recaptcha'),
phone_verification_type: Barong::App.config.phone_verification,
password_min_entropy: Barong::App.config.password_min_entropy,
password_regexp: Barong::App.config.password_regexp
}.compact
end
desc 'Get auth0 configuration'
get '/configs/auth0' do
{
auth0_domain: Barong::App.config.auth0_domain,
auth0_client_id: Barong::App.config.auth0_client_id
}.compact
end
end
end
end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
# queries helping module
module API::V2::Queries
class ActivityFilter
attr_accessor :initial_scope
# initialize query
def initialize(initial_scope)
@initial_scope = initial_scope
end
# returns query with with all applied filters
def call(params)
params[:with_user] ? @initial_scope = @initial_scope.joins(:user) : @initial_scope
scoped = filter_by_date(@initial_scope, params[:from], params[:to])
scoped = filter_by_topic(scoped, params[:topic])
scoped = filter_by_action(scoped, params[:action])
scoped = filter_by_result(scoped, params[:result])
scoped = filter_by_uid(scoped, params[:uid])
scoped = filter_by_email(scoped, params[:email])
scoped = filter_by_target(scoped, params[:target_uid])
scoped = scoped.order('activities.id' => 'DESC') if params[:ordered]
scoped
end
private
# adds where(activities.created_at > from and activities.created_at < to) to query
def filter_by_date(scoped, from = nil, to = nil)
updated_scope = from ? scoped.where('activities.created_at >= ?', Time.at(from.to_i)) : scoped
to ? updated_scope.where('activities.created_at <= ?', Time.at(to.to_i)) : updated_scope
end
# adds where(activities.topic = topic) to query
def filter_by_topic(scoped, topic = nil)
topic ? scoped.where(activities: { topic: topic }) : scoped
end
# adds where(activities.action = action) to query
def filter_by_action(scoped, action = nil)
action ? scoped.where(activities: { action: action }) : scoped
end
# adds where(activities.result = result) to query
def filter_by_result(scoped, result = nil)
result ? scoped.where(activities: { result: result }) : scoped
end
# adds where(users.uid = uid) to query
def filter_by_uid(scoped, uid = nil)
uid ? scoped.where(users: { uid: uid }) : scoped
end
# adds where(users.email = email) to query
def filter_by_email(scoped, email = nil)
email ? scoped.where(users: { email: email }) : scoped
end
# adds where(activities.target_uid = target_uid) to query
def filter_by_target(scoped, target_uid = nil)
target_uid ? scoped.where(activities: { target_uid: target_uid }) : scoped
end
end
end

View File

@@ -0,0 +1,64 @@
# queries helping module
module API::V2::Queries
class UserFilter
attr_accessor :initial_scope
# initialize query to get User.all
def initialize(initial_scope)
@initial_scope = initial_scope.left_outer_joins(:profiles)
end
# returns query with with all applied filters
def call(params)
scoped = filter_by_date(initial_scope, params[:range], params[:from], params[:to])
scoped = filter_by_uid(scoped, params[:uid])
scoped = filter_by_email(scoped, params[:email])
scoped = filter_by_role(scoped, params[:role])
scoped = filter_by_country(scoped, params[:country])
scoped = filter_by_level(scoped, params[:level])
scoped = filter_by_state(scoped, params[:state])
scoped
end
private
# adds where(users.[created, updated]_at > from and users.[created, updated]_at < to) to query
def filter_by_date(scoped, range = 'created', from = nil, to = nil)
newer_than_sql = "users.#{range}_at >= ?"
older_than_sql = "users.#{range}_at <= ?"
updated_scope = from ? scoped.where(newer_than_sql, Time.at(from.to_i)) : scoped
to ? updated_scope.where(older_than_sql, Time.at(to.to_i)) : updated_scope
end
# adds where(users.uid = uid) to query
def filter_by_uid(scoped, uid = nil)
uid ? scoped.where(users: { uid: uid }) : scoped
end
# adds where(users.email = email) to query
def filter_by_email(scoped, email = nil)
email ? scoped.where(users: { email: email }) : scoped
end
# adds where(users.role = role) to query
def filter_by_role(scoped, role = nil)
role ? scoped.where(users: { role: role }) : scoped
end
# adds where(users.level = level) to query
def filter_by_level(scoped, level = nil)
level ? scoped.where(users: { level: level }) : scoped
end
# adds where(users.state = state) to query
def filter_by_state(scoped, state = nil)
state ? scoped.where(users: { state: state }) : scoped
end
# adds where(users.country = country) to query
def filter_by_country(scoped, country = nil)
country ? scoped.where(profiles: { country: country }) : scoped
end
end
end

View File

@@ -0,0 +1,47 @@
# queries helping module
module API::V2::Queries
class UserWithLabelFilter
attr_accessor :initial_scope
# initialize query to get User.all
def initialize(initial_scope)
@initial_scope = initial_scope.left_outer_joins(:labels)
end
# returns query with with all applied filters
def call(params)
scoped = filter_by_date(initial_scope, params[:range], params[:from], params[:to])
scoped = filter_by_key(scoped, params[:key])
scoped = filter_by_value(scoped, params[:value])
scoped = filter_by_scope(scoped, params[:scope])
scoped
end
private
# adds where(labels.[created, updated]_at > from and labels.[created, updated]_at < to) to query
def filter_by_date(scoped, range = 'created', from = nil, to = nil)
newer_than_sql = "labels.#{range}_at >= ?"
older_than_sql = "labels.#{range}_at <= ?"
updated_scope = from ? scoped.where(newer_than_sql, Time.at(from.to_i)) : scoped
to ? updated_scope.where(older_than_sql, Time.at(to.to_i)) : updated_scope
end
# adds where(labels.key = key) to query
def filter_by_key(scoped, key = nil)
key ? scoped.where(labels: { key: key }) : scoped
end
# adds where(labels.value = value) to query
def filter_by_value(scoped, value = nil)
value ? scoped.where(labels: { value: value }) : scoped
end
# adds where(labels.scope = scope) to query
def filter_by_scope(scoped, scope = nil)
scope ? scoped.where(labels: { scope: scope }) : scoped
end
end
end

View 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

View 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

View 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

View 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

View 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

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

View 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

View 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

View 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

View 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

View 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

38
app/api/v2/utils.rb Normal file
View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
module API::V2
module Utils
def remote_ip
# default behaviour, IP from HTTP_X_FORWARDED_FOR
ip = env['action_dispatch.remote_ip'].to_s
if Barong::App.config.gateway == 'akamai'
# custom header that contains only client IP
true_client_ip = request.env['HTTP_TRUE_CLIENT_IP'].to_s
# take IP from TRUE_CLIENT_IP only if its not nil or empty
ip = true_client_ip unless true_client_ip.nil? || true_client_ip.empty?
end
Rails.logger.debug "User login IP address: #{ip}"
return ip
end
def code_error!(errors, code)
final = errors.inject([]) do |result, (key, errs)|
result.concat(
errs.map { |e| e.values.first }
.uniq
.flatten
.map { |e| [key, e].join('.') }
)
end
error!({ errors: final }, code)
end
def admin_authorize!(*args)
AdminAbility.new(current_user).authorize!(*args)
rescue CanCan::AccessDenied
error!({ errors: ['admin.ability.not_permitted'] }, 401)
end
end
end

77
app/api/v2/validations.rb Normal file
View File

@@ -0,0 +1,77 @@
# frozen_string_literal: true
module API
module V2
module Validations
class AllowBlankValidator < Grape::Validations::AllowBlankValidator
# Default exception is costructed from `@api` class name.
# E.g
# @api.base.name => API::V2::Account::Withdraws
# default_message => "account.withdraw.missing_otp"
def message(_param)
api = @scope.instance_variable_get(:@api)
module_name = api.base.parent.name.humanize.demodulize
class_name = api.base.name.humanize.demodulize.singularize
# Return default API error message for Management module (no errors unify).
return super if module_name == 'management'
options_key?(:message) ? @option[:message] : default_exception(module_name, class_name)
end
def default_exception(module_name, class_name)
"#{module_name}.#{class_name}.empty_#{attrs.first}"
end
end
class PresenceValidator < Grape::Validations::PresenceValidator
# Default exception is costructed from `@api` class name.
# E.g
# @api.base.name => API::V2::Account::Withdraws
# default_message => "account.withdraw.missing_otp"
def message(_param)
api = @scope.instance_variable_get(:@api)
module_name = api.base.parent.name.humanize.demodulize
class_name = api.base.name.humanize.demodulize.singularize
# Return default API error message for Management module (no errors unify).
return super if module_name == 'management'
options_key?(:message) ? @option[:message] : default_exception(module_name, class_name)
end
def default_exception(module_name, class_name)
"#{module_name}.#{class_name}.missing_#{attrs.first}"
end
end
# Range validation for pagination tool
class Range < Grape::Validations::Base
def initialize(*)
super
@range = @option
end
def validate_param!(attr, params)
return unless (params[attr] || @required) && !@range.cover?(params[attr])
raise Grape::Exceptions::Validation, \
params: [@scope.full_name(attr)],
message: "must be in range: #{@range}."
end
end
# Greater then zero validation for integers
class IntegerGTZero < Grape::Validations::Base
def validate_param!(name, params)
return unless params.key?(name)
return if params[name].to_s.to_i.positive?
raise Grape::Exceptions::Validation,
params: [@scope.full_name(name)],
message: "#{name} must be greater than zero."
end
end
end
end
end

View File

@@ -0,0 +1,3 @@
//= link_tree ../images
//= link_directory ../javascripts .js
//= link_directory ../stylesheets .css

0
app/assets/images/.keep Normal file
View File

View File

@@ -0,0 +1,15 @@
// This is a manifest file that'll be compiled into application.js, which will include all the files
// listed below.
//
// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's
// vendor/assets/javascripts directory can be referenced here using a relative path.
//
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file. JavaScript code in this file should be added after the last require_* statement.
//
// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details
// about supported directives.
//
//= require rails-ujs
//= require activestorage
//= require_tree .

View File

@@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

View File

@@ -0,0 +1,15 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
* vendor/assets/stylesheets directory can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
* files in this directory. Styles in this file should be added after the last require_* statement.
* It is generally better to create a new file per style scope.
*
*= require_tree .
*= require_self
*/

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

View File

@@ -0,0 +1,4 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
end
end

View File

@@ -0,0 +1,2 @@
class ApplicationController < ActionController::Base
end

View File

@@ -0,0 +1,100 @@
# frozen_string_literal: true
require_dependency 'barong/authorize'
# Rails Metal base controller to manage AuthZ story
class AuthorizeController < ActionController::Metal
include AbstractController::Rendering
# /api/v2/auth endpoint
def authorize
@restrictions = Rails.cache.fetch('restrictions', expires_in: 5.minutes) { fetch_restrictions }
# whitelink path
unless params[:path] == 'api/v2/barong/identity/users/access'
Restriction::CATEGORIES.each do |category|
restriction = first_matched_restriction(category)
if restriction && category.in?(%w[blacklist maintenance])
return deny_access(category, restriction)
elsif restriction && category == 'blocklogin' && params[:path] == 'api/v2/barong/identity/sessions'
return deny_access(category, restriction)
elsif restriction && category == 'whitelist'
break
end
end
end
req = Barong::Authorize.new(request, params[:path]) # initialize params of request
# checks if request is blacklisted
return access_error!('authz.permission_denied', 401) if req.under_path_rules?('block')
response.status = 200
return if req.under_path_rules?('pass') # check if request is whitelisted
response.headers['Authorization'] = req.auth # sets bearer token
rescue Barong::Authorize::AuthError => e # returns error from validations
response.body = e.message
response.status = e.code
end
private
# finds first (if exist) the most precise restriction ip -> ip_subnet -> continent -> country
def first_matched_restriction(category)
request_ip = remote_ip
country = Barong::GeoIP.info(ip: request_ip, key: :country)
continent = Barong::GeoIP.info(ip: request_ip, key: :continent)
if restriction = @restrictions[category]['all']&.find { |r| r.present? } then return restriction end
if restriction = @restrictions[category]['ip']&.find { |r| r.include?(request_ip) } then return restriction end
if restriction = @restrictions[category]['ip_subnet']&.find { |r| IPAddr.new(r[0])&.include?(request_ip) } then return restriction end
if restriction = @restrictions[category]['continent']&.find { |r| r[0]&.casecmp?(continent) } then return restriction end
if restriction = @restrictions[category]['country']&.find { |r| r[0]&.casecmp?(country) } then return restriction end
end
# as a result gives complex Hash, { category: { scope: values, scope: values }, category: { scope: values, scope: values } }
# { "blacklist"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]},
# "whitelist"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]},
# "maintenance"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]} }
def fetch_restrictions
enabled = Restriction.where(state: 'enabled').to_a
Restriction::CATEGORIES.inject({}) do |table, category|
grouped_by_category = enabled.select { |r| r.category == category }
grouped_by_scope = Restriction::SCOPES.inject({}) do |table, scope|
scope_restrictions = grouped_by_category.select { |r| r.scope == scope }.pluck(:value, :code)
table.tap { |t| t[scope] = scope_restrictions }
end
table.tap { |t| t[category] = grouped_by_scope }
end
end
def deny_access(category, restriction)
Rails.logger.info("Access denied for ip #{request.remote_ip} because of #{restriction[0]} restriction")
access_error!("authz.restrict.#{category}", restriction[1])
end
def session
request.session
end
# error for blacklisted routes
def access_error!(text, code)
response.status = code
response.body = { 'errors': [text] }.to_json
end
def remote_ip
# default behaviour, IP from HTTP_X_FORWARDED_FOR
ip = request.remote_ip
if Barong::App.config.gateway == 'akamai'
# custom header that contains only client IP
true_client_ip = request.env['HTTP_TRUE_CLIENT_IP']
# take IP from TRUE_CLIENT_IP only if its not nil or empty
ip = true_client_ip unless true_client_ip.nil? || true_client_ip.empty?
end
ip
end
end

View File

View File

@@ -0,0 +1,2 @@
module ApplicationHelper
end

View File

@@ -0,0 +1,7 @@
# frozen_string_literal: true
class SaltedCrc32
def self.generate_hash(value)
Zlib::crc32(value + Barong::App.config.crc32_salt)
end
end

View File

@@ -0,0 +1,2 @@
class ApplicationJob < ActiveJob::Base
end

View File

@@ -0,0 +1,4 @@
# frozen_string_literal: true
class ApplicationMailer < ActionMailer::Base
end

23
app/mailers/postmaster.rb Normal file
View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
class Postmaster < ApplicationMailer
layout 'mailer'
def process_payload(params)
@record = params[:record]
@changes = params[:changes]
@user = params[:user]
@logo = params[:logo]
sender = "#{Barong::App.config.sender_name} <#{Barong::App.config.sender_email}>"
email_options = {
subject: params[:subject],
template_name: params[:template_name],
from: sender,
to: @user.email
}
mail(email_options)
end
end

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

Some files were not shown because too many files have changed in this diff Show More