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

73
.codeclimate.yml Normal file
View File

@@ -0,0 +1,73 @@
#TODO enable codeclimate in spec files and fix issues
version: '2'
checks:
method-complexity:
config:
threshold: 10
file-lines:
config:
threshold: 400
plugins:
brakeman:
enabled: true
rubocop:
enabled: false
checks:
Rubocop/Metrics/ClassLength:
enabled: false
Rubocop/Metrics/BlockLength:
enabled: false
Rubocop/Style/RedundantSelf:
enabled: false
Rubocop/Metrics/AbcSize:
enabled: false
Rubocop/Layout/AlignHash:
enabled: false
Rubocop/Metrics/LineLength:
enabled: false
reek:
enabled: false
checks:
FeatureEnvy:
enabled: false
RepeatedConditional:
enabled: false
NilCheck:
enabled: false
InstanceVariableAssumption:
enabled: false
IrresponsibleModule:
enabled: false
UncommunicativeModuleName:
enabled: false
exclude_patterns:
- spec/
bundler-audit:
enabled: true
todo:
enabled: false
fixme:
enabled: false
duplication:
enabled: true
exclude_patterns:
- spec/
- app/api/
config:
languages:
- ruby
exclude_patterns:
- db/
- ci/bump.rb
- node_modules/
- vendor/
- config/initializers/*
- config/plugins.rb
- config/
- test/
- config.ru
- db/
- dist/
- script/
- spec/

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
Dockerfile
.dockerignore
.git
.gitignore
LICENSE.md
Makefile
README.md
.travis.yml
VERSION
config/charts

408
.drone.yml Normal file
View File

@@ -0,0 +1,408 @@
---
kind: pipeline
name: "Mysql 5.7"
services:
- name: database
image: mysql:5.7
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: vault
image: vault:0.11.4
ports:
- 8200
environment:
SKIP_SETCAP: 1
BARONG_VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
BARONG_VAULT_ADDRESS: http://vault:8200
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: Configure Vault
image: vault:0.11.4
commands:
- vault secrets disable secret
- vault secrets enable -path=secret -version=1 kv
- vault secrets enable totp
environment:
VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
VAULT_ADDR: http://vault:8200
- name: Run rspec
image: ruby:2.6.6
environment:
DATABASE_HOST: database
BARONG_EVENT_API_RABBITMQ_HOST: rabbitmq
BARONG_VAULT_ADDRESS: http://vault:8200
BARONG_VAULT_TOKEN: changeme
RAILS_ENV: test
REDIS_URL: redis://redis:6379
BARONG_REDIS_URL: redis://redis:6379
RAILS_MASTER_KEY:
from_secret: rails_master_key
commands:
- gem update bundler
- bundle install --system
- ./bin/init_config
- rake db:create db:migrate
- bundle exec rspec
- name: Bump and tag
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
BRANCH_NAME: ${DRONE_BRANCH}
REPO_NAME: ${DRONE_REPO}
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:prebuild
when:
branch:
- "*-stable"
- name: Tag snapshot
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
BRANCH_NAME: master
REPO_NAME: ${DRONE_REPO}
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:prebuild
when:
branch:
- master
- "fix/*"
- "integration/*"
- name: Generate docs
image: quay.io/openware/ruby-node:2.6.5
environment:
DATABASE_HOST: database
BARONG_EVENT_API_RABBITMQ_HOST: rabbitmq
BARONG_VAULT_ADDRESS: http://vault:8200
RAILS_MASTER_KEY:
from_secret: rails_master_key
commands:
- gem update bundler
- bundle install
- bundle exec bin/gendocs
when:
branch:
- master
- "*-stable"
- name: Docker build and push
image: plugins/docker
environment:
MAXMIND_LICENSE_KEY:
from_secret: maxmind_license_key
settings:
username:
from_secret: quay_username
password:
from_secret: quay_password
repo: quay.io/openware/barong
registry: quay.io
build_args_from_env:
- MAXMIND_LICENSE_KEY
when:
branch:
- master
- "*-stable"
- "fix/*"
- "integration/*"
- name: Redeploy on master.devkube.com
image: rubykube/microkube:0.2.0
environment:
WEBHOOK_JWT_SECRET:
from_secret: devkube_webhook_secret
commands:
- export latest_image=quay.io/openware/barong:$(cat .tags)
- cd /home/app
- bundle exec rake payload:send[barong,$latest_image,http://master.devkube.com:1337]
when:
branch:
- master
- name: Push and Notify
image: quay.io/openware/sdk-citools:2.6.7
environment:
BOT_USERNAME: kite-bot
BOT_NAME: Kite Bot
BOT_EMAIL: kite-bot@heliostech.fr
BRANCH_NAME: ${DRONE_BRANCH}
SDK_BRANCH: ${DRONE_BRANCH}
REPO_NAME: ${DRONE_REPO}
TELEGRAM_BOT_TOKEN:
from_secret: telegram_bot_token
TELEGRAM_CHAT_ID:
from_secret: telegram_chat_id
SLACK_TOKEN:
from_secret: slack_token
SLACK_CHANNEL:
from_secret: slack_channel
GITHUB_API_KEY:
from_secret: kite_bot_key
commands:
- BUNDLE_GEMFILE=/sdk/Gemfile bundle exec rake --rakefile=/sdk/Rakefile ci:postbuild[/drone/src]
when:
branch:
- master
- "*-stable"
- name: "Update global OpenDAX version"
image: quay.io/openware/goci:0.0.7
environment:
GIT_TOKEN:
from_secret: kite_bot_key
commands:
- /app/goci versions
when:
branch:
- "*-stable"
trigger:
event:
- push
image_pull_secrets:
- dockerconfigjson
---
kind: pipeline
name: "MariaDB 10.3"
services:
- name: database
image: mariadb:10.3
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: vault
image: vault:0.11.4
ports:
- 8200
environment:
SKIP_SETCAP: 1
BARONG_VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
BARONG_VAULT_ADDRESS: http://vault:8200
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: Configure Vault
image: vault:0.11.4
commands:
- vault secrets disable secret
- vault secrets enable -path=secret -version=1 kv
- vault secrets enable totp
environment:
VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
VAULT_ADDR: http://vault:8200
- name: Run rspec
image: ruby:2.6.6
environment:
DATABASE_HOST: database
BARONG_EVENT_API_RABBITMQ_HOST: rabbitmq
BARONG_VAULT_ADDRESS: http://vault:8200
BARONG_VAULT_TOKEN: changeme
RAILS_ENV: test
REDIS_URL: redis://redis:6379
BARONG_REDIS_URL: redis://redis:6379
RAILS_MASTER_KEY:
from_secret: rails_master_key
commands:
- gem update bundler
- bundle install --system
- ./bin/init_config
- rake db:create db:migrate
- bundle exec rspec
trigger:
event:
- push
branch:
- "compatibility/*"
- "*-stable"
---
kind: pipeline
name: "MySQL 8.0"
services:
- name: database
image: mysql:8.0
commands:
- /entrypoint.sh --default-authentication-plugin=mysql_native_password
ports:
- 3306
environment:
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: vault
image: vault:0.11.4
ports:
- 8200
environment:
SKIP_SETCAP: 1
BARONG_VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
BARONG_VAULT_ADDRESS: http://vault:8200
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: Configure Vault
image: vault:0.11.4
commands:
- vault secrets disable secret
- vault secrets enable -path=secret -version=1 kv
- vault secrets enable totp
environment:
VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
VAULT_ADDR: http://vault:8200
- name: Run rspec
image: ruby:2.6.6
environment:
DATABASE_HOST: database
BARONG_EVENT_API_RABBITMQ_HOST: rabbitmq
BARONG_VAULT_ADDRESS: http://vault:8200
BARONG_VAULT_TOKEN: changeme
RAILS_ENV: test
REDIS_URL: redis://redis:6379
BARONG_REDIS_URL: redis://redis:6379
RAILS_MASTER_KEY:
from_secret: rails_master_key
commands:
- gem update bundler
- bundle install --system
- ./bin/init_config
- rake db:create db:migrate
- bundle exec rspec
trigger:
event:
- push
branch:
- "compatibility/*"
- "*-stable"
---
kind: pipeline
name: "PostgreSQL 13.0"
services:
- name: database
image: postgres:13.0
ports:
- 5432
environment:
POSTGRES_PASSWORD: changeme
- name: rabbitmq
image: rabbitmq:3.8.9
ports:
- 5672
- name: vault
image: vault:0.11.4
ports:
- 8200
environment:
SKIP_SETCAP: 1
BARONG_VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
BARONG_VAULT_ADDRESS: http://vault:8200
- name: redis
image: redis:4.0
ports:
- 6379
steps:
- name: Configure Vault
image: vault:0.11.4
commands:
- vault secrets disable secret
- vault secrets enable -path=secret -version=1 kv
- vault secrets enable totp
environment:
VAULT_TOKEN: changeme
VAULT_DEV_ROOT_TOKEN_ID: changeme
VAULT_ADDR: http://vault:8200
- name: Run rspec
image: ruby:2.6.6
environment:
DATABASE_HOST: database
DATABASE_ADAPTER: postgresql
DATABASE_PORT: "5432"
DATABASE_PASS: changeme
DATABASE_USER: postgres
BARONG_EVENT_API_RABBITMQ_HOST: rabbitmq
BARONG_VAULT_ADDRESS: http://vault:8200
BARONG_VAULT_TOKEN: changeme
RAILS_ENV: test
REDIS_URL: redis://redis:6379
BARONG_REDIS_URL: redis://redis:6379
RAILS_MASTER_KEY:
from_secret: rails_master_key
commands:
- gem update bundler
- bundle install --system
- ./bin/init_config
- rake db:create db:migrate
- bundle exec rspec
trigger:
event:
- push
branch:
- "compatibility/*"
- "*-stable"

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
*.sh text eol=lf

36
.gitignore vendored Normal file
View File

@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore uploaded files in development
/storage/*
!/storage/.keep
/node_modules
/yarn-error.log
/public/uploads
/public/assets
.byebug_history
# Ignore master key for decrypting credentials and more.
/config/master.key
/config/rsa-key.pub
/config/rsa-key
/config/mailer.yml
/config/cloudflare_ips.yml
/config/management_api.yml
/config/seeds.yml
barong.env
.idea/

1
.rspec Normal file
View File

@@ -0,0 +1 @@
--require spec_helper

1
.ruby-version Normal file
View File

@@ -0,0 +1 @@
ruby-2.6.6

2056
CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# syntax=docker/dockerfile:1.4
# Fast app image — gems come from deps base (custom/dalan-deps:1).
# Rebuild deps when Gemfile.lock changes: docker build -f Dockerfile.deps -t custom/dalan-deps:1 .
ARG DEPS_IMAGE=custom/dalan-deps:1
FROM ${DEPS_IMAGE}
ARG MAXMINDDB_LINK
ARG MAXMINDDB_LICENSE_KEY=T6ElPBlyOOuCyjzw
ENV MAXMINDDB_LINK=${MAXMINDDB_LINK:-https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&suffix=tar.gz&license_key=${MAXMINDDB_LICENSE_KEY}} \
RAILS_ENV=production \
APP_HOME=/home/app
USER app
WORKDIR $APP_HOME
COPY --chown=app:app . .
RUN mkdir -p ${APP_HOME}/geolite \
&& (wget -qO ${APP_HOME}/geolite.tar.gz "${MAXMINDDB_LINK}" \
&& tar xzf ${APP_HOME}/geolite.tar.gz -C ${APP_HOME}/geolite --strip-components 1 \
&& rm ${APP_HOME}/geolite.tar.gz \
|| echo "GeoLite download skipped — using placeholder") \
&& test -f ${APP_HOME}/geolite/GeoLite2-Country.mmdb || touch ${APP_HOME}/geolite/GeoLite2-Country.mmdb
ENV BARONG_MAXMINDDB_PATH=${APP_HOME}/geolite/GeoLite2-Country.mmdb
RUN curl -fsSL https://www.cloudflare.com/ips-v4 >> ${APP_HOME}/config/cloudflare_ips.yml \
&& echo "" >> ${APP_HOME}/config/cloudflare_ips.yml \
&& curl -fsSL https://www.cloudflare.com/ips-v6 >> ${APP_HOME}/config/cloudflare_ips.yml
RUN ruby ./bin/init_config \
&& bundle exec rake tmp:create
EXPOSE 8080
CMD ["bundle", "exec", "puma", "--config", "config/puma.rb"]

35
Dockerfile.deps Normal file
View File

@@ -0,0 +1,35 @@
# syntax=docker/dockerfile:1.4
# Heavy layer: Ruby gems + system packages. Rebuild only when Gemfile.lock changes.
# docker build -f Dockerfile.deps -t custom/dalan-deps:1 .
FROM ruby:2.7.8
ARG UID=1000
ARG GID=1000
ENV APP_HOME=/home/app \
BUNDLE_PATH=/opt/vendor/bundle \
TZ=UTC
RUN groupadd -r --gid ${GID} app \
&& useradd --system --create-home --home ${APP_HOME} --shell /sbin/nologin --no-log-init \
--gid ${GID} --uid ${UID} app
WORKDIR $APP_HOME
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends default-libmysqlclient-dev git curl wget \
&& rm -rf /var/lib/apt/lists/* \
&& gem install bundler:2.2.16
COPY Gemfile Gemfile.lock ./
RUN mkdir -p ${BUNDLE_PATH} \
&& chown -R app:app ${BUNDLE_PATH} ${APP_HOME} \
&& su app -s /bin/bash -c "bundle config set --local path ${BUNDLE_PATH} \
&& bundle config set --local without 'development test' \
&& bundle install --jobs $(nproc)"
USER app
ENV PATH="${BUNDLE_PATH}/bin:${PATH}"

125
Gemfile Normal file
View File

@@ -0,0 +1,125 @@
source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby '~> 2.7'
gem 'aliyun-sdk', '~> 0.7.0'
gem 'api-pagination', '~> 4.8.2'
gem 'env-tweaks', '~> 1.0.0'
gem 'hashie', '~> 3.6.0'
# storage related gems
gem 'carrierwave', '~> 2.1', '>= 2.1.0'
# fog's core, shared behaviors without API and provider specifics
gem 'fog-core', '~> 2.1.0'
# alicloud support
gem 'fog-aliyun', '~> 0.3.5'
# aws support (amazon)
gem 'fog-aws', '~> 3.5.2'
# gcp support (google)
gem 'fog-google', '~> 1.9.1'
gem 'kycaid'
gem 'sidekiq', '>= 6.4.0', '< 7'
# GLI
gem 'gli', '~> 2.19.0'
##
## abilities and permissions for admin API module
gem 'cancancan', '~> 2.3.0'
gem 'hiredis', '~> 0.6.1'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.4', '>= 5.2.4.4'
# Use mysql as the database for Active Record
gem 'mysql2', '>= 0.4.4', '< 0.6.0'
# Use Puma as the app server
gem 'puma', '~> 3.12', '>= 3.12.6'
# Use Uglifier as compressor for JavaScript assets
gem 'uglifier', '>= 1.3.0'
# See https://github.com/rails/execjs#readme for more supported runtimes
gem 'mini_racer', platforms: :ruby
gem 'maxmind-db', '~> 1.0'
gem 'kaminari', '>= 1.2.1'
gem 'peatio', '~> 0.4.4'
gem 'rack-cors', '~> 1.0.2'
# REST-like API framework for Ruby
gem 'grape', '~> 1.4'
gem 'grape-entity', '~> 0.8'
gem 'grape-swagger', '~> 1.2'
gem 'grape-swagger-entity', '~> 0.5'
gem 'grape_logging', '~> 1.8'
gem 'memoist', '~> 0.16'
gem 'jwt', '~> 2.2'
gem 'jwt-multisig', '~> 1.0', '>= 1.0.4'
gem 'bunny'
gem 'phonelib', '~> 0.6.45'
gem 'twilio-ruby', '~> 5.25.4'
gem 'vault', '~> 0.1'
gem 'vault-rails', git: 'https://github.com/rubykube/vault-rails'
# Use Redis adapter to run Action Cable in production
gem 'redis', '~> 4.0', :require => ['redis', 'redis/connection/hiredis']
gem 'bcrypt', '~> 3.1'
# Email validators. Lock at 1.6.0 to use /strict dependency
gem 'email_validator', '= 1.6.0', require: 'email_validator/strict'
gem 'countries', require: 'countries/global'
gem 'browser', require: "browser/browser"
gem 'bump'
# Use gem to verify recatpcha on server side
gem 'recaptcha', '>= 5.2.1'
# Password validators
gem 'strong_password', '~> 0.0.8'
# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'
# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false
# Add the Sentry Ruby SDK
gem 'sentry-raven', '~> 2.9.0'
gem 'pry-rails'
#for kave negar third party
gem 'kavenegar-ruby'
group :development, :test do
# Call 'byebug' or 'binding.pry' anywhere in the code to stop execution and get a debugger console
gem 'pry-byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'factory_bot_rails', '~> 4.11', '>= 4.11.1'
gem 'faker', '~> 2.1'
end
group :development do
gem 'grape_on_rails_routes', '~> 0.3.2'
gem 'web-console', '>= 3.7.0'
gem 'listen', '>= 3.0.5', '< 3.2'
gem 'annotate', '~> 2.7', '>= 2.7.5'
end
group :test do
gem 'capybara', '>= 3.29.0'
# gem 'selenium-webdriver'
# gem 'chromedriver-helper'
gem 'rspec-rails', '~> 3.9', '>= 3.9.1'
gem 'shoulda-matchers', '~> 4.0.1.0'
gem 'rails-controller-testing', '>= 1.0.5'
gem 'database_cleaner', '~> 2.0.1'
end
gem "pg", "~> 1.2"
gem 'dotenv-rails'
# Security patches — see Docs/10-cve-audit.md (phase 1)
gem 'rack', '~> 2.2.13'
gem 'nokogiri', '~> 1.15.5'
gem 'rexml', '>= 3.3.9'
gem 'tzinfo', '~> 1.2.10'
gem 'rails-html-sanitizer', '>= 1.4.4'

541
Gemfile.lock Normal file
View File

@@ -0,0 +1,541 @@
GIT
remote: https://github.com/rubykube/vault-rails
revision: b5e6570d1268fe0153b72200bbc58c5254d6c848
specs:
vault-rails (0.6.0)
rails (>= 4.1)
vault (~> 0.14)
GEM
remote: https://rubygems.org/
specs:
actioncable (5.2.4.4)
actionpack (= 5.2.4.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
actionmailer (5.2.4.4)
actionpack (= 5.2.4.4)
actionview (= 5.2.4.4)
activejob (= 5.2.4.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.2.4.4)
actionview (= 5.2.4.4)
activesupport (= 5.2.4.4)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.2.4.4)
activesupport (= 5.2.4.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.2.4.4)
activesupport (= 5.2.4.4)
globalid (>= 0.3.6)
activemodel (5.2.4.4)
activesupport (= 5.2.4.4)
activerecord (5.2.4.4)
activemodel (= 5.2.4.4)
activesupport (= 5.2.4.4)
arel (>= 9.0)
activestorage (5.2.4.4)
actionpack (= 5.2.4.4)
activerecord (= 5.2.4.4)
marcel (~> 0.3.1)
activesupport (5.2.4.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
aliyun-sdk (0.7.3)
nokogiri (~> 1.6)
rest-client (~> 2.0)
amq-protocol (2.3.2)
amqp (1.8.0)
amq-protocol (>= 2.2.0)
eventmachine
annotate (2.7.5)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 13.0)
api-pagination (4.8.2)
arel (9.0.0)
aws-eventstream (1.1.0)
aws-sigv4 (1.2.2)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.15)
bindex (0.8.1)
bootsnap (1.4.7)
msgpack (~> 1.0)
browser (4.2.0)
builder (3.2.4)
bump (0.9.0)
bunny (2.15.0)
amq-protocol (~> 2.3, >= 2.3.1)
byebug (11.1.3)
cancancan (2.3.0)
capybara (3.33.0)
addressable
mini_mime (>= 0.1.3)
nokogiri (~> 1.8)
rack (>= 1.6.0)
rack-test (>= 0.6.3)
regexp_parser (~> 1.5)
xpath (~> 3.2)
carrierwave (2.1.0)
activemodel (>= 5.0.0)
activesupport (>= 5.0.0)
addressable (~> 2.6)
image_processing (~> 1.1)
mimemagic (>= 0.3.0)
mini_mime (>= 0.1.3)
clamp (1.3.1)
coderay (1.1.3)
concurrent-ruby (1.1.7)
connection_pool (2.5.5)
countries (3.0.1)
i18n_data (~> 0.10.0)
sixarm_ruby_unaccent (~> 1.1)
unicode_utils (~> 1.4)
crass (1.0.7)
database_cleaner (2.0.1)
database_cleaner-active_record (~> 2.0.0)
database_cleaner-active_record (2.0.0)
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
declarative (0.0.20)
declarative-option (0.1.0)
diff-lcs (1.4.4)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
dotenv-rails (2.7.6)
dotenv (= 2.7.6)
railties (>= 3.2)
dry-configurable (0.11.6)
concurrent-ruby (~> 1.0)
dry-core (~> 0.4, >= 0.4.7)
dry-equalizer (~> 0.2)
dry-container (0.7.2)
concurrent-ruby (~> 1.0)
dry-configurable (~> 0.1, >= 0.1.3)
dry-core (0.4.9)
concurrent-ruby (~> 1.0)
dry-equalizer (0.3.0)
dry-inflector (0.2.0)
dry-logic (1.0.6)
concurrent-ruby (~> 1.0)
dry-core (~> 0.2)
dry-equalizer (~> 0.2)
dry-types (1.4.0)
concurrent-ruby (~> 1.0)
dry-container (~> 0.3)
dry-core (~> 0.4, >= 0.4.4)
dry-equalizer (~> 0.3)
dry-inflector (~> 0.1, >= 0.1.2)
dry-logic (~> 1.0, >= 1.0.2)
em-websocket (0.5.1)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0.6.0)
email_validator (1.6.0)
activemodel
env-tweaks (1.0.0)
activesupport (>= 3.0, < 6.0)
erubi (1.9.0)
eventmachine (1.2.7)
excon (0.76.0)
execjs (2.7.0)
factory_bot (4.11.1)
activesupport (>= 3.0.0)
factory_bot_rails (4.11.1)
factory_bot (~> 4.11.1)
railties (>= 3.0.0)
faker (2.13.0)
i18n (>= 1.6, < 2)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
feedjira (3.1.2)
loofah (>= 2.3.1)
sax-machine (>= 1.0)
ffi (1.13.1)
fog-aliyun (0.3.18)
aliyun-sdk (~> 0.7.3)
fog-core
fog-json
ipaddress (~> 0.8)
xml-simple (~> 1.1)
fog-aws (3.5.2)
fog-core (~> 2.1)
fog-json (~> 1.1)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
fog-core (2.1.0)
builder
excon (~> 0.58)
formatador (~> 0.2)
mime-types
fog-google (1.9.1)
fog-core (<= 2.1.0)
fog-json (~> 1.2)
fog-xml (~> 0.1.0)
google-api-client (~> 0.23.0)
fog-json (1.2.0)
fog-core
multi_json (~> 1.10)
fog-xml (0.1.3)
fog-core
nokogiri (>= 1.5.11, < 2.0.0)
formatador (0.2.5)
gli (2.19.2)
globalid (0.4.2)
activesupport (>= 4.2.0)
google-api-client (0.23.9)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.5, < 0.7.0)
httpclient (>= 2.8.1, < 3.0)
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
googleauth (0.6.7)
faraday (~> 0.12)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (~> 0.7)
grape (1.4.0)
activesupport
builder
dry-types (>= 1.1)
mustermann-grape (~> 1.0.0)
rack (>= 1.3.0)
rack-accept
grape-entity (0.8.1)
activesupport (>= 3.0.0)
multi_json (>= 1.3.2)
grape-swagger (1.2.1)
grape (~> 1.3)
grape-swagger-entity (0.5.1)
grape-entity (>= 0.6.0)
grape-swagger (>= 1.2.0)
grape_logging (1.8.3)
grape
rack
grape_on_rails_routes (0.3.2)
rails (>= 3.1.1)
hashie (3.6.0)
hiredis (0.6.3)
http-accept (1.7.0)
http-cookie (1.0.3)
domain_name (~> 0.5)
http_parser.rb (0.6.0)
httpclient (2.8.3)
i18n (1.8.5)
concurrent-ruby (~> 1.0)
i18n_data (0.10.0)
image_processing (1.11.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
ipaddress (0.8.3)
json (2.3.1)
jwt (2.2.2)
jwt-multisig (1.0.4)
activesupport (>= 4.0)
jwt (~> 2.2)
kaminari (1.2.1)
activesupport (>= 4.1.0)
kaminari-actionview (= 1.2.1)
kaminari-activerecord (= 1.2.1)
kaminari-core (= 1.2.1)
kaminari-actionview (1.2.1)
actionview
kaminari-core (= 1.2.1)
kaminari-activerecord (1.2.1)
activerecord
kaminari-core (= 1.2.1)
kaminari-core (1.2.1)
kavenegar-ruby (1.0.0)
faraday
faraday_middleware
feedjira
i18n
json
validatable (~> 1.6)
kycaid (1.0.0)
faraday
json
libv8 (8.4.255.0)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
ruby_dep (~> 1.2)
loofah (2.25.1)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
mail (2.7.1)
mini_mime (>= 0.1.1)
marcel (0.3.3)
mimemagic (~> 0.3.2)
maxmind-db (1.1.1)
memoist (0.16.2)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.0512)
mimemagic (0.3.10)
nokogiri (~> 1)
rake
mini_magick (4.10.1)
mini_mime (1.0.2)
mini_portile2 (2.8.9)
mini_racer (0.3.1)
libv8 (~> 8.4.255)
minitest (5.14.2)
msgpack (1.3.3)
multi_json (1.15.0)
multipart-post (2.1.1)
mustermann (1.1.1)
ruby2_keywords (~> 0.0.1)
mustermann-grape (1.0.1)
mustermann (>= 1.0.0)
mysql2 (0.5.3)
netrc (0.11.0)
nio4r (2.5.3)
nokogiri (1.15.7)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
os (1.1.1)
peatio (0.4.5)
amqp
bunny
clamp
em-websocket
eventmachine
jwt
mysql2
pg (1.2.3)
phonelib (0.6.45)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-byebug (3.9.0)
byebug (~> 11.0)
pry (~> 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.5)
puma (3.12.6)
racc (1.8.1)
rack (2.2.23)
rack-accept (0.4.5)
rack (>= 0.4)
rack-cors (1.0.6)
rack (>= 1.6.0)
rack-test (1.1.0)
rack (>= 1.0, < 3)
rails (5.2.4.4)
actioncable (= 5.2.4.4)
actionmailer (= 5.2.4.4)
actionpack (= 5.2.4.4)
actionview (= 5.2.4.4)
activejob (= 5.2.4.4)
activemodel (= 5.2.4.4)
activerecord (= 5.2.4.4)
activestorage (= 5.2.4.4)
activesupport (= 5.2.4.4)
bundler (>= 1.3.0)
railties (= 5.2.4.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
activesupport (>= 5.0.1.rc1)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.5.0)
loofah (~> 2.19, >= 2.19.1)
railties (5.2.4.4)
actionpack (= 5.2.4.4)
activesupport (= 5.2.4.4)
method_source
rake (>= 0.8.7)
thor (>= 0.19.0, < 2.0)
rake (12.3.3)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
recaptcha (5.5.0)
json
redis (4.8.1)
regexp_parser (1.7.1)
representable (3.0.4)
declarative (< 0.1.0)
declarative-option (< 0.2.0)
uber (< 0.2.0)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
retriable (3.1.2)
rexml (3.4.4)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (3.9.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-support (~> 3.9.0)
rspec-support (3.9.3)
ruby-vips (2.0.17)
ffi (~> 1.9)
ruby2_keywords (0.0.2)
ruby_dep (1.5.0)
sax-machine (1.3.2)
sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0)
shoulda-matchers (4.0.1)
activesupport (>= 4.2.0)
sidekiq (6.5.12)
connection_pool (>= 2.2.5, < 3)
rack (~> 2.0)
redis (>= 4.5.0, < 5)
signet (0.14.0)
addressable (~> 2.3)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
sixarm_ruby_unaccent (1.2.0)
sprockets (4.0.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
strong_password (0.0.9)
thor (1.0.1)
thread_safe (0.3.6)
twilio-ruby (5.25.4)
faraday (~> 0.9)
jwt (>= 1.5, <= 2.5)
nokogiri (>= 1.6, < 2.0)
tzinfo (1.2.11)
thread_safe (~> 0.1)
uber (0.1.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.7)
unicode_utils (1.4.0)
validatable (1.6.7)
vault (0.15.0)
aws-sigv4
web-console (3.7.0)
actionview (>= 5.0)
activemodel (>= 5.0)
bindex (>= 0.4.0)
railties (>= 5.0)
websocket-driver (0.7.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xml-simple (1.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
PLATFORMS
ruby
DEPENDENCIES
aliyun-sdk (~> 0.7.0)
annotate (~> 2.7, >= 2.7.5)
api-pagination (~> 4.8.2)
bcrypt (~> 3.1)
bootsnap (>= 1.1.0)
browser
bump
bunny
cancancan (~> 2.3.0)
capybara (>= 3.29.0)
carrierwave (~> 2.1, >= 2.1.0)
countries
database_cleaner (~> 2.0.1)
dotenv-rails
email_validator (= 1.6.0)
env-tweaks (~> 1.0.0)
factory_bot_rails (~> 4.11, >= 4.11.1)
faker (~> 2.1)
fog-aliyun (~> 0.3.5)
fog-aws (~> 3.5.2)
fog-core (~> 2.1.0)
fog-google (~> 1.9.1)
gli (~> 2.19.0)
grape (~> 1.4)
grape-entity (~> 0.8)
grape-swagger (~> 1.2)
grape-swagger-entity (~> 0.5)
grape_logging (~> 1.8)
grape_on_rails_routes (~> 0.3.2)
hashie (~> 3.6.0)
hiredis (~> 0.6.1)
jwt (~> 2.2)
jwt-multisig (~> 1.0, >= 1.0.4)
kaminari (>= 1.2.1)
kavenegar-ruby
kycaid
listen (>= 3.0.5, < 3.2)
maxmind-db (~> 1.0)
memoist (~> 0.16)
mini_racer
mysql2 (>= 0.4.4, < 0.6.0)
nokogiri (~> 1.15.5)
peatio (~> 0.4.4)
pg (~> 1.2)
phonelib (~> 0.6.45)
pry-byebug
pry-rails
puma (~> 3.12, >= 3.12.6)
rack (~> 2.2.13)
rack-cors (~> 1.0.2)
rails (~> 5.2.4, >= 5.2.4.4)
rails-controller-testing (>= 1.0.5)
rails-html-sanitizer (>= 1.4.4)
recaptcha (>= 5.2.1)
redis (~> 4.0)
rexml (>= 3.3.9)
rspec-rails (~> 3.9, >= 3.9.1)
sentry-raven (~> 2.9.0)
shoulda-matchers (~> 4.0.1.0)
sidekiq (>= 6.4.0, < 7)
strong_password (~> 0.0.8)
twilio-ruby (~> 5.25.4)
tzinfo (~> 1.2.10)
uglifier (>= 1.3.0)
vault (~> 0.1)
vault-rails!
web-console (>= 3.7.0)
RUBY VERSION
ruby 2.7.8p225
BUNDLED WITH
2.2.16

202
LICENSE.md Normal file
View File

@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

113
README.md Normal file
View File

@@ -0,0 +1,113 @@
<div dir="rtl">
# Dalan — احراز هویت Fibitex (Barong)
**Dalan** فورک [Barong](https://github.com/openware/barong) 2.6 برای JWT، KYC، و مدیریت کاربر Fibitex.
| OpenDAX | Fibitex |
|---------|---------|
| Barong | **Dalan** |
| `/api/v2/barong` | `/api/v2/dalan` |
## پیش‌نیازها
Ruby 2.6.6، MySQL، Vault (در stack Alvand-P)
## راه‌اندازی سریع
<div dir="ltr">
```bash
cd ../Alvand-P && rake service:all
```
</div>
## مستندات
| سند | موضوع |
|-----|--------|
| [`Docs/03-dalan-microservice.md`](../Docs/03-dalan-microservice.md) | معماری Dalan |
| [`Docs/Fibitex-staging-deploy.md`](../Docs/Fibitex-staging-deploy.md) | Deploy |
| [`Alvand-P/Docs/QA-troubleshooting.md`](../Alvand-P/Docs/QA-troubleshooting.md) | عیب‌یابی |
| [`Docs/README.md`](../Docs/README.md) | فهرست مرکزی |
</div>
---
# Dalan
it is fork project of barong
### Barong
[![Build Status](https://ci.openware.work/api/badges/openware/barong/status.svg)](https://ci.openware.work/openware/barong)
Barong is a authentication service for microservice architectures using JWT standard.
It's developped and maintained by [Openware](https://www.openware.com) team.
# Overview
It includes the following features:
- Registration of users
- Role based access control (RBAC)
- Embedded KyC process
- Integrated [KycAID](https://www.openware.com/sdk/docs/barong/kycaid.html) plugin
- Mailing system: event based, support multi-language, secured by cryptographic signatures
- [Service accounts](https://www.openware.com/sdk/docs/barong/service-accounts.html)
- Focused on user privacy: sensitive informations are encrypted in database using vault, masks are applied on fields in user API endpoints
# Development
Prerequisites:
- Ruby version: `2.6.6`
- Bundler preinstalled
- MySQL preinstalled
1. Install RubyGems dependencies
```
bundle install
```
2. Copy initialisation files
```
bin/init_config
```
3. Create database and run migrations
```
bundle exec rake db:create db:migrate
```
4. Start local server
```
bundle exec rails server
```
# Barong Levels
In the process of verification Barong assign different levels to accounts
- Level 0 is default account level
- Level 1 will apply after email verification
- Level 2 will apply after phone verification
- Level 3 will apply after identity & document verification
# Useful links to documentation
[Barong configuration](https://www.openware.com/sdk/docs/barong/configuration.html)
[Troubleshooting](https://www.openware.com/sdk/docs/barong/troubleshooting.html)
[REST API documentation](https://www.openware.com/sdk/docs/barong/rest-api.html)
[API Keys creation and usage](https://www.openware.com/sdk/docs/barong/general/api-keys.html)
[Captcha policy overview and configuration](https://www.openware.com/sdk/docs/barong/general/captcha.html)
[Setting up 2FA](https://www.openware.com/sdk/docs/barong/2fa.html)
[Barong password hashing](https://www.openware.com/sdk/docs/barong/general/password-hashing.html)
[Barong data encryption](https://www.openware.com/sdk/docs/barong/general/encryption.html)

7
Rakefile Normal file
View File

@@ -0,0 +1,7 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require_relative 'config/application'
require 'bump/tasks'
Rails.application.load_tasks

1
VERSION Normal file
View File

@@ -0,0 +1 @@
2.6.0

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

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