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