commit de1d57a67b9cf946643ec671e751315097aa7ba9 Author: Yaser Date: Thu Aug 13 19:56:46 2026 +0330 Initial commit diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000..970b114 --- /dev/null +++ b/.codeclimate.yml @@ -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/ \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03fa602 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +Dockerfile +.dockerignore +.git +.gitignore +LICENSE.md +Makefile +README.md +.travis.yml +VERSION +config/charts diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..d14764a --- /dev/null +++ b/.drone.yml @@ -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" diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e719089 --- /dev/null +++ b/.gitignore @@ -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/ \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..c99d2e7 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..f6ab44e --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +ruby-2.6.6 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..36cb21a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,2056 @@ +# Change Log + +## [2.3.39](https://github.com/rubykube/barong/tree/2.3.39) (2019-10-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.38...2.3.39) + +**Merged pull requests:** + +- Update storage-related gems [\#957](https://github.com/rubykube/barong/pull/957) ([ec](https://github.com/ec)) +- Protect superadmin against changes from non-superadmin users [\#955](https://github.com/rubykube/barong/pull/955) ([ec](https://github.com/ec)) + +## [2.3.38](https://github.com/rubykube/barong/tree/2.3.38) (2019-10-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.37...2.3.38) + +## [2.3.37](https://github.com/rubykube/barong/tree/2.3.37) (2019-10-11) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.36...2.3.37) + +**Merged pull requests:** + +- Server side sessions implementation \(cache store\) [\#949](https://github.com/rubykube/barong/pull/949) ([ec](https://github.com/ec)) + +## [2.3.36](https://github.com/rubykube/barong/tree/2.3.36) (2019-10-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.35...2.3.36) + +**Merged pull requests:** + +- Feature: allow \# \ \(\) & ' : " in profile residental address field [\#950](https://github.com/rubykube/barong/pull/950) ([ymasiuk](https://github.com/ymasiuk)) + +## [2.3.35](https://github.com/rubykube/barong/tree/2.3.35) (2019-10-07) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.34...2.3.35) + +**Merged pull requests:** + +- Add ability to configure aws\_signature\_version && endpoint [\#947](https://github.com/rubykube/barong/pull/947) ([ec](https://github.com/ec)) + +## [2.3.34](https://github.com/rubykube/barong/tree/2.3.34) (2019-10-02) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.33...2.3.34) + +**Merged pull requests:** + +- Update ruby to 2.6.5 for security reasons [\#946](https://github.com/rubykube/barong/pull/946) ([ec](https://github.com/ec)) +- Profile act as eventable on update [\#945](https://github.com/rubykube/barong/pull/945) ([ec](https://github.com/ec)) + +## [2.3.33](https://github.com/rubykube/barong/tree/2.3.33) (2019-09-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.32...2.3.33) + +**Merged pull requests:** + +- Use uid instead of id on profile update && delete api [\#943](https://github.com/rubykube/barong/pull/943) ([ec](https://github.com/ec)) + +## [2.3.32](https://github.com/rubykube/barong/tree/2.3.32) (2019-09-23) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.31...2.3.32) + +**Merged pull requests:** + +- Use CGI::escape instead of pure url in documents event api [\#942](https://github.com/rubykube/barong/pull/942) ([ec](https://github.com/ec)) + +## [2.3.31](https://github.com/rubykube/barong/tree/2.3.31) (2019-09-23) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.30...2.3.31) + +**Closed issues:** + +- Missing model.user.updated event description [\#931](https://github.com/rubykube/barong/issues/931) +- Expose levels configuration [\#929](https://github.com/rubykube/barong/issues/929) +- Update Label\#create and Label\#update in admin and management API [\#896](https://github.com/rubykube/barong/issues/896) + +**Merged pull requests:** + +- Introduce MockPhoneVerifyService [\#941](https://github.com/rubykube/barong/pull/941) ([ec](https://github.com/ec)) + +## [2.3.30](https://github.com/rubykube/barong/tree/2.3.30) (2019-09-20) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.29...2.3.30) + +**Merged pull requests:** + +- Change twilio API [\#928](https://github.com/rubykube/barong/pull/928) ([chumaknadya](https://github.com/chumaknadya)) + +## [2.3.29](https://github.com/rubykube/barong/tree/2.3.29) (2019-09-20) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.28...2.3.29) + +**Merged pull requests:** + +- Add missing labels event api documentation [\#937](https://github.com/rubykube/barong/pull/937) ([ec](https://github.com/ec)) + +## [2.3.28](https://github.com/rubykube/barong/tree/2.3.28) (2019-09-18) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.27...2.3.28) + +**Merged pull requests:** + +- Make labels acts\_as\_eventable on: \[create update\] [\#936](https://github.com/rubykube/barong/pull/936) ([ec](https://github.com/ec)) + +## [2.3.27](https://github.com/rubykube/barong/tree/2.3.27) (2019-09-18) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.26...2.3.27) + +**Merged pull requests:** + +- Define DocumentTypes class for flexible doc type configuration [\#935](https://github.com/rubykube/barong/pull/935) ([ec](https://github.com/ec)) + +## [2.3.26](https://github.com/rubykube/barong/tree/2.3.26) (2019-09-17) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.25...2.3.26) + +**Merged pull requests:** + +- Update seeds.yml [\#934](https://github.com/rubykube/barong/pull/934) ([ec](https://github.com/ec)) +- Event API changes: remove blocking check for on\_update events [\#933](https://github.com/rubykube/barong/pull/933) ([ec](https://github.com/ec)) +- Simple GET levels requirements endpoint for admin [\#932](https://github.com/rubykube/barong/pull/932) ([ec](https://github.com/ec)) +- Get rid of serialize: JSON, accept metadata as json instead of pure hash [\#930](https://github.com/rubykube/barong/pull/930) ([ec](https://github.com/ec)) + +## [2.3.25](https://github.com/rubykube/barong/tree/2.3.25) (2019-09-17) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.24...2.3.25) + +**Closed issues:** + +- Missing enable of OTP in rake task [\#890](https://github.com/rubykube/barong/issues/890) +- Missing storage\_region env config for carrierwave [\#880](https://github.com/rubykube/barong/issues/880) +- Missing check on vault connection while creating api\_key [\#879](https://github.com/rubykube/barong/issues/879) + +**Merged pull requests:** + +- Add system.session.create event with user and request\_ip data [\#916](https://github.com/rubykube/barong/pull/916) ([chumaknadya](https://github.com/chumaknadya)) + +## [2.3.24](https://github.com/rubykube/barong/tree/2.3.24) (2019-09-12) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.23...2.3.24) + +**Merged pull requests:** + +- Allow blank doc\_expire for documents controllers [\#927](https://github.com/rubykube/barong/pull/927) ([dnfd](https://github.com/dnfd)) +- Add AliCloud Uploader [\#926](https://github.com/rubykube/barong/pull/926) ([dnfd](https://github.com/dnfd)) + +## [2.3.23](https://github.com/rubykube/barong/tree/2.3.23) (2019-09-07) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.21...2.3.23) + +**Merged pull requests:** + +- Add ability to disable 2fa [\#925](https://github.com/rubykube/barong/pull/925) ([dnfd](https://github.com/dnfd)) +- Use Barong::App.config.barong\_uid\_prefix in referral validation [\#923](https://github.com/rubykube/barong/pull/923) ([ec](https://github.com/ec)) +- Add ability to skip label creation on doc save [\#922](https://github.com/rubykube/barong/pull/922) ([dnfd](https://github.com/dnfd)) +- Avoid no method error UTC for nil entities [\#921](https://github.com/rubykube/barong/pull/921) ([ec](https://github.com/ec)) + +## [2.3.21](https://github.com/rubykube/barong/tree/2.3.21) (2019-09-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.20...2.3.21) + +**Merged pull requests:** + +- Add ability to restrict by geoip [\#920](https://github.com/rubykube/barong/pull/920) ([dnfd](https://github.com/dnfd)) + +## [2.3.20](https://github.com/rubykube/barong/tree/2.3.20) (2019-09-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.19...2.3.20) + +**Merged pull requests:** + +- Handle Vault errors on POST /api/v2/resource/api\_keys [\#918](https://github.com/rubykube/barong/pull/918) ([dnfd](https://github.com/dnfd)) + +## [2.3.19](https://github.com/rubykube/barong/tree/2.3.19) (2019-09-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.18...2.3.19) + +**Merged pull requests:** + +- Fix structure of event API messages [\#912](https://github.com/rubykube/barong/pull/912) ([shal](https://github.com/shal)) + +## [2.3.18](https://github.com/rubykube/barong/tree/2.3.18) (2019-09-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.17...2.3.18) + +**Merged pull requests:** + +- Add :upload in list of params to be filtered [\#919](https://github.com/rubykube/barong/pull/919) ([ec](https://github.com/ec)) + +## [2.3.17](https://github.com/rubykube/barong/tree/2.3.17) (2019-09-04) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.16...2.3.17) + +**Merged pull requests:** + +- UPDATES: sessions delete, permissions update, log error on 500 [\#917](https://github.com/rubykube/barong/pull/917) ([chumaknadya](https://github.com/chumaknadya)) + +## [2.3.16](https://github.com/rubykube/barong/tree/2.3.16) (2019-09-04) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.15...2.3.16) + +**Merged pull requests:** + +- Support 'if not exist - create' logic on labels update [\#915](https://github.com/rubykube/barong/pull/915) ([ec](https://github.com/ec)) +- Fix rollback to pending state, add missing :data in entities [\#914](https://github.com/rubykube/barong/pull/914) ([ec](https://github.com/ec)) +- Add AliCloud Carrierwave storage provider [\#911](https://github.com/rubykube/barong/pull/911) ([dnfd](https://github.com/dnfd)) + +## [2.3.15](https://github.com/rubykube/barong/tree/2.3.15) (2019-09-03) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.14...2.3.15) + +**Merged pull requests:** + +- Switch to State - Label dependency [\#907](https://github.com/rubykube/barong/pull/907) ([ec](https://github.com/ec)) + +## [2.3.14](https://github.com/rubykube/barong/tree/2.3.14) (2019-09-03) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.13...2.3.14) + +**Merged pull requests:** + +- Ability to update and delete users profiles [\#913](https://github.com/rubykube/barong/pull/913) ([ec](https://github.com/ec)) + +## [2.3.13](https://github.com/rubykube/barong/tree/2.3.13) (2019-09-02) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.12...2.3.13) + +**Merged pull requests:** + +- Ability create Profile with empty fields. Add state to Profile model. [\#910](https://github.com/rubykube/barong/pull/910) ([mnaichuk](https://github.com/mnaichuk)) + +## [2.3.12](https://github.com/rubykube/barong/tree/2.3.12) (2019-09-02) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.11...2.3.12) + +**Merged pull requests:** + +- Update Barong::App validations [\#904](https://github.com/rubykube/barong/pull/904) ([dnfd](https://github.com/dnfd)) + +## [2.3.11](https://github.com/rubykube/barong/tree/2.3.11) (2019-08-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.10...2.3.11) + +**Merged pull requests:** + +- Update nokogiri version \(Command injection\) [\#906](https://github.com/rubykube/barong/pull/906) ([dnfd](https://github.com/dnfd)) + +## [2.3.10](https://github.com/rubykube/barong/tree/2.3.10) (2019-08-22) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.9...2.3.10) + +**Closed issues:** + +- How to configure session expire time? [\#900](https://github.com/rubykube/barong/issues/900) + +**Merged pull requests:** + +- Add session autorenew [\#903](https://github.com/rubykube/barong/pull/903) ([dnfd](https://github.com/dnfd)) +- Add endpoint for delete restriction [\#902](https://github.com/rubykube/barong/pull/902) ([chumaknadya](https://github.com/chumaknadya)) + +## [2.3.9](https://github.com/rubykube/barong/tree/2.3.9) (2019-08-19) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.8...2.3.9) + +**Merged pull requests:** + +- Small fixes in document and profile model \(\#887\) [\#901](https://github.com/rubykube/barong/pull/901) ([ec](https://github.com/ec)) + +## [2.3.8](https://github.com/rubykube/barong/tree/2.3.8) (2019-08-19) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.7...2.3.8) + +**Merged pull requests:** + +- Update models' annotations [\#895](https://github.com/rubykube/barong/pull/895) ([dnfd](https://github.com/dnfd)) + +## [2.3.7](https://github.com/rubykube/barong/tree/2.3.7) (2019-08-19) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.6...2.3.7) + +**Merged pull requests:** + +- Updating to ruby 2.6.3 and rails 5.2.3 [\#867](https://github.com/rubykube/barong/pull/867) ([mod](https://github.com/mod)) + +## [2.3.6](https://github.com/rubykube/barong/tree/2.3.6) (2019-08-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.5...2.3.6) + +**Merged pull requests:** + +- Management API to push documents for user [\#894](https://github.com/rubykube/barong/pull/894) ([ec](https://github.com/ec)) +- Add ability to configure uid prefix [\#893](https://github.com/rubykube/barong/pull/893) ([ec](https://github.com/ec)) + +## [2.3.5](https://github.com/rubykube/barong/tree/2.3.5) (2019-08-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.4...2.3.5) + +**Merged pull requests:** + +- Add Restrictions [\#891](https://github.com/rubykube/barong/pull/891) ([dnfd](https://github.com/dnfd)) + +## [2.3.4](https://github.com/rubykube/barong/tree/2.3.4) (2019-08-09) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.3...2.3.4) + +**Closed issues:** + +- Permissions are not checked [\#889](https://github.com/rubykube/barong/issues/889) +- event\_api\_jwt\_private\_key Error [\#888](https://github.com/rubykube/barong/issues/888) + +**Merged pull requests:** + +- Simple rake task to create users and api keys [\#816](https://github.com/rubykube/barong/pull/816) ([ec](https://github.com/ec)) + +## [2.3.3](https://github.com/rubykube/barong/tree/2.3.3) (2019-08-02) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.25...2.3.3) + +## [2.2.25](https://github.com/rubykube/barong/tree/2.2.25) (2019-08-02) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.24...2.2.25) + +**Merged pull requests:** + +- Allow additional symbols ' / , : in residential address && Switch to string in required\_docs\_expire config [\#887](https://github.com/rubykube/barong/pull/887) ([ec](https://github.com/ec)) +- Add rake task for load users and referrals [\#885](https://github.com/rubykube/barong/pull/885) ([mnaichuk](https://github.com/mnaichuk)) + +## [2.2.24](https://github.com/rubykube/barong/tree/2.2.24) (2019-07-31) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.23...2.2.24) + +**Merged pull requests:** + +- Extend doc\_type inclusion list, make validation case insensitive [\#884](https://github.com/rubykube/barong/pull/884) ([ec](https://github.com/ec)) + +## [2.2.23](https://github.com/rubykube/barong/tree/2.2.23) (2019-07-31) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.2...2.2.23) + +**Closed issues:** + +- Make phone code message configurable [\#876](https://github.com/rubykube/barong/issues/876) +- Add verb PATCH to available verbs for admin audit [\#868](https://github.com/rubykube/barong/issues/868) + +**Merged pull requests:** + +- Add ability to get referral uid && Expose referral\_uid in user, user\_with\_profile, user\_with\_full\_info entities [\#882](https://github.com/rubykube/barong/pull/882) ([chumaknadya](https://github.com/chumaknadya)) + +## [2.3.2](https://github.com/rubykube/barong/tree/2.3.2) (2019-07-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.3.1...2.3.2) + +**Merged pull requests:** + +- Allow pending user to open session [\#881](https://github.com/rubykube/barong/pull/881) ([ec](https://github.com/ec)) +- Add ability to configure phone verification sms content [\#877](https://github.com/rubykube/barong/pull/877) ([ec](https://github.com/ec)) + +## [2.3.1](https://github.com/rubykube/barong/tree/2.3.1) (2019-07-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.22...2.3.1) + +**Closed issues:** + +- An error occurred while installing mysql2 \(0.5.2\), and Bundler cannot continue. Make sure that `gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'` succeeds before bundling. [\#873](https://github.com/rubykube/barong/issues/873) + +**Merged pull requests:** + +- Feature: Add redeploy on devkube step to drone [\#874](https://github.com/rubykube/barong/pull/874) ([alinetskyi](https://github.com/alinetskyi)) +- \[ci skip\] Release 2.2.0 [\#836](https://github.com/rubykube/barong/pull/836) ([ec](https://github.com/ec)) + +## [2.2.22](https://github.com/rubykube/barong/tree/2.2.22) (2019-07-17) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.21...2.2.22) + +**Merged pull requests:** + +- Add optional :extended UserWithFullInfo entity in post /get management [\#872](https://github.com/rubykube/barong/pull/872) ([ec](https://github.com/ec)) +- Make documents act\_as\_eventable on create [\#871](https://github.com/rubykube/barong/pull/871) ([ec](https://github.com/ec)) + +## [2.2.21](https://github.com/rubykube/barong/tree/2.2.21) (2019-07-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.20...2.2.21) + +**Closed issues:** + +- \[To Confirm\] Strange admin activity creation behavior on UID filtering requests [\#869](https://github.com/rubykube/barong/issues/869) +- ActionController::InvalidAuthenticityToken in SessionsController\#confirm [\#866](https://github.com/rubykube/barong/issues/866) + +**Merged pull requests:** + +- Add verb PATCH and ALL for admin audit && Update permission seeds [\#870](https://github.com/rubykube/barong/pull/870) ([chumaknadya](https://github.com/chumaknadya)) +- Make profile act\_as\_eventable on create [\#865](https://github.com/rubykube/barong/pull/865) ([ec](https://github.com/ec)) +- Add filter/users in labels management api module [\#864](https://github.com/rubykube/barong/pull/864) ([ec](https://github.com/ec)) +- Update Readme [\#862](https://github.com/rubykube/barong/pull/862) ([liutenko](https://github.com/liutenko)) +- Support pagination & timestamp filtering on post '/list' management API [\#861](https://github.com/rubykube/barong/pull/861) ([ec](https://github.com/ec)) + +## [2.2.20](https://github.com/rubykube/barong/tree/2.2.20) (2019-06-27) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.19...2.2.20) + +**Merged pull requests:** + +- Move to active storage [\#858](https://github.com/rubykube/barong/pull/858) ([ec](https://github.com/ec)) +- Fix /api/v2/admin/users/update [\#857](https://github.com/rubykube/barong/pull/857) ([dnfd](https://github.com/dnfd)) + +## [2.2.19](https://github.com/rubykube/barong/tree/2.2.19) (2019-06-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.18...2.2.19) + +**Closed issues:** + +- add public api version check [\#848](https://github.com/rubykube/barong/issues/848) +- Split endpoints `admin/users` [\#847](https://github.com/rubykube/barong/issues/847) +- Admin activities duplicated in user activities for admin users [\#845](https://github.com/rubykube/barong/issues/845) + +**Merged pull requests:** + +- Add docs in bump and update redeploy step [\#856](https://github.com/rubykube/barong/pull/856) ([dnfd](https://github.com/dnfd)) +- skip tag [\#855](https://github.com/rubykube/barong/pull/855) ([dnfd](https://github.com/dnfd)) +- Generate docs in CI [\#854](https://github.com/rubykube/barong/pull/854) ([dnfd](https://github.com/dnfd)) +- Use JSON logs in production [\#849](https://github.com/rubykube/barong/pull/849) ([dnfd](https://github.com/dnfd)) + +## [2.2.18](https://github.com/rubykube/barong/tree/2.2.18) (2019-06-24) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.17...2.2.18) + +## [2.2.17](https://github.com/rubykube/barong/tree/2.2.17) (2019-06-24) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.16...2.2.17) + +## [2.2.16](https://github.com/rubykube/barong/tree/2.2.16) (2019-06-24) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.15...2.2.16) + +**Merged pull requests:** + +- Add missing data field in activity logger [\#853](https://github.com/rubykube/barong/pull/853) ([ec](https://github.com/ec)) +- Changes in admin API module [\#850](https://github.com/rubykube/barong/pull/850) ([ec](https://github.com/ec)) + +## [2.2.15](https://github.com/rubykube/barong/tree/2.2.15) (2019-06-23) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.14...2.2.15) + +**Merged pull requests:** + +- Split and refactor auth spec, add sync auditor for test env [\#852](https://github.com/rubykube/barong/pull/852) ([ec](https://github.com/ec)) +- Add GET /identity/version endpoint [\#851](https://github.com/rubykube/barong/pull/851) ([ec](https://github.com/ec)) + +## [2.2.14](https://github.com/rubykube/barong/tree/2.2.14) (2019-06-14) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.13...2.2.14) + +**Merged pull requests:** + +- AUDIT: support PATCH, add default topic, action, seeds [\#844](https://github.com/rubykube/barong/pull/844) ([ec](https://github.com/ec)) + +## [2.2.13](https://github.com/rubykube/barong/tree/2.2.13) (2019-06-13) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.12...2.2.13) + +**Merged pull requests:** + +- Admin Auditor [\#835](https://github.com/rubykube/barong/pull/835) ([ec](https://github.com/ec)) + +## [2.2.12](https://github.com/rubykube/barong/tree/2.2.12) (2019-06-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.11...2.2.12) + +## [2.2.11](https://github.com/rubykube/barong/tree/2.2.11) (2019-06-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.10...2.2.11) + +**Closed issues:** + +- BUG: Barong and Postmaster do not communicate well [\#818](https://github.com/rubykube/barong/issues/818) +- Feature Request: Configurable roles and permissions [\#813](https://github.com/rubykube/barong/issues/813) +- Expose Referral ID in API [\#802](https://github.com/rubykube/barong/issues/802) +- Implement api keys scope logic [\#779](https://github.com/rubykube/barong/issues/779) +- API Keys seed feature [\#778](https://github.com/rubykube/barong/issues/778) +- Add readable logs in AuthZ lib [\#777](https://github.com/rubykube/barong/issues/777) +- Admin feature: delete nil / unverified records from DB [\#758](https://github.com/rubykube/barong/issues/758) +- Assymetric api keys support [\#732](https://github.com/rubykube/barong/issues/732) + +**Merged pull requests:** + +- Disable APIKeys when user state or otp changes [\#829](https://github.com/rubykube/barong/pull/829) ([dnfd](https://github.com/dnfd)) + +## [2.2.10](https://github.com/rubykube/barong/tree/2.2.10) (2019-06-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.9...2.2.10) + +**Merged pull requests:** + +- Use FullInfo instead of WithProfile to add documents array in response [\#842](https://github.com/rubykube/barong/pull/842) ([ec](https://github.com/ec)) +- Remove activities from full\_info entity [\#841](https://github.com/rubykube/barong/pull/841) ([ec](https://github.com/ec)) +- Fix nil language problem in case of empty string and upcased param [\#840](https://github.com/rubykube/barong/pull/840) ([ec](https://github.com/ec)) +- Return empty array instead of 404 error after filtering [\#839](https://github.com/rubykube/barong/pull/839) ([ec](https://github.com/ec)) + +## [2.2.9](https://github.com/rubykube/barong/tree/2.2.9) (2019-06-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.8...2.2.9) + +## [2.2.8](https://github.com/rubykube/barong/tree/2.2.8) (2019-06-04) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.7...2.2.8) + +**Merged pull requests:** + +- Add filters feature support on doc pending endpoint [\#837](https://github.com/rubykube/barong/pull/837) ([ec](https://github.com/ec)) + +## [2.2.7](https://github.com/rubykube/barong/tree/2.2.7) (2019-05-28) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.6...2.2.7) + +**Merged pull requests:** + +- Add ability to filter by multiple fields [\#833](https://github.com/rubykube/barong/pull/833) ([gfedorenko](https://github.com/gfedorenko)) + +## [2.2.6](https://github.com/rubykube/barong/tree/2.2.6) (2019-05-28) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.5...2.2.6) + +## [2.2.5](https://github.com/rubykube/barong/tree/2.2.5) (2019-05-28) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.4...2.2.5) + +**Merged pull requests:** + +- Add ability to get list of users with profile [\#830](https://github.com/rubykube/barong/pull/830) ([gfedorenko](https://github.com/gfedorenko)) + +## [2.2.4](https://github.com/rubykube/barong/tree/2.2.4) (2019-05-28) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.3...2.2.4) + +## [2.2.3](https://github.com/rubykube/barong/tree/2.2.3) (2019-05-27) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.2...2.2.3) + +**Closed issues:** + +- Barong api session create don't return tokens [\#832](https://github.com/rubykube/barong/issues/832) +- Admin api list users with pending documents [\#799](https://github.com/rubykube/barong/issues/799) +- Automate documentation updating \(v2\) [\#784](https://github.com/rubykube/barong/issues/784) +- Move all envs to config.store [\#736](https://github.com/rubykube/barong/issues/736) +- \[Barong v2; Feature request\] handle document expired date. [\#666](https://github.com/rubykube/barong/issues/666) + +**Merged pull requests:** + +- Update seeds.yml [\#838](https://github.com/rubykube/barong/pull/838) ([pkucherenk0](https://github.com/pkucherenk0)) +- Add metrics namespace in admin module [\#834](https://github.com/rubykube/barong/pull/834) ([ec](https://github.com/ec)) +- Add endpoint to list all user's phones [\#831](https://github.com/rubykube/barong/pull/831) ([dnfd](https://github.com/dnfd)) +- Added admin endpoints to get activities [\#828](https://github.com/rubykube/barong/pull/828) ([gfedorenko](https://github.com/gfedorenko)) +- Add management otp sign endpoint and specs [\#827](https://github.com/rubykube/barong/pull/827) ([ec](https://github.com/ec)) +- Avoid nil language bug on user creation event [\#820](https://github.com/rubykube/barong/pull/820) ([ec](https://github.com/ec)) +- Permissions & roles, seed feature and authz logic [\#819](https://github.com/rubykube/barong/pull/819) ([ec](https://github.com/ec)) + +## [2.2.2](https://github.com/rubykube/barong/tree/2.2.2) (2019-05-13) +[Full Changelog](https://github.com/rubykube/barong/compare/2.2.1...2.2.2) + +**Merged pull requests:** + +- Added API call to get users with pending documents [\#826](https://github.com/rubykube/barong/pull/826) ([gfedorenko](https://github.com/gfedorenko)) + +## [2.2.1](https://github.com/rubykube/barong/tree/2.2.1) (2019-05-09) +[Full Changelog](https://github.com/rubykube/barong/compare/2.1.4...2.2.1) + +**Closed issues:** + +- Internal Error when clicking on email confirmation [\#821](https://github.com/rubykube/barong/issues/821) +- How to generate OTP using barong? [\#817](https://github.com/rubykube/barong/issues/817) +- Allow user to close his account [\#812](https://github.com/rubykube/barong/issues/812) + +**Merged pull requests:** + +- Add GET /labels/list to return key, value, scope of all existing labels [\#824](https://github.com/rubykube/barong/pull/824) ([ec](https://github.com/ec)) +- Make :doc\_expire an optional field [\#823](https://github.com/rubykube/barong/pull/823) ([ec](https://github.com/ec)) +- \[ci skip\] Release 2.1.0 [\#822](https://github.com/rubykube/barong/pull/822) ([ec](https://github.com/ec)) +- Add DELETE /resource/users/me to allow user to block his account [\#811](https://github.com/rubykube/barong/pull/811) ([ec](https://github.com/ec)) + +## [2.1.4](https://github.com/rubykube/barong/tree/2.1.4) (2019-04-09) +[Full Changelog](https://github.com/rubykube/barong/compare/2.1.3...2.1.4) + +**Merged pull requests:** + +- Add opportunity to get list of users by management API [\#815](https://github.com/rubykube/barong/pull/815) ([ymasiuk](https://github.com/ymasiuk)) +- Move event api jwt private key to Barong::App.config store [\#805](https://github.com/rubykube/barong/pull/805) ([ec](https://github.com/ec)) +- Add auto-generating doc support [\#795](https://github.com/rubykube/barong/pull/795) ([ec](https://github.com/ec)) +- Allow to configure multiple CORS at barong [\#790](https://github.com/rubykube/barong/pull/790) ([ec](https://github.com/ec)) + +## [2.1.3](https://github.com/rubykube/barong/tree/2.1.3) (2019-03-19) +[Full Changelog](https://github.com/rubykube/barong/compare/2.1.2...2.1.3) + +**Closed issues:** + +- Update Gemfile.lock to fix github vulnerabilities alert [\#808](https://github.com/rubykube/barong/issues/808) +- Private key is invalid [\#798](https://github.com/rubykube/barong/issues/798) +- Recaptcha not working [\#797](https://github.com/rubykube/barong/issues/797) +- Ask [\#796](https://github.com/rubykube/barong/issues/796) +- limit rows on activity not work [\#793](https://github.com/rubykube/barong/issues/793) +- add domain info inside events [\#792](https://github.com/rubykube/barong/issues/792) +- How to use file Local file storage in production env? [\#764](https://github.com/rubykube/barong/issues/764) +- Routing error [\#752](https://github.com/rubykube/barong/issues/752) +- Fix documentation [\#748](https://github.com/rubykube/barong/issues/748) +- Drop confirm\_password check on change password endpoing [\#744](https://github.com/rubykube/barong/issues/744) +- Configuration mail [\#729](https://github.com/rubykube/barong/issues/729) +- Rework errors to be strict to one standard [\#728](https://github.com/rubykube/barong/issues/728) +- Generate changelog [\#715](https://github.com/rubykube/barong/issues/715) +- Ability to specify multiple cors in 1.8 [\#708](https://github.com/rubykube/barong/issues/708) +- \[Feature Request\] Add the ability to search user by email on barong admin page [\#635](https://github.com/rubykube/barong/issues/635) + +**Merged pull requests:** + +- Update rails-related gems versions in favor of vulnerability alerts [\#809](https://github.com/rubykube/barong/pull/809) ([ec](https://github.com/ec)) +- Allow to list API Keys without providing OTP code [\#807](https://github.com/rubykube/barong/pull/807) ([ec](https://github.com/ec)) +- API user activity sort desc [\#804](https://github.com/rubykube/barong/pull/804) ([mkalenska](https://github.com/mkalenska)) +- Add ability to search users by labels [\#800](https://github.com/rubykube/barong/pull/800) ([mkalenska](https://github.com/mkalenska)) +- Improve language support in event API, add domain info inside event [\#791](https://github.com/rubykube/barong/pull/791) ([ec](https://github.com/ec)) + +## [2.1.2](https://github.com/rubykube/barong/tree/2.1.2) (2019-03-01) +[Full Changelog](https://github.com/rubykube/barong/compare/2.1.1...2.1.2) + +**Merged pull requests:** + +- Use 'paginate' on api/v2/admin/users/search [\#789](https://github.com/rubykube/barong/pull/789) ([ec](https://github.com/ec)) + +## [2.1.1](https://github.com/rubykube/barong/tree/2.1.1) (2019-03-01) +[Full Changelog](https://github.com/rubykube/barong/compare/2.1.0...2.1.1) + +**Closed issues:** + +- withCredentials error [\#788](https://github.com/rubykube/barong/issues/788) +- Include Activity info in response on endpoint: GET admin/users/:uid [\#785](https://github.com/rubykube/barong/issues/785) + +**Merged pull requests:** + +- Add Entities::Activity model, include it Entities::UserWithFullInfo [\#786](https://github.com/rubykube/barong/pull/786) ([ec](https://github.com/ec)) + +## [2.1.0](https://github.com/rubykube/barong/tree/2.1.0) (2019-02-27) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.51...2.1.0) + +**Merged pull requests:** + +- Start 2.1 development\[ci skip\] [\#787](https://github.com/rubykube/barong/pull/787) ([ec](https://github.com/ec)) +- Update REST API documentation to v2.0.51\[ci skip\] [\#783](https://github.com/rubykube/barong/pull/783) ([ec](https://github.com/ec)) + +## [2.0.51](https://github.com/rubykube/barong/tree/2.0.51) (2019-02-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.50...2.0.51) + +**Merged pull requests:** + +- Feature/validation errors [\#774](https://github.com/rubykube/barong/pull/774) ([ec](https://github.com/ec)) + +## [2.0.50](https://github.com/rubykube/barong/tree/2.0.50) (2019-02-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.49-alpha...2.0.50) + +**Merged pull requests:** + +- Remove "-alpha" from version name [\#782](https://github.com/rubykube/barong/pull/782) ([ec](https://github.com/ec)) +- Account api improvements \(management module\) [\#773](https://github.com/rubykube/barong/pull/773) ([ec](https://github.com/ec)) + +## [2.0.49-alpha](https://github.com/rubykube/barong/tree/2.0.49-alpha) (2019-02-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.48-alpha...2.0.49-alpha) + +**Merged pull requests:** + +- Search implementation \(admin api\) [\#769](https://github.com/rubykube/barong/pull/769) ([ec](https://github.com/ec)) + +## [2.0.48-alpha](https://github.com/rubykube/barong/tree/2.0.48-alpha) (2019-02-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.47-alpha...2.0.48-alpha) + +**Merged pull requests:** + +- Configure api-pagination tool, add api keys pagination [\#780](https://github.com/rubykube/barong/pull/780) ([ec](https://github.com/ec)) + +## [2.0.47-alpha](https://github.com/rubykube/barong/tree/2.0.47-alpha) (2019-02-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.46-alpha...2.0.47-alpha) + +**Closed issues:** + +- \[TO CHECK\] Level is not recalculated on labels delete [\#775](https://github.com/rubykube/barong/issues/775) + +**Merged pull requests:** + +- Use destroy instead of delete to enable missing validations [\#776](https://github.com/rubykube/barong/pull/776) ([ec](https://github.com/ec)) + +## [2.0.46-alpha](https://github.com/rubykube/barong/tree/2.0.46-alpha) (2019-02-20) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.45-alpha...2.0.46-alpha) + +**Closed issues:** + +- Split activity error [\#743](https://github.com/rubykube/barong/issues/743) + +**Merged pull requests:** + +- Split activity no records error to wrong\_topic and no\_activity [\#772](https://github.com/rubykube/barong/pull/772) ([ec](https://github.com/ec)) + +## [2.0.45-alpha](https://github.com/rubykube/barong/tree/2.0.45-alpha) (2019-02-20) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.44-alpha...2.0.45-alpha) + +**Merged pull requests:** + +- Add pagination on /resource/users/activity endpoint [\#771](https://github.com/rubykube/barong/pull/771) ([ec](https://github.com/ec)) + +## [2.0.44-alpha](https://github.com/rubykube/barong/tree/2.0.44-alpha) (2019-02-18) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.43-alpha...2.0.44-alpha) + +**Closed issues:** + +- authz unexpected behaivor with incorrect api\_key [\#770](https://github.com/rubykube/barong/issues/770) +- Api keys error [\#761](https://github.com/rubykube/barong/issues/761) + +**Merged pull requests:** + +- Add more API Key validations, fix error on creating with invalid algo [\#768](https://github.com/rubykube/barong/pull/768) ([ec](https://github.com/ec)) + +## [2.0.43-alpha](https://github.com/rubykube/barong/tree/2.0.43-alpha) (2019-02-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.42-alpha...2.0.43-alpha) + +**Closed issues:** + +- Allow user get on a base of additional fields \(not only uid\) [\#762](https://github.com/rubykube/barong/issues/762) + +**Merged pull requests:** + +- User controller improvements \(management module\) [\#763](https://github.com/rubykube/barong/pull/763) ([ec](https://github.com/ec)) + +## [2.0.42-alpha](https://github.com/rubykube/barong/tree/2.0.42-alpha) (2019-02-15) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.41-alpha...2.0.42-alpha) + +**Merged pull requests:** + +- Add language field in reset pass and confirm acc events [\#767](https://github.com/rubykube/barong/pull/767) ([ec](https://github.com/ec)) + +## [2.0.41-alpha](https://github.com/rubykube/barong/tree/2.0.41-alpha) (2019-02-07) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.40-alpha...2.0.41-alpha) + +**Closed issues:** + +- Return specific error for banned users [\#754](https://github.com/rubykube/barong/issues/754) + +**Merged pull requests:** + +- Add additional error on login in case of banned user [\#760](https://github.com/rubykube/barong/pull/760) ([ec](https://github.com/ec)) + +## [2.0.40-alpha](https://github.com/rubykube/barong/tree/2.0.40-alpha) (2019-02-07) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.39-alpha...2.0.40-alpha) + +**Closed issues:** + +- Phone api improvement [\#746](https://github.com/rubykube/barong/issues/746) +- Event api doc v2 [\#735](https://github.com/rubykube/barong/issues/735) + +**Merged pull requests:** + +- Phone API improvements [\#747](https://github.com/rubykube/barong/pull/747) ([ec](https://github.com/ec)) +- Added event API documentation with examples [\#745](https://github.com/rubykube/barong/pull/745) ([ec](https://github.com/ec)) +- API errors unifying [\#741](https://github.com/rubykube/barong/pull/741) ([ec](https://github.com/ec)) + +## [2.0.39-alpha](https://github.com/rubykube/barong/tree/2.0.39-alpha) (2019-02-06) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.38-alpha...2.0.39-alpha) + +**Closed issues:** + +- Vault secret is an object and not assignable to string. [\#756](https://github.com/rubykube/barong/issues/756) +- Remove unnecessary call merge func [\#755](https://github.com/rubykube/barong/issues/755) + +**Merged pull requests:** + +- Take only data from Vault::Secret object && minor refactoring [\#757](https://github.com/rubykube/barong/pull/757) ([ec](https://github.com/ec)) + +## [2.0.38-alpha](https://github.com/rubykube/barong/tree/2.0.38-alpha) (2019-02-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.37-alpha...2.0.38-alpha) + +**Merged pull requests:** + +- Change default set-cookie header policy on authz to :skip [\#753](https://github.com/rubykube/barong/pull/753) ([ec](https://github.com/ec)) + +## [2.0.37-alpha](https://github.com/rubykube/barong/tree/2.0.37-alpha) (2019-01-31) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.36-alpha...2.0.37-alpha) + +**Closed issues:** + +- Session\_id doesnt have any influence on session authorizer [\#751](https://github.com/rubykube/barong/issues/751) +- Session\_id doesnt have any influence on session. [\#750](https://github.com/rubykube/barong/issues/750) +- Missing 'present user, with: API::V2::Entities::User' in response on 2fa login [\#740](https://github.com/rubykube/barong/issues/740) + +**Merged pull requests:** + +- Fix session\_id check & add bypass session lazy load [\#749](https://github.com/rubykube/barong/pull/749) ([ec](https://github.com/ec)) + +## [2.0.36-alpha](https://github.com/rubykube/barong/tree/2.0.36-alpha) (2019-01-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.35-alpha...2.0.36-alpha) + +**Merged pull requests:** + +- Add missing user details in 2fa login response [\#742](https://github.com/rubykube/barong/pull/742) ([ec](https://github.com/ec)) + +## [2.0.35-alpha](https://github.com/rubykube/barong/tree/2.0.35-alpha) (2019-01-22) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.34-alpha...2.0.35-alpha) + +**Closed issues:** + +- CORS problem in production environment [\#738](https://github.com/rubykube/barong/issues/738) + +**Merged pull requests:** + +- Fix Barong::CORS load problem in production env [\#739](https://github.com/rubykube/barong/pull/739) ([ec](https://github.com/ec)) + +## [2.0.34-alpha](https://github.com/rubykube/barong/tree/2.0.34-alpha) (2019-01-21) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.33-alpha...2.0.34-alpha) + +**Merged pull requests:** + +- Configure redis and add JTI blacklisting [\#734](https://github.com/rubykube/barong/pull/734) ([ec](https://github.com/ec)) + +## [2.0.33-alpha](https://github.com/rubykube/barong/tree/2.0.33-alpha) (2019-01-21) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.32-alpha...2.0.33-alpha) + +**Closed issues:** + +- Invalid session cookies returns on session logout [\#733](https://github.com/rubykube/barong/issues/733) +- Password Reset Token doesn't expire [\#727](https://github.com/rubykube/barong/issues/727) +- logging phone verification code may not be secure [\#647](https://github.com/rubykube/barong/issues/647) + +**Merged pull requests:** + +- Fix change code on phone initialize, remove code from logs [\#730](https://github.com/rubykube/barong/pull/730) ([ec](https://github.com/ec)) + +## [2.0.32-alpha](https://github.com/rubykube/barong/tree/2.0.32-alpha) (2019-01-21) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.31-alpha...2.0.32-alpha) + +**Closed issues:** + +- Split swagger docs to management and restfull api [\#712](https://github.com/rubykube/barong/issues/712) + +**Merged pull requests:** + +- Add storage envs in config store and update fetch logic [\#737](https://github.com/rubykube/barong/pull/737) ([ec](https://github.com/ec)) + +## [2.0.31-alpha](https://github.com/rubykube/barong/tree/2.0.31-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.30-alpha...2.0.31-alpha) + +**Closed issues:** + +- Admin api to promote member to admin or decrease [\#719](https://github.com/rubykube/barong/issues/719) +- Admin api to disable users 2fa [\#717](https://github.com/rubykube/barong/issues/717) +- Add Cors middleware to v2 branch [\#711](https://github.com/rubykube/barong/issues/711) +- Make session to be created only in sessions controller [\#710](https://github.com/rubykube/barong/issues/710) + +**Merged pull requests:** + +- Split swagger doc into restful and management [\#713](https://github.com/rubykube/barong/pull/713) ([ec](https://github.com/ec)) + +## [2.0.30-alpha](https://github.com/rubykube/barong/tree/2.0.30-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.29-alpha...2.0.30-alpha) + +## [2.0.29-alpha](https://github.com/rubykube/barong/tree/2.0.29-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.28-alpha...2.0.29-alpha) + +## [2.0.28-alpha](https://github.com/rubykube/barong/tree/2.0.28-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.27-alpha...2.0.28-alpha) + +**Merged pull requests:** + +- Add admin api functionality on user update [\#722](https://github.com/rubykube/barong/pull/722) ([ec](https://github.com/ec)) + +## [2.0.27-alpha](https://github.com/rubykube/barong/tree/2.0.27-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.26-alpha...2.0.27-alpha) + +**Closed issues:** + +- Change password endpoint action should be `put` instead of `post` [\#723](https://github.com/rubykube/barong/issues/723) + +**Merged pull requests:** + +- Fix change password api action from post to put [\#724](https://github.com/rubykube/barong/pull/724) ([m-an](https://github.com/m-an)) + +## [2.0.26-alpha](https://github.com/rubykube/barong/tree/2.0.26-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.25-alpha...2.0.26-alpha) + +**Merged pull requests:** + +- Change regex validations to support different language characters [\#726](https://github.com/rubykube/barong/pull/726) ([ec](https://github.com/ec)) +- Add an ability to specify CORS\(multiple\) [\#716](https://github.com/rubykube/barong/pull/716) ([ec](https://github.com/ec)) + +## [2.0.25-alpha](https://github.com/rubykube/barong/tree/2.0.25-alpha) (2019-01-16) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.24-alpha...2.0.25-alpha) + +**Merged pull requests:** + +- Updating to ruby 2.6.0 [\#707](https://github.com/rubykube/barong/pull/707) ([mod](https://github.com/mod)) +- Add ability to lock account, add filter for state on admin index [\#701](https://github.com/rubykube/barong/pull/701) ([ec](https://github.com/ec)) + +## [2.0.24-alpha](https://github.com/rubykube/barong/tree/2.0.24-alpha) (2019-01-11) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.23-alpha...2.0.24-alpha) + +**Closed issues:** + +- Documents label bug [\#714](https://github.com/rubykube/barong/issues/714) + +**Merged pull requests:** + +- Add expirable session [\#720](https://github.com/rubykube/barong/pull/720) ([m-an](https://github.com/m-an)) +- Move ActionDispatch::Session::CookieStore to sessions controller [\#718](https://github.com/rubykube/barong/pull/718) ([ec](https://github.com/ec)) + +## [2.0.23-alpha](https://github.com/rubykube/barong/tree/2.0.23-alpha) (2019-01-09) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.22-alpha...2.0.23-alpha) + +**Closed issues:** + +- Multi-label levels [\#680](https://github.com/rubykube/barong/issues/680) + +**Merged pull requests:** + +- Add referral\_id to users table and as an optional param on signup [\#709](https://github.com/rubykube/barong/pull/709) ([ec](https://github.com/ec)) + +## [2.0.22-alpha](https://github.com/rubykube/barong/tree/2.0.22-alpha) (2019-01-03) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.21-alpha...2.0.22-alpha) + +**Closed issues:** + +- Event API model.user.updated bug [\#690](https://github.com/rubykube/barong/issues/690) + +**Merged pull requests:** + +- Add users entity on login and /me [\#706](https://github.com/rubykube/barong/pull/706) ([ec](https://github.com/ec)) +- Small fixes of event api and lock feature [\#705](https://github.com/rubykube/barong/pull/705) ([ec](https://github.com/ec)) +- Small fixes of event api and lock feature [\#704](https://github.com/rubykube/barong/pull/704) ([ec](https://github.com/ec)) +- Add ability to lock account, add filter for state on admin index [\#700](https://github.com/rubykube/barong/pull/700) ([ec](https://github.com/ec)) + +## [2.0.21-alpha](https://github.com/rubykube/barong/tree/2.0.21-alpha) (2018-12-26) +[Full Changelog](https://github.com/rubykube/barong/compare/v2.0.20-alpha...2.0.21-alpha) + +**Closed issues:** + +- CarrierWave::Storage::Fog unitialized constant in prod mode [\#698](https://github.com/rubykube/barong/issues/698) + +**Merged pull requests:** + +- Small fixes \(drone ci, event api updated\_at bug\) [\#702](https://github.com/rubykube/barong/pull/702) ([ec](https://github.com/ec)) + +## [v2.0.20-alpha](https://github.com/rubykube/barong/tree/v2.0.20-alpha) (2018-12-25) +[Full Changelog](https://github.com/rubykube/barong/compare/v2.0.19-alpha...v2.0.20-alpha) + +**Closed issues:** + +- Codeclimate [\#691](https://github.com/rubykube/barong/issues/691) + +**Merged pull requests:** + +- Add carrierwave config for production [\#699](https://github.com/rubykube/barong/pull/699) ([alinetskyi](https://github.com/alinetskyi)) + +## [v2.0.19-alpha](https://github.com/rubykube/barong/tree/v2.0.19-alpha) (2018-12-24) +[Full Changelog](https://github.com/rubykube/barong/compare/v2.0.18-alpha...v2.0.19-alpha) + +**Merged pull requests:** + +- Admin update label func and resourse api labels CRUD [\#696](https://github.com/rubykube/barong/pull/696) ([ec](https://github.com/ec)) + +## [v2.0.18-alpha](https://github.com/rubykube/barong/tree/v2.0.18-alpha) (2018-12-24) +[Full Changelog](https://github.com/rubykube/barong/compare/v2.0.17-alpha...v2.0.18-alpha) + +**Merged pull requests:** + +- Add label on profile adding without level increase [\#697](https://github.com/rubykube/barong/pull/697) ([ec](https://github.com/ec)) + +## [v2.0.17-alpha](https://github.com/rubykube/barong/tree/v2.0.17-alpha) (2018-12-24) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.16-alpha...v2.0.17-alpha) + +**Merged pull requests:** + +- Update the Drone CI to replace Travis and DockerHub [\#695](https://github.com/rubykube/barong/pull/695) ([vshatravenko](https://github.com/vshatravenko)) +- AuthZ move to controller and lib [\#686](https://github.com/rubykube/barong/pull/686) ([ec](https://github.com/ec)) + +## [2.0.16-alpha](https://github.com/rubykube/barong/tree/2.0.16-alpha) (2018-12-20) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.15-alpha...2.0.16-alpha) + +**Merged pull requests:** + +- Added missing event with confirmation token on signup [\#694](https://github.com/rubykube/barong/pull/694) ([ec](https://github.com/ec)) + +## [2.0.15-alpha](https://github.com/rubykube/barong/tree/2.0.15-alpha) (2018-12-19) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.14-alpha...2.0.15-alpha) + +**Closed issues:** + +- RuntimeError: No EVENT\_API\_JWT\_PRIVATE\_KEY found in env! [\#692](https://github.com/rubykube/barong/issues/692) +- remove ability to save the same phone numbers for user [\#689](https://github.com/rubykube/barong/issues/689) +- Call for change password [\#687](https://github.com/rubykube/barong/issues/687) +- GeeTest.com captcha integration on Barong v2 [\#659](https://github.com/rubykube/barong/issues/659) +- Ability to fetch my login history from API [\#524](https://github.com/rubykube/barong/issues/524) + +**Merged pull requests:** + +- Change multiple docs uploading logic from object to array [\#693](https://github.com/rubykube/barong/pull/693) ([ec](https://github.com/ec)) + +## [2.0.14-alpha](https://github.com/rubykube/barong/tree/2.0.14-alpha) (2018-12-17) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.13-alpha...2.0.14-alpha) + +**Closed issues:** + +- \[Barong v2\] Levels update issue [\#676](https://github.com/rubykube/barong/issues/676) + +**Merged pull requests:** + +- Add change password endpoint [\#688](https://github.com/rubykube/barong/pull/688) ([m-an](https://github.com/m-an)) +- Feature/admin api [\#681](https://github.com/rubykube/barong/pull/681) ([ec](https://github.com/ec)) +- GeeTest.com captcha integration on Barong v2 [\#665](https://github.com/rubykube/barong/pull/665) ([bodiasuprun](https://github.com/bodiasuprun)) + +## [2.0.13-alpha](https://github.com/rubykube/barong/tree/2.0.13-alpha) (2018-12-12) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.12-alpha...2.0.13-alpha) + +**Merged pull requests:** + +- Update the production database configuration with DATABASE\_NAME from env [\#685](https://github.com/rubykube/barong/pull/685) ([vshatravenko](https://github.com/vshatravenko)) + +## [2.0.12-alpha](https://github.com/rubykube/barong/tree/2.0.12-alpha) (2018-12-11) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.11-alpha...2.0.12-alpha) + +**Merged pull requests:** + +- Add link\_config script [\#684](https://github.com/rubykube/barong/pull/684) ([alinetskyi](https://github.com/alinetskyi)) + +## [2.0.11-alpha](https://github.com/rubykube/barong/tree/2.0.11-alpha) (2018-12-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.10-alpha...2.0.11-alpha) + +**Merged pull requests:** + +- Hotfix for whitelisting public peatio and barong routes [\#683](https://github.com/rubykube/barong/pull/683) ([ec](https://github.com/ec)) +- Hotfix for whitelisting public peatio and barong routes [\#682](https://github.com/rubykube/barong/pull/682) ([ec](https://github.com/ec)) +- Add base swagger configuration and documentation for API [\#669](https://github.com/rubykube/barong/pull/669) ([ec](https://github.com/ec)) + +## [2.0.10-alpha](https://github.com/rubykube/barong/tree/2.0.10-alpha) (2018-12-10) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.9-alpha...2.0.10-alpha) + +## [2.0.9-alpha](https://github.com/rubykube/barong/tree/2.0.9-alpha) (2018-12-09) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.8-alpha...2.0.9-alpha) + +**Closed issues:** + +- \[Barong v2\] User activity api endpoint improvments [\#663](https://github.com/rubykube/barong/issues/663) +- \[Barong v2\] Document upload multi pictures feature request [\#661](https://github.com/rubykube/barong/issues/661) +- \[Barong v2\] Document upload limit [\#660](https://github.com/rubykube/barong/issues/660) + +## [2.0.8-alpha](https://github.com/rubykube/barong/tree/2.0.8-alpha) (2018-12-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.7-alpha...2.0.8-alpha) + +**Merged pull requests:** + +- Add ability to upload multiple documents [\#679](https://github.com/rubykube/barong/pull/679) ([alinetskyi](https://github.com/alinetskyi)) +- Improve activities coverage [\#677](https://github.com/rubykube/barong/pull/677) ([ec](https://github.com/ec)) + +## [2.0.7-alpha](https://github.com/rubykube/barong/tree/2.0.7-alpha) (2018-12-05) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.6-alpha...2.0.7-alpha) + +**Merged pull requests:** + +- Fix unhandled totp errors [\#675](https://github.com/rubykube/barong/pull/675) ([ec](https://github.com/ec)) + +## [2.0.6-alpha](https://github.com/rubykube/barong/tree/2.0.6-alpha) (2018-12-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.42...2.0.6-alpha) + +**Closed issues:** + +- \[Barong v2\] Add possibility to disable captcha in dev environment, for login and sign in. [\#668](https://github.com/rubykube/barong/issues/668) +- \[Barong v2\] Api\_Keys endpoint response with `500 internal server error` [\#662](https://github.com/rubykube/barong/issues/662) + +**Merged pull requests:** + +- Add codeclimate config file [\#678](https://github.com/rubykube/barong/pull/678) ([ec](https://github.com/ec)) + +## [1.8.42](https://github.com/rubykube/barong/tree/1.8.42) (2018-12-04) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.5-alpha...1.8.42) + +## [2.0.5-alpha](https://github.com/rubykube/barong/tree/2.0.5-alpha) (2018-12-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.9.1...2.0.5-alpha) + +**Closed issues:** + +- \[Barong v1.9,2\] Hard password, password reset bug. [\#667](https://github.com/rubykube/barong/issues/667) +- User documents are publicly visible by default \(carrierwave / fog\) [\#581](https://github.com/rubykube/barong/issues/581) + +**Merged pull requests:** + +- Unlock account after 1 hour of being locked [\#650](https://github.com/rubykube/barong/pull/650) ([m-an](https://github.com/m-an)) + +## [1.9.1](https://github.com/rubykube/barong/tree/1.9.1) (2018-12-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.41...1.9.1) + +**Merged pull requests:** + +- Make documents invisible publicly by changing fog policy [\#673](https://github.com/rubykube/barong/pull/673) ([ec](https://github.com/ec)) +- Make documents invisible publicly by changing fog policy [\#672](https://github.com/rubykube/barong/pull/672) ([ec](https://github.com/ec)) + +## [1.8.41](https://github.com/rubykube/barong/tree/1.8.41) (2018-12-03) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.4-alpha...1.8.41) + +**Closed issues:** + +- how to create new account by api? [\#670](https://github.com/rubykube/barong/issues/670) +- \[Bug\] Devise:Lockable:time Not working for User/api/V1 [\#636](https://github.com/rubykube/barong/issues/636) + +**Merged pull requests:** + +- Hotfix for rack\_attack [\#671](https://github.com/rubykube/barong/pull/671) ([alinetskyi](https://github.com/alinetskyi)) + +## [2.0.4-alpha](https://github.com/rubykube/barong/tree/2.0.4-alpha) (2018-11-29) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.40...2.0.4-alpha) + +**Merged pull requests:** + +- Added missing validations on password update functionality [\#674](https://github.com/rubykube/barong/pull/674) ([ec](https://github.com/ec)) +- Fix activities [\#664](https://github.com/rubykube/barong/pull/664) ([alinetskyi](https://github.com/alinetskyi)) +- Fix share cookies for development, fix Bump [\#658](https://github.com/rubykube/barong/pull/658) ([alinetskyi](https://github.com/alinetskyi)) + +## [1.8.40](https://github.com/rubykube/barong/tree/1.8.40) (2018-11-27) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.3-alpha...1.8.40) + +**Merged pull requests:** + +- Add filter by lvl [\#657](https://github.com/rubykube/barong/pull/657) ([vpetrusenko](https://github.com/vpetrusenko)) + +## [2.0.3-alpha](https://github.com/rubykube/barong/tree/2.0.3-alpha) (2018-11-26) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.2-alpha...2.0.3-alpha) + +## [2.0.2-alpha](https://github.com/rubykube/barong/tree/2.0.2-alpha) (2018-11-25) +[Full Changelog](https://github.com/rubykube/barong/compare/2.0.1-alpha...2.0.2-alpha) + +**Merged pull requests:** + +- Feature/seeder [\#655](https://github.com/rubykube/barong/pull/655) ([calj](https://github.com/calj)) + +## [2.0.1-alpha](https://github.com/rubykube/barong/tree/2.0.1-alpha) (2018-11-25) +[Full Changelog](https://github.com/rubykube/barong/compare/1.9.0...2.0.1-alpha) + +**Merged pull requests:** + +- Bump improvements [\#654](https://github.com/rubykube/barong/pull/654) ([alinetskyi](https://github.com/alinetskyi)) + +## [1.9.0](https://github.com/rubykube/barong/tree/1.9.0) (2018-11-24) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.39...1.9.0) + +## [1.8.39](https://github.com/rubykube/barong/tree/1.8.39) (2018-11-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.38...1.8.39) + +## [1.8.38](https://github.com/rubykube/barong/tree/1.8.38) (2018-11-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.37...1.8.38) + +**Merged pull requests:** + +- Add RabbitMQ middleware for event api [\#653](https://github.com/rubykube/barong/pull/653) ([vpetrusenko](https://github.com/vpetrusenko)) +- Add rack-attack and some API usage limits [\#652](https://github.com/rubykube/barong/pull/652) ([dnfd](https://github.com/dnfd)) +- Fix account unlocking \(account should be unlocked after 1 hour by default\) [\#649](https://github.com/rubykube/barong/pull/649) ([m-an](https://github.com/m-an)) + +## [1.8.37](https://github.com/rubykube/barong/tree/1.8.37) (2018-11-17) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.36...1.8.37) + +**Closed issues:** + +- wrong customer id or password,please try again [\#631](https://github.com/rubykube/barong/issues/631) +- How to edit devise\_error\_messages? [\#628](https://github.com/rubykube/barong/issues/628) +- mini\_racer can't be installed on Macos10.14 [\#615](https://github.com/rubykube/barong/issues/615) +- Rework Auth API documentation [\#602](https://github.com/rubykube/barong/issues/602) +- Error During Compilation [\#584](https://github.com/rubykube/barong/issues/584) +- \(barong\) Authentication failure! csrf\_detected: OmniAuth::Strategies::OAuth2::CallbackError, csrf\_detected | CSRF detected [\#559](https://github.com/rubykube/barong/issues/559) +- Document Upload - We're sorry, but something went wrong. - ArgumentError \( is not a recognized provider\) [\#555](https://github.com/rubykube/barong/issues/555) +- JWT Tokens [\#546](https://github.com/rubykube/barong/issues/546) +- Barong API Key Scopes not utilised in Peatio [\#540](https://github.com/rubykube/barong/issues/540) +- "Sign In With Barong" button is not shown [\#530](https://github.com/rubykube/barong/issues/530) +- How to get BARONG\_CLIENT\_ID, etc? [\#529](https://github.com/rubykube/barong/issues/529) +- User should be allowed to access their login history and receive email confirmation if they login from an unknown IP. [\#436](https://github.com/rubykube/barong/issues/436) + +**Merged pull requests:** + +- handle SMS\_CONTENT env variable [\#648](https://github.com/rubykube/barong/pull/648) ([vpetrusenko](https://github.com/vpetrusenko)) +- Fix AuthZ controller [\#646](https://github.com/rubykube/barong/pull/646) ([dmk](https://github.com/dmk)) +- Add a stronger validation for passwords [\#645](https://github.com/rubykube/barong/pull/645) ([alinetskyi](https://github.com/alinetskyi)) +- Add rack-attack and some API usage limits [\#643](https://github.com/rubykube/barong/pull/643) ([alinetskyi](https://github.com/alinetskyi)) + +## [1.8.36](https://github.com/rubykube/barong/tree/1.8.36) (2018-10-17) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.35...1.8.36) + +**Closed issues:** + +- How to custom dns? [\#621](https://github.com/rubykube/barong/issues/621) +- bundle exec rake db:create db:migrate - no implicit conversion of nil into String [\#620](https://github.com/rubykube/barong/issues/620) +- Vault::TOTP\#vaildate NoMethodError \(undefined method `data' for true:TrueClass\) [\#606](https://github.com/rubykube/barong/issues/606) +- Create scopes in api\_key as string instead array [\#585](https://github.com/rubykube/barong/issues/585) +- Remove/clean session API method [\#577](https://github.com/rubykube/barong/issues/577) +- Incorrect work variable SKIP\_EMAILS [\#572](https://github.com/rubykube/barong/issues/572) +- Social Sign In/Up Support [\#547](https://github.com/rubykube/barong/issues/547) +- 404 Error on api /v1/profiles/me when user's profile not found [\#534](https://github.com/rubykube/barong/issues/534) +- Merge login systems into 1 end-point with cookie [\#523](https://github.com/rubykube/barong/issues/523) +- Events API Rabbitmq middleware [\#518](https://github.com/rubykube/barong/issues/518) +- Support latest GDPR requiremts [\#510](https://github.com/rubykube/barong/issues/510) +- File uploaded, but system message is "error" type instead of "success" [\#310](https://github.com/rubykube/barong/issues/310) +- There is no SMS throttling \(possible to send as many SMS I as want\) [\#89](https://github.com/rubykube/barong/issues/89) + +**Merged pull requests:** + +- Add grape logger middleware [\#639](https://github.com/rubykube/barong/pull/639) ([alinetskyi](https://github.com/alinetskyi)) +- update bootstrap gem : 4.1.2 instead 4.0.0 [\#634](https://github.com/rubykube/barong/pull/634) ([Ohill](https://github.com/Ohill)) +- Fix bootstrap vulnerability by upgrading gem to 4.1.2 version [\#633](https://github.com/rubykube/barong/pull/633) ([Ohill](https://github.com/Ohill)) +- Fix issue with jwt in dev mode [\#630](https://github.com/rubykube/barong/pull/630) ([shal](https://github.com/shal)) +- Fix issue with hardcoded JWT\_SHARED\_SECRET\_KEY [\#629](https://github.com/rubykube/barong/pull/629) ([shal](https://github.com/shal)) +- Define minimum password length in ENV [\#626](https://github.com/rubykube/barong/pull/626) ([alinetskyi](https://github.com/alinetskyi)) +- Add grape logger middleware [\#624](https://github.com/rubykube/barong/pull/624) ([rxx](https://github.com/rxx)) +- Set cookies from /api/sessions [\#622](https://github.com/rubykube/barong/pull/622) ([rxx](https://github.com/rxx)) +- Fix: DEPRECATION WARNING [\#619](https://github.com/rubykube/barong/pull/619) ([ronaldoaraujo](https://github.com/ronaldoaraujo)) +- Allow to configure scopes from env [\#618](https://github.com/rubykube/barong/pull/618) ([rxx](https://github.com/rxx)) +- Verify captcha serverside if it's enabled [\#617](https://github.com/rubykube/barong/pull/617) ([chumaknadya](https://github.com/chumaknadya)) +- Allow to configure smtp url address [\#616](https://github.com/rubykube/barong/pull/616) ([rxx](https://github.com/rxx)) +- Save scopes in APIKey as serialized array \(closes \#585\) [\#597](https://github.com/rubykube/barong/pull/597) ([ysv](https://github.com/ysv)) +- Update api\_keys.md [\#592](https://github.com/rubykube/barong/pull/592) ([calj](https://github.com/calj)) +- README.md update steps to get project running [\#578](https://github.com/rubykube/barong/pull/578) ([skatkov](https://github.com/skatkov)) + +## [1.8.35](https://github.com/rubykube/barong/tree/1.8.35) (2018-09-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.34...1.8.35) + +**Merged pull requests:** + +- Add ROOT\_DOMAIN for configure sessions [\#612](https://github.com/rubykube/barong/pull/612) ([rxx](https://github.com/rxx)) +- Add ROOT\_DOMAIN for configure sessions [\#611](https://github.com/rubykube/barong/pull/611) ([rxx](https://github.com/rxx)) +- Dont let not active accounts to sign in [\#600](https://github.com/rubykube/barong/pull/600) ([rxx](https://github.com/rxx)) +- barong fixing security flags in docs [\#596](https://github.com/rubykube/barong/pull/596) ([imarakhovskiy](https://github.com/imarakhovskiy)) + +## [1.8.34](https://github.com/rubykube/barong/tree/1.8.34) (2018-09-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.33...1.8.34) + +**Merged pull requests:** + +- Fix captcha and lockable errors [\#613](https://github.com/rubykube/barong/pull/613) ([rxx](https://github.com/rxx)) +- Fix message error [\#601](https://github.com/rubykube/barong/pull/601) ([mnaichuk](https://github.com/mnaichuk)) + +## [1.8.33](https://github.com/rubykube/barong/tree/1.8.33) (2018-09-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.32...1.8.33) + +**Merged pull requests:** + +- Verify captcha serverside if it's enabled [\#610](https://github.com/rubykube/barong/pull/610) ([rxx](https://github.com/rxx)) + +## [1.8.32](https://github.com/rubykube/barong/tree/1.8.32) (2018-09-20) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.31...1.8.32) + +**Merged pull requests:** + +- Fix message error [\#609](https://github.com/rubykube/barong/pull/609) ([mnaichuk](https://github.com/mnaichuk)) +- Dont let not active accounts to sign in [\#608](https://github.com/rubykube/barong/pull/608) ([rxx](https://github.com/rxx)) +- \#PPBB-45: Allow to configure multiple CORS at barong [\#607](https://github.com/rubykube/barong/pull/607) ([rxx](https://github.com/rxx)) + +## [1.8.31](https://github.com/rubykube/barong/tree/1.8.31) (2018-09-19) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.30...1.8.31) + +## [1.8.30](https://github.com/rubykube/barong/tree/1.8.30) (2018-09-15) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.29...1.8.30) + +**Closed issues:** + +- Update rails gem to 5.2 [\#573](https://github.com/rubykube/barong/issues/573) + +**Merged pull requests:** + +- Create jwt by session [\#599](https://github.com/rubykube/barong/pull/599) ([rxx](https://github.com/rxx)) +- Merge latest changes from 1-8-stable to master [\#598](https://github.com/rubykube/barong/pull/598) ([rxx](https://github.com/rxx)) +- Update gem dependences [\#595](https://github.com/rubykube/barong/pull/595) ([rxx](https://github.com/rxx)) +- Fix doorkeeper CVE-2018-1000211 vulnerability [\#590](https://github.com/rubykube/barong/pull/590) ([rxx](https://github.com/rxx)) +- Fix doorkeeper CVE-2018-1000211 vulnerability [\#589](https://github.com/rubykube/barong/pull/589) ([rxx](https://github.com/rxx)) +- Add endpoint to create and import accounts with managment api [\#587](https://github.com/rubykube/barong/pull/587) ([rxx](https://github.com/rxx)) +- Update api keys docs [\#586](https://github.com/rubykube/barong/pull/586) ([mnaichuk](https://github.com/mnaichuk)) +- Update ruby and rails versions \(closes \#573\) [\#582](https://github.com/rubykube/barong/pull/582) ([shal](https://github.com/shal)) +- Create correct labels when seed accounts [\#579](https://github.com/rubykube/barong/pull/579) ([shal](https://github.com/shal)) + +## [1.8.29](https://github.com/rubykube/barong/tree/1.8.29) (2018-07-23) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.28...1.8.29) + +**Closed issues:** + +- User session is maintained when signing out from 1 of the 2 apps [\#571](https://github.com/rubykube/barong/issues/571) +- Signed in user gets message from previous user activity [\#570](https://github.com/rubykube/barong/issues/570) + +**Merged pull requests:** + +- Update api\_keys.md [\#575](https://github.com/rubykube/barong/pull/575) ([rxx](https://github.com/rxx)) +- Update api\_keys.md [\#574](https://github.com/rubykube/barong/pull/574) ([rxx](https://github.com/rxx)) + +## [1.8.28](https://github.com/rubykube/barong/tree/1.8.28) (2018-07-18) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.27...1.8.28) + +**Merged pull requests:** + +- Revert "Generate api on bump stage" [\#569](https://github.com/rubykube/barong/pull/569) ([rxx](https://github.com/rxx)) +- Allow pdf on barong form [\#568](https://github.com/rubykube/barong/pull/568) ([rxx](https://github.com/rxx)) +- Generate api on bump stage [\#567](https://github.com/rubykube/barong/pull/567) ([rxx](https://github.com/rxx)) +- Feature/vault security [\#566](https://github.com/rubykube/barong/pull/566) ([rxx](https://github.com/rxx)) + +## [1.8.27](https://github.com/rubykube/barong/tree/1.8.27) (2018-07-16) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.26...1.8.27) + +**Closed issues:** + +- Otp sign request unexpected behavior [\#545](https://github.com/rubykube/barong/issues/545) +- Add validation to "Document number" on the new document upload page [\#497](https://github.com/rubykube/barong/issues/497) + +**Merged pull requests:** + +- Add restrictions for expire\_in and number of documents [\#565](https://github.com/rubykube/barong/pull/565) ([rxx](https://github.com/rxx)) +- Get all labels with managment api [\#564](https://github.com/rubykube/barong/pull/564) ([rxx](https://github.com/rxx)) + +## [1.8.26](https://github.com/rubykube/barong/tree/1.8.26) (2018-07-10) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.25...1.8.26) + +**Closed issues:** + +- The server Barong\(1.8-stable\) doesnot send sms code at all. [\#562](https://github.com/rubykube/barong/issues/562) + +**Merged pull requests:** + +- Countries alpha2 alpha3 [\#563](https://github.com/rubykube/barong/pull/563) ([rxx](https://github.com/rxx)) +- Return understandable vault error, log full message [\#561](https://github.com/rubykube/barong/pull/561) ([rxx](https://github.com/rxx)) + +## [1.8.25](https://github.com/rubykube/barong/tree/1.8.25) (2018-07-06) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.24...1.8.25) + +**Merged pull requests:** + +- Create correct labels when seed accounts [\#560](https://github.com/rubykube/barong/pull/560) ([rxx](https://github.com/rxx)) +- Fix CVE-2018-3760 bug [\#557](https://github.com/rubykube/barong/pull/557) ([rxx](https://github.com/rxx)) + +## [1.8.24](https://github.com/rubykube/barong/tree/1.8.24) (2018-07-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.23...1.8.24) + +**Merged pull requests:** + +- Fix CVE-2018-3760 bug [\#558](https://github.com/rubykube/barong/pull/558) ([rxx](https://github.com/rxx)) + +## [1.8.23](https://github.com/rubykube/barong/tree/1.8.23) (2018-07-03) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.22...1.8.23) + +**Closed issues:** + +- Fix issue with lockable on sessions api [\#479](https://github.com/rubykube/barong/issues/479) +- Ability to limit rate on login API [\#440](https://github.com/rubykube/barong/issues/440) + +**Merged pull requests:** + +- Add validations to document [\#551](https://github.com/rubykube/barong/pull/551) ([rxx](https://github.com/rxx)) +- All new patches from 1-8-stable upto 1.8.22 [\#543](https://github.com/rubykube/barong/pull/543) ([rxx](https://github.com/rxx)) + +## [1.8.22](https://github.com/rubykube/barong/tree/1.8.22) (2018-06-26) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.21...1.8.22) + +**Merged pull requests:** + +- Fix wrong field in accounts/get [\#552](https://github.com/rubykube/barong/pull/552) ([rxx](https://github.com/rxx)) +- Lockable in API [\#544](https://github.com/rubykube/barong/pull/544) ([m-an](https://github.com/m-an)) + +## [1.8.21](https://github.com/rubykube/barong/tree/1.8.21) (2018-06-25) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.20...1.8.21) + +**Merged pull requests:** + +- Fixes docker image build [\#554](https://github.com/rubykube/barong/pull/554) ([shingonoide](https://github.com/shingonoide)) + +## [1.8.20](https://github.com/rubykube/barong/tree/1.8.20) (2018-06-22) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.19...1.8.20) + +**Closed issues:** + +- Add ability to run without Vault and show warning [\#346](https://github.com/rubykube/barong/issues/346) + +**Merged pull requests:** + +- Add email for Event API system.account.reset\_password\_token, system.account.unlock\_token events, system.document.verified, system.document.rejected [\#550](https://github.com/rubykube/barong/pull/550) ([mitjok](https://github.com/mitjok)) + +## [1.8.19](https://github.com/rubykube/barong/tree/1.8.19) (2018-06-22) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.18...1.8.19) + +**Merged pull requests:** + +- Moved documentation files [\#549](https://github.com/rubykube/barong/pull/549) ([calj](https://github.com/calj)) +- Compute code coverage report [\#542](https://github.com/rubykube/barong/pull/542) ([calj](https://github.com/calj)) + +## [1.8.18](https://github.com/rubykube/barong/tree/1.8.18) (2018-06-21) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.17...1.8.18) + +**Closed issues:** + +- Api endpoints to resend confirmation/unlock instructions [\#541](https://github.com/rubykube/barong/issues/541) +- Bug with extra "0" in phone number [\#499](https://github.com/rubykube/barong/issues/499) + +**Merged pull requests:** + +- Management api config generation from template [\#539](https://github.com/rubykube/barong/pull/539) ([mitjok](https://github.com/mitjok)) + +## [1.8.17](https://github.com/rubykube/barong/tree/1.8.17) (2018-06-15) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.16...1.8.17) + +**Closed issues:** + +- Reset password link with CUSTOM URL [\#538](https://github.com/rubykube/barong/issues/538) +- A little error in API docs [\#536](https://github.com/rubykube/barong/issues/536) +- API allows Login Without 2FA for a 2FA Enabled Account [\#526](https://github.com/rubykube/barong/issues/526) +- Ability to disable 2FA from admin panel [\#521](https://github.com/rubykube/barong/issues/521) +- Ability to let frontend to know that account has enabled 2FA [\#520](https://github.com/rubykube/barong/issues/520) +- Management API: Add endpoints to fetch user information by uid [\#485](https://github.com/rubykube/barong/issues/485) +- API: Add endpoints to resend confirmation [\#480](https://github.com/rubykube/barong/issues/480) +- As an Admin I can disable / remove 2FA form a user [\#210](https://github.com/rubykube/barong/issues/210) +- 2FA signing service specs [\#151](https://github.com/rubykube/barong/issues/151) + +**Merged pull requests:** + +- Get international number from phonelib directly [\#504](https://github.com/rubykube/barong/pull/504) ([rxx](https://github.com/rxx)) + +## [1.8.16](https://github.com/rubykube/barong/tree/1.8.16) (2018-06-15) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.15...1.8.16) + +**Merged pull requests:** + +- Add account uid to system.notification.account [\#537](https://github.com/rubykube/barong/pull/537) ([rxx](https://github.com/rxx)) + +## [1.8.15](https://github.com/rubykube/barong/tree/1.8.15) (2018-06-14) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.14...1.8.15) + +**Merged pull requests:** + +- Check 2FA on Barong session api, stub vault request if vault server is down [\#522](https://github.com/rubykube/barong/pull/522) ([rxx](https://github.com/rxx)) +- Add account get endpoint for managment API [\#486](https://github.com/rubykube/barong/pull/486) ([rxx](https://github.com/rxx)) + +## [1.8.14](https://github.com/rubykube/barong/tree/1.8.14) (2018-06-14) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.13...1.8.14) + +**Merged pull requests:** + +- Add validations for profile, add datepicker and country select for profile admin panel [\#477](https://github.com/rubykube/barong/pull/477) ([rxx](https://github.com/rxx)) + +## [1.8.13](https://github.com/rubykube/barong/tree/1.8.13) (2018-06-12) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.12...1.8.13) + +**Closed issues:** + +- Event API has no raw devise token and has no ability to send reset password email [\#525](https://github.com/rubykube/barong/issues/525) +- Level Mapping on admin Panel [\#517](https://github.com/rubykube/barong/issues/517) +- On document varification user Level does not update to 3 [\#503](https://github.com/rubykube/barong/issues/503) +- Optimization application [\#501](https://github.com/rubykube/barong/issues/501) +- Ability to generate API key pair server-side [\#496](https://github.com/rubykube/barong/issues/496) +- Changing already created labels scope from private to public is not changing the level of users account. [\#476](https://github.com/rubykube/barong/issues/476) +- Can get stuck on the Phone verification step [\#466](https://github.com/rubykube/barong/issues/466) +- Check all required ENV in production environment and raise for missing vars [\#457](https://github.com/rubykube/barong/issues/457) +- Label's are supporting upper and lower cases. [\#454](https://github.com/rubykube/barong/issues/454) +- Validation or informative message on Uploaded large jpg for kyc passport [\#444](https://github.com/rubykube/barong/issues/444) +- Review all field types in forms [\#432](https://github.com/rubykube/barong/issues/432) +- Broken automatic tagging [\#361](https://github.com/rubykube/barong/issues/361) +- Captcha support for signup / login [\#356](https://github.com/rubykube/barong/issues/356) +- ActionController::InvalidAuthenticityToken [\#349](https://github.com/rubykube/barong/issues/349) +- Change success message from "created" to "uploaded" [\#334](https://github.com/rubykube/barong/issues/334) +- Ability to define favicon for website [\#331](https://github.com/rubykube/barong/issues/331) +- API to destroy session [\#286](https://github.com/rubykube/barong/issues/286) +- Plugin API specs [\#201](https://github.com/rubykube/barong/issues/201) +- Make a configuration class [\#200](https://github.com/rubykube/barong/issues/200) +- Ability to login with Google Account [\#175](https://github.com/rubykube/barong/issues/175) + +**Merged pull requests:** + +- Fix issues with levels [\#535](https://github.com/rubykube/barong/pull/535) ([rxx](https://github.com/rxx)) +- Feature/admin disable 2fa [\#533](https://github.com/rubykube/barong/pull/533) ([rxx](https://github.com/rxx)) +- Add otp sign endpoint [\#527](https://github.com/rubykube/barong/pull/527) ([rxx](https://github.com/rxx)) + +## [1.8.12](https://github.com/rubykube/barong/tree/1.8.12) (2018-06-11) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.11...1.8.12) + +**Merged pull requests:** + +- Set events with correct devise tokens [\#528](https://github.com/rubykube/barong/pull/528) ([rxx](https://github.com/rxx)) +- Add localization for grape required fields [\#515](https://github.com/rubykube/barong/pull/515) ([rxx](https://github.com/rxx)) +- Fix labels bug with changing scope [\#481](https://github.com/rubykube/barong/pull/481) ([m-an](https://github.com/m-an)) +- Feature/add favicon to website [\#473](https://github.com/rubykube/barong/pull/473) ([rxx](https://github.com/rxx)) + +## [1.8.11](https://github.com/rubykube/barong/tree/1.8.11) (2018-06-06) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.10...1.8.11) + +**Closed issues:** + +- Wrong customer ID or password,please try again. [\#509](https://github.com/rubykube/barong/issues/509) + +**Merged pull requests:** + +- Remove rack-cors [\#519](https://github.com/rubykube/barong/pull/519) ([rxx](https://github.com/rxx)) + +## [1.8.10](https://github.com/rubykube/barong/tree/1.8.10) (2018-06-06) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.9...1.8.10) + +**Closed issues:** + +- Add additional document type for uploading documents \(Utility Bill\) [\#489](https://github.com/rubykube/barong/issues/489) + +**Merged pull requests:** + +- Add CORS middleware [\#516](https://github.com/rubykube/barong/pull/516) ([rxx](https://github.com/rxx)) + +## [1.8.9](https://github.com/rubykube/barong/tree/1.8.9) (2018-06-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.5.2...1.8.9) + +**Merged pull requests:** + +- Remove identity from default configuration [\#505](https://github.com/rubykube/barong/pull/505) ([rxx](https://github.com/rxx)) + +## [1.5.2](https://github.com/rubykube/barong/tree/1.5.2) (2018-06-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.8...1.5.2) + +## [1.8.8](https://github.com/rubykube/barong/tree/1.8.8) (2018-06-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.7...1.8.8) + +**Merged pull requests:** + +- Change password security error message [\#511](https://github.com/rubykube/barong/pull/511) ([rxx](https://github.com/rxx)) +- Ci bump pagination 1 5 [\#471](https://github.com/rubykube/barong/pull/471) ([rxx](https://github.com/rxx)) + +## [1.8.7](https://github.com/rubykube/barong/tree/1.8.7) (2018-06-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.7.1...1.8.7) + +**Merged pull requests:** + +- Remove env check. It does not work with docker build [\#514](https://github.com/rubykube/barong/pull/514) ([rxx](https://github.com/rxx)) +- Add vault logging [\#513](https://github.com/rubykube/barong/pull/513) ([rxx](https://github.com/rxx)) + +## [1.7.1](https://github.com/rubykube/barong/tree/1.7.1) (2018-06-04) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.5...1.7.1) + +## [1.8.5](https://github.com/rubykube/barong/tree/1.8.5) (2018-06-03) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.6...1.8.5) + +## [1.8.6](https://github.com/rubykube/barong/tree/1.8.6) (2018-06-03) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.4...1.8.6) + +**Closed issues:** + +- Omission in the passport verification flow [\#500](https://github.com/rubykube/barong/issues/500) + +**Merged pull requests:** + +- Allow travis to build image without envs [\#508](https://github.com/rubykube/barong/pull/508) ([rxx](https://github.com/rxx)) +- Update travis CI and fix version bumping [\#507](https://github.com/rubykube/barong/pull/507) ([shal](https://github.com/shal)) + +## [1.8.4](https://github.com/rubykube/barong/tree/1.8.4) (2018-05-31) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.3...1.8.4) + +## [1.8.3](https://github.com/rubykube/barong/tree/1.8.3) (2018-05-31) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.2...1.8.3) + +## [1.8.2](https://github.com/rubykube/barong/tree/1.8.2) (2018-05-30) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.1...1.8.2) + +**Merged pull requests:** + +- Add new document type [\#502](https://github.com/rubykube/barong/pull/502) ([rxx](https://github.com/rxx)) +- API tuning: Add security definitions and status code, fixed wrong messages [\#488](https://github.com/rubykube/barong/pull/488) ([rxx](https://github.com/rxx)) +- Check required environments on barong starting [\#484](https://github.com/rubykube/barong/pull/484) ([rxx](https://github.com/rxx)) +- Set correct document flash message [\#474](https://github.com/rubykube/barong/pull/474) ([rxx](https://github.com/rxx)) +- Downcase label key and value before save it [\#472](https://github.com/rubykube/barong/pull/472) ([rxx](https://github.com/rxx)) + +## [1.8.1](https://github.com/rubykube/barong/tree/1.8.1) (2018-05-30) +[Full Changelog](https://github.com/rubykube/barong/compare/1.8.0...1.8.1) + +**Closed issues:** + +- error while uploading documents [\#493](https://github.com/rubykube/barong/issues/493) + +**Merged pull requests:** + +- Add endpoint for resend confirmations [\#531](https://github.com/rubykube/barong/pull/531) ([rxx](https://github.com/rxx)) +- Feed existing account with labels corresponding their level \[for migration from 1.7 to 1.8\] [\#498](https://github.com/rubykube/barong/pull/498) ([m-an](https://github.com/m-an)) +- Fix db:seed applications creation [\#495](https://github.com/rubykube/barong/pull/495) ([calj](https://github.com/calj)) +- Fix document types [\#490](https://github.com/rubykube/barong/pull/490) ([rxx](https://github.com/rxx)) +- Fix typo in phone exists message error [\#487](https://github.com/rubykube/barong/pull/487) ([rxx](https://github.com/rxx)) +- \[ci skip\] Start 1.9.0 development [\#475](https://github.com/rubykube/barong/pull/475) ([rxx](https://github.com/rxx)) +- ci/bump pagination patch for 1.7 [\#469](https://github.com/rubykube/barong/pull/469) ([rxx](https://github.com/rxx)) + +## [1.8.0](https://github.com/rubykube/barong/tree/1.8.0) (2018-05-16) +[Full Changelog](https://github.com/rubykube/barong/compare/1.7.0...1.8.0) + +**Closed issues:** + +- Wrong session\[:phone\] == @phone\_number comparison in phones\_controller [\#462](https://github.com/rubykube/barong/issues/462) +- API Key expiration [\#456](https://github.com/rubykube/barong/issues/456) +- Barong Admin Panel: Move all logic from profile show to accounts show [\#449](https://github.com/rubykube/barong/issues/449) +- Logic error connected with changes of critical label Key [\#445](https://github.com/rubykube/barong/issues/445) +- Rewrite mailers due to state removal [\#443](https://github.com/rubykube/barong/issues/443) +- Public labels can prevent creation of private labels with the same key value. [\#442](https://github.com/rubykube/barong/issues/442) +- Deleting an user in Barong did not delete the user's phone or should user deletion be allowed? [\#437](https://github.com/rubykube/barong/issues/437) +- Use jwt for API instead of Doorkeeper Access Token [\#433](https://github.com/rubykube/barong/issues/433) +- Add /api/v1/phones/resend\_code [\#418](https://github.com/rubykube/barong/issues/418) +- Can't edit personal information after account creation [\#417](https://github.com/rubykube/barong/issues/417) +- The server Barong does not send SMS for authorization of a mobile phone. [\#408](https://github.com/rubykube/barong/issues/408) +- API, phone step can be skipped and get level 3 user [\#402](https://github.com/rubykube/barong/issues/402) +- Enforce strong passwords [\#401](https://github.com/rubykube/barong/issues/401) +- Phone verification 404 on a second attempt [\#400](https://github.com/rubykube/barong/issues/400) +- The Gmail address registration issue [\#399](https://github.com/rubykube/barong/issues/399) +- Validate fields, when send doc [\#398](https://github.com/rubykube/barong/issues/398) +- User allowed to re-upload kYC doc and phone even after account is approved [\#397](https://github.com/rubykube/barong/issues/397) +- No Email after KYC verification [\#396](https://github.com/rubykube/barong/issues/396) +- Deleting an user in Barong did not delete the user's phone or should user deletion be allowed? 1.5 [\#395](https://github.com/rubykube/barong/issues/395) +- Error 1.5 when uploading photo [\#394](https://github.com/rubykube/barong/issues/394) +- Post to /api/V1/phones will return 500 when success [\#390](https://github.com/rubykube/barong/issues/390) +- Add field "Nationality" on personal information step [\#387](https://github.com/rubykube/barong/issues/387) +- Implement Event API [\#382](https://github.com/rubykube/barong/issues/382) +- Add Ability to Confirm Email Verification via API [\#369](https://github.com/rubykube/barong/issues/369) +- Re-write phone verification step using the api v1 [\#365](https://github.com/rubykube/barong/issues/365) +- Vault 404 when enabling 2FA [\#355](https://github.com/rubykube/barong/issues/355) +- User should be notified if he was rejected by admin [\#347](https://github.com/rubykube/barong/issues/347) +- Use fog-google 1.3.3 [\#342](https://github.com/rubykube/barong/issues/342) +- Indian number - endless search [\#340](https://github.com/rubykube/barong/issues/340) +- db:seed fails [\#339](https://github.com/rubykube/barong/issues/339) +- Redirection bug after uploading a document [\#333](https://github.com/rubykube/barong/issues/333) +- Redirect user to platform after login [\#332](https://github.com/rubykube/barong/issues/332) +- Error when uploading document [\#324](https://github.com/rubykube/barong/issues/324) +- Unable to receive SMS to +38\(091\)XXX-XX-XX [\#322](https://github.com/rubykube/barong/issues/322) +- Migrate from fog-aws to carrierwave-aws [\#321](https://github.com/rubykube/barong/issues/321) +- Migrate from fog-google to carrierwave-google-storage [\#320](https://github.com/rubykube/barong/issues/320) +- «Confirm» button is crazy [\#319](https://github.com/rubykube/barong/issues/319) +- Logo couldn't be loaded [\#318](https://github.com/rubykube/barong/issues/318) +- Ability to configure level dynamically [\#315](https://github.com/rubykube/barong/issues/315) +- Log-Out issue [\#314](https://github.com/rubykube/barong/issues/314) +- "Phone is invalid" with international code [\#313](https://github.com/rubykube/barong/issues/313) +- After delete an account [\#307](https://github.com/rubykube/barong/issues/307) +- Add first and last names to JWT token [\#292](https://github.com/rubykube/barong/issues/292) +- no implicit conversion of nil into String [\#289](https://github.com/rubykube/barong/issues/289) +- Specify an endpoint GET /account/sign\_out to sign out the user from devise session [\#268](https://github.com/rubykube/barong/issues/268) +- Unable to sign in/sign up [\#266](https://github.com/rubykube/barong/issues/266) +- Specify API endpoint for JWT renewal [\#262](https://github.com/rubykube/barong/issues/262) +- It is possible to set any state for account & profile [\#240](https://github.com/rubykube/barong/issues/240) +- API should not respond with 2XX in case of invalid phone [\#217](https://github.com/rubykube/barong/issues/217) +- Ability to manage trading tokens from barong [\#211](https://github.com/rubykube/barong/issues/211) +- Email notification when user Approved [\#197](https://github.com/rubykube/barong/issues/197) +- Implement new layout for barong emails [\#174](https://github.com/rubykube/barong/issues/174) +- Admin can make himself a member [\#113](https://github.com/rubykube/barong/issues/113) +- Add Sentry [\#76](https://github.com/rubykube/barong/issues/76) + +**Merged pull requests:** + +- Enable devise lockable. [\#467](https://github.com/rubykube/barong/pull/467) ([rxx](https://github.com/rxx)) +- Feature: Add metadata to documents API [\#464](https://github.com/rubykube/barong/pull/464) ([rxx](https://github.com/rxx)) +- Release 1.8.0 [\#463](https://github.com/rubykube/barong/pull/463) ([rxx](https://github.com/rxx)) +- Bugfix/fix phone validation [\#461](https://github.com/rubykube/barong/pull/461) ([rxx](https://github.com/rxx)) +- Move all logic from profile show to accounts show. Show Phones and Account info [\#460](https://github.com/rubykube/barong/pull/460) ([rxx](https://github.com/rxx)) +- Add sentry [\#459](https://github.com/rubykube/barong/pull/459) ([rxx](https://github.com/rxx)) +- Update ci/bump.rb: add pagination for GitHub API \(fixes bumping for older branches\). [\#458](https://github.com/rubykube/barong/pull/458) ([rxx](https://github.com/rxx)) +- Added password strength validation [\#455](https://github.com/rubykube/barong/pull/455) ([gfedorenko](https://github.com/gfedorenko)) +- Fix issue with api datetime format [\#453](https://github.com/rubykube/barong/pull/453) ([rxx](https://github.com/rxx)) +- Remove outdated deployment stuff [\#452](https://github.com/rubykube/barong/pull/452) ([ysv](https://github.com/ysv)) +- Mark account as discarded istead of delete it [\#451](https://github.com/rubykube/barong/pull/451) ([rxx](https://github.com/rxx)) +- Add condition for unconfirmed emails check on session\#create api endpoint [\#448](https://github.com/rubykube/barong/pull/448) ([m-an](https://github.com/m-an)) +- Send emails if they are enabled. Add SKIP\_EMAILS env [\#446](https://github.com/rubykube/barong/pull/446) ([rxx](https://github.com/rxx)) +- Add api\_key resource and generate jwt session [\#441](https://github.com/rubykube/barong/pull/441) ([rxx](https://github.com/rxx)) +- Added link\_config script [\#434](https://github.com/rubykube/barong/pull/434) ([ysv](https://github.com/ysv)) +- Show level and labels on account index page [\#431](https://github.com/rubykube/barong/pull/431) ([rxx](https://github.com/rxx)) +- Migrate application levels logic to use labels [\#430](https://github.com/rubykube/barong/pull/430) ([m-an](https://github.com/m-an)) +- Speed up docker image build [\#429](https://github.com/rubykube/barong/pull/429) ([ysv](https://github.com/ysv)) +- Do not trust public label when calculate level [\#428](https://github.com/rubykube/barong/pull/428) ([rxx](https://github.com/rxx)) +- Fix function call [\#426](https://github.com/rubykube/barong/pull/426) ([vpetrusenko](https://github.com/vpetrusenko)) +- Use SecureRandom in db:seed [\#425](https://github.com/rubykube/barong/pull/425) ([ysv](https://github.com/ysv)) +- Fix domain to host [\#424](https://github.com/rubykube/barong/pull/424) ([rxx](https://github.com/rxx)) +- Implement send code API [\#423](https://github.com/rubykube/barong/pull/423) ([vpetrusenko](https://github.com/vpetrusenko)) +- Changing seeds to users [\#422](https://github.com/rubykube/barong/pull/422) ([mod](https://github.com/mod)) +- Feature/level logic with labels [\#421](https://github.com/rubykube/barong/pull/421) ([m-an](https://github.com/m-an)) +- Fix API error messages [\#416](https://github.com/rubykube/barong/pull/416) ([vpetrusenko](https://github.com/vpetrusenko)) +- Add labels to admin panel with CRUD [\#414](https://github.com/rubykube/barong/pull/414) ([vpetrusenko](https://github.com/vpetrusenko)) +- Allow setting JWT secret key as pem file [\#413](https://github.com/rubykube/barong/pull/413) ([dmk](https://github.com/dmk)) +- Added event\_api specs [\#412](https://github.com/rubykube/barong/pull/412) ([rxx](https://github.com/rxx)) +- Events API, Plugins [\#410](https://github.com/rubykube/barong/pull/410) ([rxx](https://github.com/rxx)) +- Adding level definition [\#409](https://github.com/rubykube/barong/pull/409) ([mod](https://github.com/mod)) +- Feature/managment api [\#407](https://github.com/rubykube/barong/pull/407) ([rxx](https://github.com/rxx)) +- Update 1.7.0 migration steps notes [\#405](https://github.com/rubykube/barong/pull/405) ([rxx](https://github.com/rxx)) +- Make db:seed more flexible [\#404](https://github.com/rubykube/barong/pull/404) ([dmk](https://github.com/dmk)) +- Fix errors in phones api [\#393](https://github.com/rubykube/barong/pull/393) ([rxx](https://github.com/rxx)) +- Feature/account confirm api [\#391](https://github.com/rubykube/barong/pull/391) ([rxx](https://github.com/rxx)) +- Fix missing PhoneUtils bug [\#389](https://github.com/rubykube/barong/pull/389) ([rxx](https://github.com/rxx)) +- Fix/bump eligible [\#381](https://github.com/rubykube/barong/pull/381) ([mod](https://github.com/mod)) +- Start Barong 1.8.0 development! [\#370](https://github.com/rubykube/barong/pull/370) ([rxx](https://github.com/rxx)) + +## [1.7.0](https://github.com/rubykube/barong/tree/1.7.0) (2018-04-20) +[Full Changelog](https://github.com/rubykube/barong/compare/1.6.0...1.7.0) + +**Closed issues:** + +- Make API call with restore password. [\#362](https://github.com/rubykube/barong/issues/362) +- Setup code climate rules [\#344](https://github.com/rubykube/barong/issues/344) +- I am able to confirm my phone without verification code [\#323](https://github.com/rubykube/barong/issues/323) +- API endpoint to reset password [\#301](https://github.com/rubykube/barong/issues/301) +- API endpoint for 2FA [\#300](https://github.com/rubykube/barong/issues/300) +- Issues with verifying the phone number when working in Firefox [\#261](https://github.com/rubykube/barong/issues/261) +- Recovery codes for two factor authentication [\#244](https://github.com/rubykube/barong/issues/244) +- I scanned QR for 2FA. What's next? [\#237](https://github.com/rubykube/barong/issues/237) +- Find a way to use service account instead of GCS secrets [\#185](https://github.com/rubykube/barong/issues/185) + +**Merged pull requests:** + +- Fix missing PhoneUtils bug [\#388](https://github.com/rubykube/barong/pull/388) ([rxx](https://github.com/rxx)) +- \#382: Event API [\#385](https://github.com/rubykube/barong/pull/385) ([rxx](https://github.com/rxx)) +- Release 1.7.0 [\#384](https://github.com/rubykube/barong/pull/384) ([rxx](https://github.com/rxx)) +- Fix phones controller [\#383](https://github.com/rubykube/barong/pull/383) ([dmk](https://github.com/dmk)) +- Fixing bump [\#379](https://github.com/rubykube/barong/pull/379) ([mod](https://github.com/mod)) +- Increase spec coverage [\#378](https://github.com/rubykube/barong/pull/378) ([m-an](https://github.com/m-an)) +- Fix failing profile specs [\#377](https://github.com/rubykube/barong/pull/377) ([m-an](https://github.com/m-an)) +- Fix failing profile specs [\#376](https://github.com/rubykube/barong/pull/376) ([m-an](https://github.com/m-an)) +- Fix failing profile specs [\#375](https://github.com/rubykube/barong/pull/375) ([m-an](https://github.com/m-an)) +- Notify user by email when his state changes [\#374](https://github.com/rubykube/barong/pull/374) ([m-an](https://github.com/m-an)) +- Notify user by email when his state changes [\#373](https://github.com/rubykube/barong/pull/373) ([m-an](https://github.com/m-an)) +- Notify user by email when his state changes [\#372](https://github.com/rubykube/barong/pull/372) ([m-an](https://github.com/m-an)) +- Add reset password api [\#371](https://github.com/rubykube/barong/pull/371) ([rxx](https://github.com/rxx)) +- Restify api endpoints [\#368](https://github.com/rubykube/barong/pull/368) ([rxx](https://github.com/rxx)) +- User level is downgraded to 2 when his profile is rejected [\#367](https://github.com/rubykube/barong/pull/367) ([m-an](https://github.com/m-an)) +- User level is downgraded to 2 when his profile is rejected [\#366](https://github.com/rubykube/barong/pull/366) ([m-an](https://github.com/m-an)) +- User level is downgraded to 2 when his profile is rejected [\#360](https://github.com/rubykube/barong/pull/360) ([m-an](https://github.com/m-an)) +- Fix db:seed [\#359](https://github.com/rubykube/barong/pull/359) ([spavlishak](https://github.com/spavlishak)) +- Add ability to set uid and gid as docker build args [\#358](https://github.com/rubykube/barong/pull/358) ([gfedorenko](https://github.com/gfedorenko)) +- Fix typo in error rescue [\#357](https://github.com/rubykube/barong/pull/357) ([m-an](https://github.com/m-an)) +- Bugfix/phone verification without code\[1.5\] [\#354](https://github.com/rubykube/barong/pull/354) ([rxx](https://github.com/rxx)) +- Fix phone verification issue\[1.6\] [\#353](https://github.com/rubykube/barong/pull/353) ([rxx](https://github.com/rxx)) +- Start Barong 1.7.0 development! [\#352](https://github.com/rubykube/barong/pull/352) ([m-an](https://github.com/m-an)) +- Fix phone verification issue [\#341](https://github.com/rubykube/barong/pull/341) ([rxx](https://github.com/rxx)) +- Feature/profiles api [\#336](https://github.com/rubykube/barong/pull/336) ([m-an](https://github.com/m-an)) +- Feature/2fa api [\#327](https://github.com/rubykube/barong/pull/327) ([rxx](https://github.com/rxx)) + +## [1.6.0](https://github.com/rubykube/barong/tree/1.6.0) (2018-04-12) +[Full Changelog](https://github.com/rubykube/barong/compare/1.3.1...1.6.0) + +**Closed issues:** + +- Swagger docs [\#345](https://github.com/rubykube/barong/issues/345) +- Add API for manage labels [\#306](https://github.com/rubykube/barong/issues/306) +- API endpoint to create and validate phone [\#299](https://github.com/rubykube/barong/issues/299) + +**Merged pull requests:** + +- Configure codeclimate [\#348](https://github.com/rubykube/barong/pull/348) ([rxx](https://github.com/rxx)) +- Release 1.6.0 [\#343](https://github.com/rubykube/barong/pull/343) ([m-an](https://github.com/m-an)) + +## [1.3.1](https://github.com/rubykube/barong/tree/1.3.1) (2018-04-11) +[Full Changelog](https://github.com/rubykube/barong/compare/1.5.1...1.3.1) + +**Closed issues:** + +- API for admin panel [\#280](https://github.com/rubykube/barong/issues/280) +- Cannot upload document to GCP - Excon::Error::Socket \(Broken pipe \(Errno::EPIPE\)\): [\#278](https://github.com/rubykube/barong/issues/278) +- Add API calls to manage user documents [\#275](https://github.com/rubykube/barong/issues/275) +- Add an API call to update password of current user [\#274](https://github.com/rubykube/barong/issues/274) + +**Merged pull requests:** + +- Fix/rubocop [\#337](https://github.com/rubykube/barong/pull/337) ([rxx](https://github.com/rxx)) +- Update .travis.yml [\#335](https://github.com/rubykube/barong/pull/335) ([rxx](https://github.com/rxx)) +- Add '/v1' prefix for API::V1 routes [\#330](https://github.com/rubykube/barong/pull/330) ([m-an](https://github.com/m-an)) +- Feature/phones api [\#309](https://github.com/rubykube/barong/pull/309) ([rxx](https://github.com/rxx)) + +## [1.5.1](https://github.com/rubykube/barong/tree/1.5.1) (2018-04-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.5.0...1.5.1) + +**Closed issues:** + +- Something went wrong when upload document photo [\#311](https://github.com/rubykube/barong/issues/311) +- old Barong accounts [\#308](https://github.com/rubykube/barong/issues/308) +- Your account has been disabled, contact admin if you have any problem. [\#295](https://github.com/rubykube/barong/issues/295) +- Can’t verify Indian number [\#267](https://github.com/rubykube/barong/issues/267) +- Username input throws exception [\#252](https://github.com/rubykube/barong/issues/252) +- Email input is not focused at page load, after submit password input is not focused too. Low user experience [\#247](https://github.com/rubykube/barong/issues/247) +- It is possible to register the same phone twice [\#239](https://github.com/rubykube/barong/issues/239) +- English error at phone verification [\#238](https://github.com/rubykube/barong/issues/238) +- If I submit second document after approval of first it is not displayed in admin panel [\#236](https://github.com/rubykube/barong/issues/236) +- I can't upload files with UPPERCASE extension [\#235](https://github.com/rubykube/barong/issues/235) +- Markup & CSS issue: page is jumping, very ugly [\#226](https://github.com/rubykube/barong/issues/226) +- Undefined method «request\_uri» at /security [\#219](https://github.com/rubykube/barong/issues/219) +- Phone number is send with double «+» sign when performing verification [\#218](https://github.com/rubykube/barong/issues/218) +- AWS S3 support [\#212](https://github.com/rubykube/barong/issues/212) +- Phone is invalid when it isn't [\#138](https://github.com/rubykube/barong/issues/138) +- 2 Factor Authentication [\#121](https://github.com/rubykube/barong/issues/121) + +**Merged pull requests:** + +- \#306: labels api [\#328](https://github.com/rubykube/barong/pull/328) ([mitjok](https://github.com/mitjok)) +- Downgrade fog-gem version \(fix \#311\) [\#326](https://github.com/rubykube/barong/pull/326) ([shal](https://github.com/shal)) +- Downgrade fog-gem version \(fix \#311\) [\#317](https://github.com/rubykube/barong/pull/317) ([shal](https://github.com/shal)) +- Add documentation for entities [\#312](https://github.com/rubykube/barong/pull/312) ([spavlishak](https://github.com/spavlishak)) +- Fix travis-ci version bump [\#305](https://github.com/rubykube/barong/pull/305) ([dmk](https://github.com/dmk)) +- Update code style checks [\#304](https://github.com/rubykube/barong/pull/304) ([dmk](https://github.com/dmk)) +- Feature/signup api [\#303](https://github.com/rubykube/barong/pull/303) ([rxx](https://github.com/rxx)) +- Ignore yarn files [\#302](https://github.com/rubykube/barong/pull/302) ([rxx](https://github.com/rxx)) +- Documents API [\#297](https://github.com/rubykube/barong/pull/297) ([m-an](https://github.com/m-an)) +- Implemented ability to change password by API for \#274 [\#291](https://github.com/rubykube/barong/pull/291) ([m-an](https://github.com/m-an)) +- Updated loofah to 2.2.2 [\#290](https://github.com/rubykube/barong/pull/290) ([gfedorenko](https://github.com/gfedorenko)) +- Changed devise logout path from delete to get request [\#288](https://github.com/rubykube/barong/pull/288) ([akhlopiachyi](https://github.com/akhlopiachyi)) +- Updated loofah to 2.2.2 [\#287](https://github.com/rubykube/barong/pull/287) ([gfedorenko](https://github.com/gfedorenko)) +- Implemented API JWT renewal [\#264](https://github.com/rubykube/barong/pull/264) ([m-an](https://github.com/m-an)) + +## [1.5.0](https://github.com/rubykube/barong/tree/1.5.0) (2018-03-23) +[Full Changelog](https://github.com/rubykube/barong/compare/1.3.0...1.5.0) + +**Merged pull requests:** + +- Changed devise logout path from delete to get request [\#269](https://github.com/rubykube/barong/pull/269) ([m-an](https://github.com/m-an)) + +## [1.3.0](https://github.com/rubykube/barong/tree/1.3.0) (2018-03-23) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.14...1.3.0) + +**Closed issues:** + +- Add an api with profile information, like first and last names [\#272](https://github.com/rubykube/barong/issues/272) +- Finish websites [\#178](https://github.com/rubykube/barong/issues/178) + +**Merged pull requests:** + +- Implemented API endpoind for receiving JWT by account and password [\#298](https://github.com/rubykube/barong/pull/298) ([m-an](https://github.com/m-an)) +- Add ability to define title [\#284](https://github.com/rubykube/barong/pull/284) ([vpetrusenko](https://github.com/vpetrusenko)) +- Set account state to 'active' on email confirmation [\#279](https://github.com/rubykube/barong/pull/279) ([gfedorenko](https://github.com/gfedorenko)) +- Refactored phones [\#276](https://github.com/rubykube/barong/pull/276) ([gfedorenko](https://github.com/gfedorenko)) + +## [1.0.14](https://github.com/rubykube/barong/tree/1.0.14) (2018-03-20) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.13...1.0.14) + +**Merged pull requests:** + +- Changed devise logout path from delete to get request [\#329](https://github.com/rubykube/barong/pull/329) ([m-an](https://github.com/m-an)) +- Added state for GET profile [\#273](https://github.com/rubykube/barong/pull/273) ([gfedorenko](https://github.com/gfedorenko)) +- Added get profile API call [\#271](https://github.com/rubykube/barong/pull/271) ([gfedorenko](https://github.com/gfedorenko)) +- Set account state along with profile state [\#270](https://github.com/rubykube/barong/pull/270) ([dmk](https://github.com/dmk)) +- Update travis CI script as rubykube/peatio\#639 \(closes \#255\) [\#256](https://github.com/rubykube/barong/pull/256) ([shal](https://github.com/shal)) + +## [1.0.13](https://github.com/rubykube/barong/tree/1.0.13) (2018-03-16) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.12...1.0.13) + +**Closed issues:** + +- Specify API endpoint for JWT revoke [\#263](https://github.com/rubykube/barong/issues/263) +- Otp signing service specs [\#260](https://github.com/rubykube/barong/issues/260) +- Update bump script as in peatio [\#255](https://github.com/rubykube/barong/issues/255) + +**Merged pull requests:** + +- Fix multiple issue with cloud storage configurations [\#259](https://github.com/rubykube/barong/pull/259) ([shal](https://github.com/shal)) + +## [1.0.12](https://github.com/rubykube/barong/tree/1.0.12) (2018-03-13) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.11...1.0.12) + +**Closed issues:** + +- Extremely difficult to understand UID, use email instead [\#258](https://github.com/rubykube/barong/issues/258) +- Update documentation for Barong interaction with client app [\#177](https://github.com/rubykube/barong/issues/177) +- Implement fully working 2FA [\#146](https://github.com/rubykube/barong/issues/146) + +**Merged pull requests:** + +- Use email as account name in Vault TOTP; allow setting issuer name in env [\#257](https://github.com/rubykube/barong/pull/257) ([gfedorenko](https://github.com/gfedorenko)) +- \#247 Add focus to email input at page load and password input [\#249](https://github.com/rubykube/barong/pull/249) ([amir-budaychiev](https://github.com/amir-budaychiev)) + +## [1.0.11](https://github.com/rubykube/barong/tree/1.0.11) (2018-03-09) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.10...1.0.11) + +**Merged pull requests:** + +- Update fog and carrierwave & cleanup \(closes \#212\) [\#241](https://github.com/rubykube/barong/pull/241) ([shal](https://github.com/shal)) + +## [1.0.10](https://github.com/rubykube/barong/tree/1.0.10) (2018-03-07) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.9...1.0.10) + +**Merged pull requests:** + +- Add 2FA docs [\#251](https://github.com/rubykube/barong/pull/251) ([dmk](https://github.com/dmk)) + +## [1.0.9](https://github.com/rubykube/barong/tree/1.0.9) (2018-03-07) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.8...1.0.9) + +**Closed issues:** + +- Customization seems broken in latest master [\#248](https://github.com/rubykube/barong/issues/248) +- \[WIP\] Update the JWT Session payload [\#245](https://github.com/rubykube/barong/issues/245) +- All entered data is lost when submitting form with invalid data [\#223](https://github.com/rubykube/barong/issues/223) +- It is possible to specify & submit DOB in future [\#220](https://github.com/rubykube/barong/issues/220) +- Tag and bump patch on each master change [\#186](https://github.com/rubykube/barong/issues/186) +- Input Authenticator Page [\#152](https://github.com/rubykube/barong/issues/152) +- Fix issue with unlocking account [\#147](https://github.com/rubykube/barong/issues/147) + +**Merged pull requests:** + +- Update the JWT Session payload [\#250](https://github.com/rubykube/barong/pull/250) ([dmk](https://github.com/dmk)) + +## [1.0.8](https://github.com/rubykube/barong/tree/1.0.8) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.7...1.0.8) + +**Merged pull requests:** + +- Different gem subsets dynamic installation [\#207](https://github.com/rubykube/barong/pull/207) ([mitjok](https://github.com/mitjok)) + +## [1.0.7](https://github.com/rubykube/barong/tree/1.0.7) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.6...1.0.7) + +**Closed issues:** + +- After submitting document I am still at the same page. What's next? [\#225](https://github.com/rubykube/barong/issues/225) + +**Merged pull requests:** + +- Add redirect after document create [\#227](https://github.com/rubykube/barong/pull/227) ([vpetrusenko](https://github.com/vpetrusenko)) + +## [1.0.6](https://github.com/rubykube/barong/tree/1.0.6) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.5...1.0.6) + +**Merged pull requests:** + +- Validate dates [\#228](https://github.com/rubykube/barong/pull/228) ([gfedorenko](https://github.com/gfedorenko)) + +## [1.0.5](https://github.com/rubykube/barong/tree/1.0.5) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.4...1.0.5) + +**Merged pull requests:** + +- Fix all entered data which is lost when submitting form with invalid data [\#231](https://github.com/rubykube/barong/pull/231) ([amir-budaychiev](https://github.com/amir-budaychiev)) + +## [1.0.4](https://github.com/rubykube/barong/tree/1.0.4) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.3...1.0.4) + +**Closed issues:** + +- Document form doesn't work with valid data [\#224](https://github.com/rubykube/barong/issues/224) +- Missing assets at /documents/new [\#221](https://github.com/rubykube/barong/issues/221) + +**Merged pull requests:** + +- Add 2FA [\#205](https://github.com/rubykube/barong/pull/205) ([dmk](https://github.com/dmk)) + +## [1.0.3](https://github.com/rubykube/barong/tree/1.0.3) (2018-03-05) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.2...1.0.3) + +**Closed issues:** + +- Invalid background color for error message block in case of failed phone verification [\#216](https://github.com/rubykube/barong/issues/216) +- Fix failing chrome-driver in travis [\#213](https://github.com/rubykube/barong/issues/213) +- Fix dropify icon displaying [\#195](https://github.com/rubykube/barong/issues/195) +- ActionView::Template::Error \(couldn't find file 'dropify/src/js/dropify' with type 'application/javascript' [\#189](https://github.com/rubykube/barong/issues/189) +- Add Vault to the stack [\#176](https://github.com/rubykube/barong/issues/176) +- Add field "E-mail" in Profile [\#168](https://github.com/rubykube/barong/issues/168) +- Refresh spec/models tests [\#167](https://github.com/rubykube/barong/issues/167) +- Make admin panel fixes [\#160](https://github.com/rubykube/barong/issues/160) +- Create a role for managing only profiles [\#108](https://github.com/rubykube/barong/issues/108) + +**Merged pull requests:** + +- Final issue in travis configuration [\#233](https://github.com/rubykube/barong/pull/233) ([shal](https://github.com/shal)) +- Add bot envs in travis.yml [\#232](https://github.com/rubykube/barong/pull/232) ([shal](https://github.com/shal)) +- Fix travis bumping version [\#230](https://github.com/rubykube/barong/pull/230) ([shal](https://github.com/shal)) +- Fix phones notification color [\#222](https://github.com/rubykube/barong/pull/222) ([vpetrusenko](https://github.com/vpetrusenko)) +- Add chrome in .travis.yml & update chromedriver-helper \(closes \#213\) [\#215](https://github.com/rubykube/barong/pull/215) ([shal](https://github.com/shal)) +- Handle file size and extension on front-end side with dropify [\#209](https://github.com/rubykube/barong/pull/209) ([vpetrusenko](https://github.com/vpetrusenko)) +- Remove unused docs and invalid concourse pipeline [\#208](https://github.com/rubykube/barong/pull/208) ([shal](https://github.com/shal)) +- Fix issue with resending unlock instructions for \#147 [\#206](https://github.com/rubykube/barong/pull/206) ([m-an](https://github.com/m-an)) +- Rake task to generate swagger-slate docs [\#204](https://github.com/rubykube/barong/pull/204) ([mitjok](https://github.com/mitjok)) +- Removed minimum size limitation for document [\#199](https://github.com/rubykube/barong/pull/199) ([gfedorenko](https://github.com/gfedorenko)) +- Fix icon in dropify input [\#196](https://github.com/rubykube/barong/pull/196) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Fixed bin/setup to work in correct order [\#194](https://github.com/rubykube/barong/pull/194) ([vshatravenko](https://github.com/vshatravenko)) +- Continue implement features regarding PR \#118 [\#193](https://github.com/rubykube/barong/pull/193) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Fixed tests [\#192](https://github.com/rubykube/barong/pull/192) ([gfedorenko](https://github.com/gfedorenko)) +- Updated documentation [\#190](https://github.com/rubykube/barong/pull/190) ([gfedorenko](https://github.com/gfedorenko)) +- Automatically bump version from TravisCI [\#188](https://github.com/rubykube/barong/pull/188) ([shal](https://github.com/shal)) + +## [1.0.2](https://github.com/rubykube/barong/tree/1.0.2) (2018-02-26) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.1...1.0.2) + +**Merged pull requests:** + +- Remove main-logo and useless \s [\#182](https://github.com/rubykube/barong/pull/182) ([spavlishak](https://github.com/spavlishak)) +- Admin panel fixes [\#165](https://github.com/rubykube/barong/pull/165) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Added autoauthorized application [\#156](https://github.com/rubykube/barong/pull/156) ([gfedorenko](https://github.com/gfedorenko)) +- Creating a role for managing only profiles [\#118](https://github.com/rubykube/barong/pull/118) ([amir-budaychiev](https://github.com/amir-budaychiev)) + +## [1.0.1](https://github.com/rubykube/barong/tree/1.0.1) (2018-02-23) +[Full Changelog](https://github.com/rubykube/barong/compare/1.0.0...1.0.1) + +**Closed issues:** + +- Put links and style pages [\#157](https://github.com/rubykube/barong/issues/157) +- Disable submit phone button and show loader [\#148](https://github.com/rubykube/barong/issues/148) +- Validate Document's size [\#145](https://github.com/rubykube/barong/issues/145) +- Set Devise layout as application [\#144](https://github.com/rubykube/barong/issues/144) +- Barong outputs warning when confirming email [\#137](https://github.com/rubykube/barong/issues/137) +- Make drag and drop for documents work [\#125](https://github.com/rubykube/barong/issues/125) +- Delete merged branchs [\#110](https://github.com/rubykube/barong/issues/110) +- Document number doesn't have max length which results in 500 error when submitting form [\#87](https://github.com/rubykube/barong/issues/87) +- Markup issues [\#84](https://github.com/rubykube/barong/issues/84) + +**Merged pull requests:** + +- Fix for index redirecting [\#180](https://github.com/rubykube/barong/pull/180) ([gfedorenko](https://github.com/gfedorenko)) +- remove some tests/specs [\#173](https://github.com/rubykube/barong/pull/173) ([mitjok](https://github.com/mitjok)) +- Add link for enabling 2FA [\#172](https://github.com/rubykube/barong/pull/172) ([spavlishak](https://github.com/spavlishak)) +- Small fixes [\#171](https://github.com/rubykube/barong/pull/171) ([gfedorenko](https://github.com/gfedorenko)) +- Removed 'utility bill' from 'documents' dropdown options and changed notice for successeful email confirmation [\#170](https://github.com/rubykube/barong/pull/170) ([m-an](https://github.com/m-an)) +- Increase max size of upload [\#162](https://github.com/rubykube/barong/pull/162) ([spavlishak](https://github.com/spavlishak)) +- Create API documentation [\#158](https://github.com/rubykube/barong/pull/158) ([vpetrusenko](https://github.com/vpetrusenko)) +- Set new devise layout as in application \(\#144, \#157\) [\#155](https://github.com/rubykube/barong/pull/155) ([spavlishak](https://github.com/spavlishak)) +- Make drag and drop for documents work [\#154](https://github.com/rubykube/barong/pull/154) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Switch to grape [\#153](https://github.com/rubykube/barong/pull/153) ([vpetrusenko](https://github.com/vpetrusenko)) +- Minor fixes for phones/new form [\#150](https://github.com/rubykube/barong/pull/150) ([gfedorenko](https://github.com/gfedorenko)) +- Add document validation [\#149](https://github.com/rubykube/barong/pull/149) ([spavlishak](https://github.com/spavlishak)) +- Fixes for workbench [\#142](https://github.com/rubykube/barong/pull/142) ([dmk](https://github.com/dmk)) +- Removed unused views [\#139](https://github.com/rubykube/barong/pull/139) ([gfedorenko](https://github.com/gfedorenko)) + +## [1.0.0](https://github.com/rubykube/barong/tree/1.0.0) (2018-02-19) +**Closed issues:** + +- «Some fields are empty or invalid» when submitting KyC [\#136](https://github.com/rubykube/barong/issues/136) +- Fix tests after redesign [\#126](https://github.com/rubykube/barong/issues/126) +- Create dropdown for countries [\#124](https://github.com/rubykube/barong/issues/124) +- Fix URL in readme [\#116](https://github.com/rubykube/barong/issues/116) +- User has ability to get back to already passed step verification [\#115](https://github.com/rubykube/barong/issues/115) +- Doc type dropdown awful [\#111](https://github.com/rubykube/barong/issues/111) +- Fix role changement in admin panel [\#109](https://github.com/rubykube/barong/issues/109) +- Fix license name in readme [\#94](https://github.com/rubykube/barong/issues/94) +- Phone verification form doesn't check phone for uniqueness [\#88](https://github.com/rubykube/barong/issues/88) +- It is possible to create document leaving all fields blank [\#86](https://github.com/rubykube/barong/issues/86) +- break-word causes mail addresses to display ugly [\#85](https://github.com/rubykube/barong/issues/85) +- Missing sender name for emails [\#83](https://github.com/rubykube/barong/issues/83) +- Handle issue on phone verification step [\#75](https://github.com/rubykube/barong/issues/75) +- Integration of omniauth-barong into barongClientTestApp [\#41](https://github.com/rubykube/barong/issues/41) +- Rake task for creating application [\#40](https://github.com/rubykube/barong/issues/40) +- Rake task for creating an admin user [\#39](https://github.com/rubykube/barong/issues/39) +- Add .level field [\#34](https://github.com/rubykube/barong/issues/34) +- Add ability to customize design [\#29](https://github.com/rubykube/barong/issues/29) +- Adding KYC [\#28](https://github.com/rubykube/barong/issues/28) +- Adding 2FA auth. with Google Auth. [\#25](https://github.com/rubykube/barong/issues/25) +- Adding Phone verification system [\#24](https://github.com/rubykube/barong/issues/24) +- Adding doorkeeper into the stack [\#23](https://github.com/rubykube/barong/issues/23) +- CRUD for accounts [\#19](https://github.com/rubykube/barong/issues/19) +- Prevent accounts from brute-force attack \(devise lockable\) [\#13](https://github.com/rubykube/barong/issues/13) +- Create admin panel boilerplate [\#12](https://github.com/rubykube/barong/issues/12) +- Create a light layout maybe using bootstrap [\#10](https://github.com/rubykube/barong/issues/10) +- Dockerfile and pipeline for Barong [\#3](https://github.com/rubykube/barong/issues/3) +- Ability to register with email and password [\#2](https://github.com/rubykube/barong/issues/2) +- Create Account Migration [\#1](https://github.com/rubykube/barong/issues/1) + +**Merged pull requests:** + +- Fix for login [\#143](https://github.com/rubykube/barong/pull/143) ([gfedorenko](https://github.com/gfedorenko)) +- Minor fixes [\#141](https://github.com/rubykube/barong/pull/141) ([gfedorenko](https://github.com/gfedorenko)) +- Modify email and sender name [\#140](https://github.com/rubykube/barong/pull/140) ([gfedorenko](https://github.com/gfedorenko)) +- Include profile.state in jwt [\#135](https://github.com/rubykube/barong/pull/135) ([ec](https://github.com/ec)) +- Update chart [\#134](https://github.com/rubykube/barong/pull/134) ([shal](https://github.com/shal)) +- Put working links in footer [\#133](https://github.com/rubykube/barong/pull/133) ([gfedorenko](https://github.com/gfedorenko)) +- Add UID for Account [\#132](https://github.com/rubykube/barong/pull/132) ([vpetrusenko](https://github.com/vpetrusenko)) +- Splitted profile-document form [\#131](https://github.com/rubykube/barong/pull/131) ([gfedorenko](https://github.com/gfedorenko)) +- Add dropdown for countries \(closes \#124\) [\#130](https://github.com/rubykube/barong/pull/130) ([spavlishak](https://github.com/spavlishak)) +- Fix tests due to new forms [\#128](https://github.com/rubykube/barong/pull/128) ([spavlishak](https://github.com/spavlishak)) +- Update oauth callback [\#127](https://github.com/rubykube/barong/pull/127) ([vpetrusenko](https://github.com/vpetrusenko)) +- Include name in JWT [\#123](https://github.com/rubykube/barong/pull/123) ([dmk](https://github.com/dmk)) +- Fix url in Readme [\#122](https://github.com/rubykube/barong/pull/122) ([spavlishak](https://github.com/spavlishak)) +- Add JWT support to chart [\#120](https://github.com/rubykube/barong/pull/120) ([shal](https://github.com/shal)) +- Verification process design \(Identity, phone, documents\) [\#107](https://github.com/rubykube/barong/pull/107) ([webmix](https://github.com/webmix)) +- Minor fixes to index page [\#106](https://github.com/rubykube/barong/pull/106) ([vpetrusenko](https://github.com/vpetrusenko)) +- Unifying styles in the admin panel [\#105](https://github.com/rubykube/barong/pull/105) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Add missing \ [\#104](https://github.com/rubykube/barong/pull/104) ([yivo](https://github.com/yivo)) +- Use JWT token for authentication [\#103](https://github.com/rubykube/barong/pull/103) ([dmk](https://github.com/dmk)) +- Unifying styles in the admin panel [\#102](https://github.com/rubykube/barong/pull/102) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Fix for phone code button [\#100](https://github.com/rubykube/barong/pull/100) ([vpetrusenko](https://github.com/vpetrusenko)) +- Fix for js [\#98](https://github.com/rubykube/barong/pull/98) ([gfedorenko](https://github.com/gfedorenko)) +- Fix for approving profile [\#97](https://github.com/rubykube/barong/pull/97) ([gfedorenko](https://github.com/gfedorenko)) +- Changed license in README [\#95](https://github.com/rubykube/barong/pull/95) ([gfedorenko](https://github.com/gfedorenko)) +- Added GCS credentials to helm charts [\#93](https://github.com/rubykube/barong/pull/93) ([shal](https://github.com/shal)) +- Added sender's name [\#92](https://github.com/rubykube/barong/pull/92) ([gfedorenko](https://github.com/gfedorenko)) +- Return if phone is already in db [\#91](https://github.com/rubykube/barong/pull/91) ([gfedorenko](https://github.com/gfedorenko)) +- Google cloud upload [\#90](https://github.com/rubykube/barong/pull/90) ([gfedorenko](https://github.com/gfedorenko)) +- Added some validations [\#82](https://github.com/rubykube/barong/pull/82) ([gfedorenko](https://github.com/gfedorenko)) +- Production fixes [\#81](https://github.com/rubykube/barong/pull/81) ([shal](https://github.com/shal)) +- Change license to Apache 2.0 [\#79](https://github.com/rubykube/barong/pull/79) ([calj](https://github.com/calj)) +- Two Factor Auth [\#78](https://github.com/rubykube/barong/pull/78) ([shal](https://github.com/shal)) +- Small improvements [\#74](https://github.com/rubykube/barong/pull/74) ([gfedorenko](https://github.com/gfedorenko)) +- Style improvements on documents, sign in/sign up, profiles, removing header [\#73](https://github.com/rubykube/barong/pull/73) ([spavlishak](https://github.com/spavlishak)) +- Improve some styles [\#71](https://github.com/rubykube/barong/pull/71) ([spavlishak](https://github.com/spavlishak)) +- Rework devise error to be UI benevolent [\#70](https://github.com/rubykube/barong/pull/70) ([ec](https://github.com/ec)) +- Changed datepicker and sms body [\#69](https://github.com/rubykube/barong/pull/69) ([gfedorenko](https://github.com/gfedorenko)) +- Add helm chart to docker ignore files [\#68](https://github.com/rubykube/barong/pull/68) ([shal](https://github.com/shal)) +- Added favicon [\#66](https://github.com/rubykube/barong/pull/66) ([shal](https://github.com/shal)) +- Improvments for levels [\#65](https://github.com/rubykube/barong/pull/65) ([vpetrusenko](https://github.com/vpetrusenko)) +- Provide readme [\#64](https://github.com/rubykube/barong/pull/64) ([shal](https://github.com/shal)) +- Fixed background image [\#63](https://github.com/rubykube/barong/pull/63) ([shal](https://github.com/shal)) +- Added meta tag for mobile view [\#62](https://github.com/rubykube/barong/pull/62) ([gfedorenko](https://github.com/gfedorenko)) +- Fix mail sender email [\#61](https://github.com/rubykube/barong/pull/61) ([shal](https://github.com/shal)) +- Deployment [\#60](https://github.com/rubykube/barong/pull/60) ([shal](https://github.com/shal)) +- Implement levels [\#59](https://github.com/rubykube/barong/pull/59) ([vpetrusenko](https://github.com/vpetrusenko)) +- Work on uploading documents has been continued, fixed the rubocop and rspec tests [\#58](https://github.com/rubykube/barong/pull/58) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Fixed barong host in chart [\#57](https://github.com/rubykube/barong/pull/57) ([shal](https://github.com/shal)) +- Removed unnecessary views [\#56](https://github.com/rubykube/barong/pull/56) ([gfedorenko](https://github.com/gfedorenko)) +- Custom logo, header, footer support & helm chart fix [\#55](https://github.com/rubykube/barong/pull/55) ([shal](https://github.com/shal)) +- Feature/rake seed [\#54](https://github.com/rubykube/barong/pull/54) ([mod](https://github.com/mod)) +- Adding verification design [\#53](https://github.com/rubykube/barong/pull/53) ([gfedorenko](https://github.com/gfedorenko)) +- custom css helper [\#52](https://github.com/rubykube/barong/pull/52) ([mod](https://github.com/mod)) +- Added profiles section to the admin panel [\#50](https://github.com/rubykube/barong/pull/50) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Fix production mailer & move to helios-public [\#47](https://github.com/rubykube/barong/pull/47) ([ysv](https://github.com/ysv)) +- Fixing the tests [\#43](https://github.com/rubykube/barong/pull/43) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- fixed the tests [\#42](https://github.com/rubykube/barong/pull/42) ([amir-budaychiev](https://github.com/amir-budaychiev)) +- Adding layout for profiles and documents [\#38](https://github.com/rubykube/barong/pull/38) ([vpetrusenko](https://github.com/vpetrusenko)) +- Deploy [\#37](https://github.com/rubykube/barong/pull/37) ([shal](https://github.com/shal)) +- Add ability to customize design [\#33](https://github.com/rubykube/barong/pull/33) ([spavlishak](https://github.com/spavlishak)) +- Scaffold website table [\#30](https://github.com/rubykube/barong/pull/30) ([mod](https://github.com/mod)) +- Added ability to connect phone number to account [\#27](https://github.com/rubykube/barong/pull/27) ([gfedorenko](https://github.com/gfedorenko)) +- Adding doorkeeper into the stack [\#26](https://github.com/rubykube/barong/pull/26) ([vpetrusenko](https://github.com/vpetrusenko)) +- Added listing of accounts with ability to delete or edit [\#22](https://github.com/rubykube/barong/pull/22) ([gfedorenko](https://github.com/gfedorenko)) +- Provide License [\#21](https://github.com/rubykube/barong/pull/21) ([spavlishak](https://github.com/spavlishak)) +- Front-end: Bootstrap Gem, Devise forms, Skinning [\#18](https://github.com/rubykube/barong/pull/18) ([webmix](https://github.com/webmix)) +- Set devise secret on container build [\#17](https://github.com/rubykube/barong/pull/17) ([ysv](https://github.com/ysv)) +- Add BS4 for admin & navigation. [\#16](https://github.com/rubykube/barong/pull/16) ([yivo](https://github.com/yivo)) +- K8s [\#15](https://github.com/rubykube/barong/pull/15) ([ysv](https://github.com/ysv)) +- Add admin panel boilerplate [\#14](https://github.com/rubykube/barong/pull/14) ([yivo](https://github.com/yivo)) +- Lockable for devise [\#11](https://github.com/rubykube/barong/pull/11) ([gfedorenko](https://github.com/gfedorenko)) +- Initialize Devise missing views and routes [\#9](https://github.com/rubykube/barong/pull/9) ([mod](https://github.com/mod)) +- Add TravisCI configuration & RSpec + Capybara boilerplate. [\#8](https://github.com/rubykube/barong/pull/8) ([yivo](https://github.com/yivo)) +- Add rubocop.yml [\#7](https://github.com/rubykube/barong/pull/7) ([yivo](https://github.com/yivo)) +- Prepare initial configuration and gemset [\#6](https://github.com/rubykube/barong/pull/6) ([yivo](https://github.com/yivo)) +- Development & software operations [\#5](https://github.com/rubykube/barong/pull/5) ([ysv](https://github.com/ysv)) + + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a53f9f --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/Dockerfile.deps b/Dockerfile.deps new file mode 100644 index 0000000..a065d2f --- /dev/null +++ b/Dockerfile.deps @@ -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}" diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..0711f7b --- /dev/null +++ b/Gemfile @@ -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' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..3d7b03d --- /dev/null +++ b/Gemfile.lock @@ -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 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e72929e --- /dev/null +++ b/LICENSE.md @@ -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. + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..796e72b --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +
+ +# 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) + +## راه‌اندازی سریع + +
+ +```bash +cd ../Alvand-P && rake service:all +``` + +
+ +## مستندات + +| سند | موضوع | +|-----|--------| +| [`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) | فهرست مرکزی | + +
+ +--- + +# Dalan +it is fork project of barong + +### Barong +[![Build Status](https://ci.openware.work/api/badges/openware/barong/status.svg)](https://ci.openware.work/openware/barong) + +Barong is a authentication service for microservice architectures using JWT standard. +It's developped and maintained by [Openware](https://www.openware.com) team. + +# Overview + +It includes the following features: + +- Registration of users +- Role based access control (RBAC) +- Embedded KyC process +- Integrated [KycAID](https://www.openware.com/sdk/docs/barong/kycaid.html) plugin +- Mailing system: event based, support multi-language, secured by cryptographic signatures +- [Service accounts](https://www.openware.com/sdk/docs/barong/service-accounts.html) +- Focused on user privacy: sensitive informations are encrypted in database using vault, masks are applied on fields in user API endpoints + + +# Development + +Prerequisites: +- Ruby version: `2.6.6` +- Bundler preinstalled +- MySQL preinstalled + +1. Install RubyGems dependencies +``` +bundle install +``` + +2. Copy initialisation files +``` +bin/init_config +``` + +3. Create database and run migrations +``` +bundle exec rake db:create db:migrate +``` + +4. Start local server +``` +bundle exec rails server +``` + +# Barong Levels + +In the process of verification Barong assign different levels to accounts + +- Level 0 is default account level +- Level 1 will apply after email verification +- Level 2 will apply after phone verification +- Level 3 will apply after identity & document verification + +# Useful links to documentation +[Barong configuration](https://www.openware.com/sdk/docs/barong/configuration.html) + +[Troubleshooting](https://www.openware.com/sdk/docs/barong/troubleshooting.html) + +[REST API documentation](https://www.openware.com/sdk/docs/barong/rest-api.html) + +[API Keys creation and usage](https://www.openware.com/sdk/docs/barong/general/api-keys.html) + +[Captcha policy overview and configuration](https://www.openware.com/sdk/docs/barong/general/captcha.html) + +[Setting up 2FA](https://www.openware.com/sdk/docs/barong/2fa.html) + +[Barong password hashing](https://www.openware.com/sdk/docs/barong/general/password-hashing.html) + +[Barong data encryption](https://www.openware.com/sdk/docs/barong/general/encryption.html) + diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c39f789 --- /dev/null +++ b/Rakefile @@ -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 diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..e70b452 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +2.6.0 diff --git a/app/api/base.rb b/app/api/base.rb new file mode 100644 index 0000000..050bb7c --- /dev/null +++ b/app/api/base.rb @@ -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 diff --git a/app/api/v2/admin/abilities.rb b/app/api/v2/admin/abilities.rb new file mode 100644 index 0000000..8ff9ffd --- /dev/null +++ b/app/api/v2/admin/abilities.rb @@ -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 diff --git a/app/api/v2/admin/activities.rb b/app/api/v2/admin/activities.rb new file mode 100644 index 0000000..e2282a2 --- /dev/null +++ b/app/api/v2/admin/activities.rb @@ -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 diff --git a/app/api/v2/admin/api_keys.rb b/app/api/v2/admin/api_keys.rb new file mode 100644 index 0000000..61b1acf --- /dev/null +++ b/app/api/v2/admin/api_keys.rb @@ -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 diff --git a/app/api/v2/admin/base.rb b/app/api/v2/admin/base.rb new file mode 100644 index 0000000..1ad7460 --- /dev/null +++ b/app/api/v2/admin/base.rb @@ -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 diff --git a/app/api/v2/admin/entities/activity_with_user.rb b/app/api/v2/admin/entities/activity_with_user.rb new file mode 100644 index 0000000..4698533 --- /dev/null +++ b/app/api/v2/admin/entities/activity_with_user.rb @@ -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 diff --git a/app/api/v2/admin/entities/admin_activity.rb b/app/api/v2/admin/entities/admin_activity.rb new file mode 100644 index 0000000..98192f3 --- /dev/null +++ b/app/api/v2/admin/entities/admin_activity.rb @@ -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 diff --git a/app/api/v2/admin/entities/document.rb b/app/api/v2/admin/entities/document.rb new file mode 100644 index 0000000..bcdc38b --- /dev/null +++ b/app/api/v2/admin/entities/document.rb @@ -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 diff --git a/app/api/v2/admin/entities/phone.rb b/app/api/v2/admin/entities/phone.rb new file mode 100644 index 0000000..1091b33 --- /dev/null +++ b/app/api/v2/admin/entities/phone.rb @@ -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 diff --git a/app/api/v2/admin/entities/profile.rb b/app/api/v2/admin/entities/profile.rb new file mode 100644 index 0000000..06935cd --- /dev/null +++ b/app/api/v2/admin/entities/profile.rb @@ -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 diff --git a/app/api/v2/admin/entities/user_with_kyc.rb b/app/api/v2/admin/entities/user_with_kyc.rb new file mode 100644 index 0000000..5f971dc --- /dev/null +++ b/app/api/v2/admin/entities/user_with_kyc.rb @@ -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 diff --git a/app/api/v2/admin/entities/user_with_profile.rb b/app/api/v2/admin/entities/user_with_profile.rb new file mode 100644 index 0000000..50db1df --- /dev/null +++ b/app/api/v2/admin/entities/user_with_profile.rb @@ -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 diff --git a/app/api/v2/admin/levels.rb b/app/api/v2/admin/levels.rb new file mode 100644 index 0000000..8025b36 --- /dev/null +++ b/app/api/v2/admin/levels.rb @@ -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 diff --git a/app/api/v2/admin/metrics.rb b/app/api/v2/admin/metrics.rb new file mode 100644 index 0000000..9b3a764 --- /dev/null +++ b/app/api/v2/admin/metrics.rb @@ -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 diff --git a/app/api/v2/admin/named_params.rb b/app/api/v2/admin/named_params.rb new file mode 100644 index 0000000..d56588e --- /dev/null +++ b/app/api/v2/admin/named_params.rb @@ -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 diff --git a/app/api/v2/admin/permissions.rb b/app/api/v2/admin/permissions.rb new file mode 100644 index 0000000..4c45bf1 --- /dev/null +++ b/app/api/v2/admin/permissions.rb @@ -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 diff --git a/app/api/v2/admin/profiles.rb b/app/api/v2/admin/profiles.rb new file mode 100644 index 0000000..e04a83b --- /dev/null +++ b/app/api/v2/admin/profiles.rb @@ -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 diff --git a/app/api/v2/admin/restrictions.rb b/app/api/v2/admin/restrictions.rb new file mode 100644 index 0000000..a629b48 --- /dev/null +++ b/app/api/v2/admin/restrictions.rb @@ -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 diff --git a/app/api/v2/admin/users.rb b/app/api/v2/admin/users.rb new file mode 100644 index 0000000..8c1f12b --- /dev/null +++ b/app/api/v2/admin/users.rb @@ -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 diff --git a/app/api/v2/base.rb b/app/api/v2/base.rb new file mode 100644 index 0000000..1703c65 --- /dev/null +++ b/app/api/v2/base.rb @@ -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 diff --git a/app/api/v2/entities/activity.rb b/app/api/v2/entities/activity.rb new file mode 100644 index 0000000..326cf38 --- /dev/null +++ b/app/api/v2/entities/activity.rb @@ -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 diff --git a/app/api/v2/entities/admin_label_view.rb b/app/api/v2/entities/admin_label_view.rb new file mode 100644 index 0000000..623e5b0 --- /dev/null +++ b/app/api/v2/entities/admin_label_view.rb @@ -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 diff --git a/app/api/v2/entities/api_key.rb b/app/api/v2/entities/api_key.rb new file mode 100644 index 0000000..3486800 --- /dev/null +++ b/app/api/v2/entities/api_key.rb @@ -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 diff --git a/app/api/v2/entities/base.rb b/app/api/v2/entities/base.rb new file mode 100644 index 0000000..28a7da2 --- /dev/null +++ b/app/api/v2/entities/base.rb @@ -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 diff --git a/app/api/v2/entities/comment.rb b/app/api/v2/entities/comment.rb new file mode 100644 index 0000000..234f7a6 --- /dev/null +++ b/app/api/v2/entities/comment.rb @@ -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 diff --git a/app/api/v2/entities/data_storage.rb b/app/api/v2/entities/data_storage.rb new file mode 100644 index 0000000..ae77361 --- /dev/null +++ b/app/api/v2/entities/data_storage.rb @@ -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 diff --git a/app/api/v2/entities/document.rb b/app/api/v2/entities/document.rb new file mode 100644 index 0000000..4eadbf6 --- /dev/null +++ b/app/api/v2/entities/document.rb @@ -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 diff --git a/app/api/v2/entities/label.rb b/app/api/v2/entities/label.rb new file mode 100644 index 0000000..a6419a6 --- /dev/null +++ b/app/api/v2/entities/label.rb @@ -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 diff --git a/app/api/v2/entities/level.rb b/app/api/v2/entities/level.rb new file mode 100644 index 0000000..9cca435 --- /dev/null +++ b/app/api/v2/entities/level.rb @@ -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 diff --git a/app/api/v2/entities/permission.rb b/app/api/v2/entities/permission.rb new file mode 100644 index 0000000..3fc1dbe --- /dev/null +++ b/app/api/v2/entities/permission.rb @@ -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 diff --git a/app/api/v2/entities/phone.rb b/app/api/v2/entities/phone.rb new file mode 100644 index 0000000..76711f0 --- /dev/null +++ b/app/api/v2/entities/phone.rb @@ -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 diff --git a/app/api/v2/entities/profile.rb b/app/api/v2/entities/profile.rb new file mode 100644 index 0000000..096465b --- /dev/null +++ b/app/api/v2/entities/profile.rb @@ -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 diff --git a/app/api/v2/entities/restriction.rb b/app/api/v2/entities/restriction.rb new file mode 100644 index 0000000..5111127 --- /dev/null +++ b/app/api/v2/entities/restriction.rb @@ -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 diff --git a/app/api/v2/entities/service_accounts.rb b/app/api/v2/entities/service_accounts.rb new file mode 100644 index 0000000..3669a0d --- /dev/null +++ b/app/api/v2/entities/service_accounts.rb @@ -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 diff --git a/app/api/v2/entities/treasury.rb b/app/api/v2/entities/treasury.rb new file mode 100644 index 0000000..a669113 --- /dev/null +++ b/app/api/v2/entities/treasury.rb @@ -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 diff --git a/app/api/v2/entities/user.rb b/app/api/v2/entities/user.rb new file mode 100644 index 0000000..317ce8e --- /dev/null +++ b/app/api/v2/entities/user.rb @@ -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 diff --git a/app/api/v2/entities/user_with_full_info.rb b/app/api/v2/entities/user_with_full_info.rb new file mode 100644 index 0000000..cf15c57 --- /dev/null +++ b/app/api/v2/entities/user_with_full_info.rb @@ -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 diff --git a/app/api/v2/entities/user_with_kyc.rb b/app/api/v2/entities/user_with_kyc.rb new file mode 100644 index 0000000..a9c6edd --- /dev/null +++ b/app/api/v2/entities/user_with_kyc.rb @@ -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 diff --git a/app/api/v2/entities/user_with_profile.rb b/app/api/v2/entities/user_with_profile.rb new file mode 100644 index 0000000..c58311a --- /dev/null +++ b/app/api/v2/entities/user_with_profile.rb @@ -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 diff --git a/app/api/v2/exception_handlers.rb b/app/api/v2/exception_handlers.rb new file mode 100644 index 0000000..b3a5057 --- /dev/null +++ b/app/api/v2/exception_handlers.rb @@ -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 \ No newline at end of file diff --git a/app/api/v2/identity/base.rb b/app/api/v2/identity/base.rb new file mode 100644 index 0000000..b1c3168 --- /dev/null +++ b/app/api/v2/identity/base.rb @@ -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 diff --git a/app/api/v2/identity/general.rb b/app/api/v2/identity/general.rb new file mode 100644 index 0000000..51a7c9f --- /dev/null +++ b/app/api/v2/identity/general.rb @@ -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 diff --git a/app/api/v2/identity/sessions.rb b/app/api/v2/identity/sessions.rb new file mode 100644 index 0000000..287da04 --- /dev/null +++ b/app/api/v2/identity/sessions.rb @@ -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 diff --git a/app/api/v2/identity/users.rb b/app/api/v2/identity/users.rb new file mode 100644 index 0000000..5e35b8d --- /dev/null +++ b/app/api/v2/identity/users.rb @@ -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 diff --git a/app/api/v2/identity/utils.rb b/app/api/v2/identity/utils.rb new file mode 100644 index 0000000..9f74e5c --- /dev/null +++ b/app/api/v2/identity/utils.rb @@ -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 diff --git a/app/api/v2/management/api_keys.rb b/app/api/v2/management/api_keys.rb new file mode 100644 index 0000000..cd99b29 --- /dev/null +++ b/app/api/v2/management/api_keys.rb @@ -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 diff --git a/app/api/v2/management/base.rb b/app/api/v2/management/base.rb new file mode 100644 index 0000000..693c1c6 --- /dev/null +++ b/app/api/v2/management/base.rb @@ -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 diff --git a/app/api/v2/management/documents.rb b/app/api/v2/management/documents.rb new file mode 100644 index 0000000..8cde2ba --- /dev/null +++ b/app/api/v2/management/documents.rb @@ -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 diff --git a/app/api/v2/management/entities/document.rb b/app/api/v2/management/entities/document.rb new file mode 100644 index 0000000..ba2e21a --- /dev/null +++ b/app/api/v2/management/entities/document.rb @@ -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 diff --git a/app/api/v2/management/entities/phone.rb b/app/api/v2/management/entities/phone.rb new file mode 100644 index 0000000..7dad4ea --- /dev/null +++ b/app/api/v2/management/entities/phone.rb @@ -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 diff --git a/app/api/v2/management/entities/profile.rb b/app/api/v2/management/entities/profile.rb new file mode 100644 index 0000000..4dbc44e --- /dev/null +++ b/app/api/v2/management/entities/profile.rb @@ -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 diff --git a/app/api/v2/management/entities/user_with_kyc.rb b/app/api/v2/management/entities/user_with_kyc.rb new file mode 100644 index 0000000..082d702 --- /dev/null +++ b/app/api/v2/management/entities/user_with_kyc.rb @@ -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 diff --git a/app/api/v2/management/entities/user_with_profile.rb b/app/api/v2/management/entities/user_with_profile.rb new file mode 100644 index 0000000..fdbde39 --- /dev/null +++ b/app/api/v2/management/entities/user_with_profile.rb @@ -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 diff --git a/app/api/v2/management/exceptions/authentication.rb b/app/api/v2/management/exceptions/authentication.rb new file mode 100644 index 0000000..a9cf1e6 --- /dev/null +++ b/app/api/v2/management/exceptions/authentication.rb @@ -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 diff --git a/app/api/v2/management/exceptions/base.rb b/app/api/v2/management/exceptions/base.rb new file mode 100644 index 0000000..55494f7 --- /dev/null +++ b/app/api/v2/management/exceptions/base.rb @@ -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 diff --git a/app/api/v2/management/jwt_authentication_middleware.rb b/app/api/v2/management/jwt_authentication_middleware.rb new file mode 100644 index 0000000..1d36ff1 --- /dev/null +++ b/app/api/v2/management/jwt_authentication_middleware.rb @@ -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 diff --git a/app/api/v2/management/labels.rb b/app/api/v2/management/labels.rb new file mode 100644 index 0000000..1a101de --- /dev/null +++ b/app/api/v2/management/labels.rb @@ -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 diff --git a/app/api/v2/management/otp.rb b/app/api/v2/management/otp.rb new file mode 100644 index 0000000..d19941e --- /dev/null +++ b/app/api/v2/management/otp.rb @@ -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 diff --git a/app/api/v2/management/phones.rb b/app/api/v2/management/phones.rb new file mode 100644 index 0000000..f1dcce6 --- /dev/null +++ b/app/api/v2/management/phones.rb @@ -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 diff --git a/app/api/v2/management/profiles.rb b/app/api/v2/management/profiles.rb new file mode 100644 index 0000000..0b9100f --- /dev/null +++ b/app/api/v2/management/profiles.rb @@ -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 diff --git a/app/api/v2/management/service_accounts.rb b/app/api/v2/management/service_accounts.rb new file mode 100644 index 0000000..6aa51a9 --- /dev/null +++ b/app/api/v2/management/service_accounts.rb @@ -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 diff --git a/app/api/v2/management/tools.rb b/app/api/v2/management/tools.rb new file mode 100644 index 0000000..5f3d818 --- /dev/null +++ b/app/api/v2/management/tools.rb @@ -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 diff --git a/app/api/v2/management/users.rb b/app/api/v2/management/users.rb new file mode 100644 index 0000000..47294ac --- /dev/null +++ b/app/api/v2/management/users.rb @@ -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 diff --git a/app/api/v2/named_params.rb b/app/api/v2/named_params.rb new file mode 100644 index 0000000..2ed43e7 --- /dev/null +++ b/app/api/v2/named_params.rb @@ -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 diff --git a/app/api/v2/public/base.rb b/app/api/v2/public/base.rb new file mode 100644 index 0000000..6f0bfb2 --- /dev/null +++ b/app/api/v2/public/base.rb @@ -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 diff --git a/app/api/v2/public/general.rb b/app/api/v2/public/general.rb new file mode 100644 index 0000000..7c479e9 --- /dev/null +++ b/app/api/v2/public/general.rb @@ -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 diff --git a/app/api/v2/queries/activity_filter.rb b/app/api/v2/queries/activity_filter.rb new file mode 100644 index 0000000..3fca2e5 --- /dev/null +++ b/app/api/v2/queries/activity_filter.rb @@ -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 diff --git a/app/api/v2/queries/user_filter.rb b/app/api/v2/queries/user_filter.rb new file mode 100644 index 0000000..a8a4edf --- /dev/null +++ b/app/api/v2/queries/user_filter.rb @@ -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 diff --git a/app/api/v2/queries/user_with_label_filter.rb b/app/api/v2/queries/user_with_label_filter.rb new file mode 100644 index 0000000..b81ff9d --- /dev/null +++ b/app/api/v2/queries/user_with_label_filter.rb @@ -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 diff --git a/app/api/v2/resource/addresses.rb b/app/api/v2/resource/addresses.rb new file mode 100644 index 0000000..653d185 --- /dev/null +++ b/app/api/v2/resource/addresses.rb @@ -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 diff --git a/app/api/v2/resource/api_keys.rb b/app/api/v2/resource/api_keys.rb new file mode 100644 index 0000000..12d8d66 --- /dev/null +++ b/app/api/v2/resource/api_keys.rb @@ -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 diff --git a/app/api/v2/resource/base.rb b/app/api/v2/resource/base.rb new file mode 100644 index 0000000..1726b69 --- /dev/null +++ b/app/api/v2/resource/base.rb @@ -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 diff --git a/app/api/v2/resource/data_storage.rb b/app/api/v2/resource/data_storage.rb new file mode 100644 index 0000000..36f5342 --- /dev/null +++ b/app/api/v2/resource/data_storage.rb @@ -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 diff --git a/app/api/v2/resource/documents.rb b/app/api/v2/resource/documents.rb new file mode 100644 index 0000000..e490e0f --- /dev/null +++ b/app/api/v2/resource/documents.rb @@ -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 diff --git a/app/api/v2/resource/labels.rb b/app/api/v2/resource/labels.rb new file mode 100644 index 0000000..f52a08a --- /dev/null +++ b/app/api/v2/resource/labels.rb @@ -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 diff --git a/app/api/v2/resource/otp.rb b/app/api/v2/resource/otp.rb new file mode 100644 index 0000000..d0ebe51 --- /dev/null +++ b/app/api/v2/resource/otp.rb @@ -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 diff --git a/app/api/v2/resource/phones.rb b/app/api/v2/resource/phones.rb new file mode 100644 index 0000000..291f0d2 --- /dev/null +++ b/app/api/v2/resource/phones.rb @@ -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 diff --git a/app/api/v2/resource/profiles.rb b/app/api/v2/resource/profiles.rb new file mode 100644 index 0000000..ceda2c3 --- /dev/null +++ b/app/api/v2/resource/profiles.rb @@ -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 diff --git a/app/api/v2/resource/service_accounts.rb b/app/api/v2/resource/service_accounts.rb new file mode 100644 index 0000000..648d372 --- /dev/null +++ b/app/api/v2/resource/service_accounts.rb @@ -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 diff --git a/app/api/v2/resource/users.rb b/app/api/v2/resource/users.rb new file mode 100644 index 0000000..bdf2281 --- /dev/null +++ b/app/api/v2/resource/users.rb @@ -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 diff --git a/app/api/v2/resource/utils.rb b/app/api/v2/resource/utils.rb new file mode 100644 index 0000000..d5a245b --- /dev/null +++ b/app/api/v2/resource/utils.rb @@ -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 diff --git a/app/api/v2/utils.rb b/app/api/v2/utils.rb new file mode 100644 index 0000000..cccaf8e --- /dev/null +++ b/app/api/v2/utils.rb @@ -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 diff --git a/app/api/v2/validations.rb b/app/api/v2/validations.rb new file mode 100644 index 0000000..e20ba02 --- /dev/null +++ b/app/api/v2/validations.rb @@ -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 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 0000000..b16e53d --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,3 @@ +//= link_tree ../images +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 0000000..43ba7e9 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, or any plugin's +// vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require rails-ujs +//= require activestorage +//= require_tree . diff --git a/app/assets/javascripts/cable.js b/app/assets/javascripts/cable.js new file mode 100644 index 0000000..739aa5f --- /dev/null +++ b/app/assets/javascripts/cable.js @@ -0,0 +1,13 @@ +// Action Cable provides the framework to deal with WebSockets in Rails. +// You can generate new channels where WebSocket features live using the `rails generate channel` command. +// +//= require action_cable +//= require_self +//= require_tree ./channels + +(function() { + this.App || (this.App = {}); + + App.cable = ActionCable.createConsumer(); + +}).call(this); diff --git a/app/assets/javascripts/channels/.keep b/app/assets/javascripts/channels/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css new file mode 100644 index 0000000..d05ea0f --- /dev/null +++ b/app/assets/stylesheets/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's + * vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/app/channels/application_cable/channel.rb b/app/channels/application_cable/channel.rb new file mode 100644 index 0000000..d672697 --- /dev/null +++ b/app/channels/application_cable/channel.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Channel < ActionCable::Channel::Base + end +end diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb new file mode 100644 index 0000000..0ff5442 --- /dev/null +++ b/app/channels/application_cable/connection.rb @@ -0,0 +1,4 @@ +module ApplicationCable + class Connection < ActionCable::Connection::Base + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 0000000..09705d1 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -0,0 +1,2 @@ +class ApplicationController < ActionController::Base +end diff --git a/app/controllers/authorize_controller.rb b/app/controllers/authorize_controller.rb new file mode 100644 index 0000000..e7f6df1 --- /dev/null +++ b/app/controllers/authorize_controller.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +require_dependency 'barong/authorize' + +# Rails Metal base controller to manage AuthZ story +class AuthorizeController < ActionController::Metal + include AbstractController::Rendering + + # /api/v2/auth endpoint + def authorize + @restrictions = Rails.cache.fetch('restrictions', expires_in: 5.minutes) { fetch_restrictions } + + # whitelink path + unless params[:path] == 'api/v2/barong/identity/users/access' + Restriction::CATEGORIES.each do |category| + restriction = first_matched_restriction(category) + if restriction && category.in?(%w[blacklist maintenance]) + return deny_access(category, restriction) + elsif restriction && category == 'blocklogin' && params[:path] == 'api/v2/barong/identity/sessions' + return deny_access(category, restriction) + elsif restriction && category == 'whitelist' + break + end + end + end + + req = Barong::Authorize.new(request, params[:path]) # initialize params of request + # checks if request is blacklisted + return access_error!('authz.permission_denied', 401) if req.under_path_rules?('block') + + response.status = 200 + return if req.under_path_rules?('pass') # check if request is whitelisted + + response.headers['Authorization'] = req.auth # sets bearer token + rescue Barong::Authorize::AuthError => e # returns error from validations + response.body = e.message + response.status = e.code + end + + private + + # finds first (if exist) the most precise restriction ip -> ip_subnet -> continent -> country + def first_matched_restriction(category) + request_ip = remote_ip + country = Barong::GeoIP.info(ip: request_ip, key: :country) + continent = Barong::GeoIP.info(ip: request_ip, key: :continent) + if restriction = @restrictions[category]['all']&.find { |r| r.present? } then return restriction end + if restriction = @restrictions[category]['ip']&.find { |r| r.include?(request_ip) } then return restriction end + if restriction = @restrictions[category]['ip_subnet']&.find { |r| IPAddr.new(r[0])&.include?(request_ip) } then return restriction end + if restriction = @restrictions[category]['continent']&.find { |r| r[0]&.casecmp?(continent) } then return restriction end + if restriction = @restrictions[category]['country']&.find { |r| r[0]&.casecmp?(country) } then return restriction end + end + + # as a result gives complex Hash, { category: { scope: values, scope: values }, category: { scope: values, scope: values } } + # { "blacklist"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]}, + # "whitelist"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]}, + # "maintenance"=>{"continent"=>[], "country"=>[], "ip"=>[], "ip_subnet"=>[]} } + def fetch_restrictions + enabled = Restriction.where(state: 'enabled').to_a + + Restriction::CATEGORIES.inject({}) do |table, category| + grouped_by_category = enabled.select { |r| r.category == category } + + grouped_by_scope = Restriction::SCOPES.inject({}) do |table, scope| + scope_restrictions = grouped_by_category.select { |r| r.scope == scope }.pluck(:value, :code) + table.tap { |t| t[scope] = scope_restrictions } + end + table.tap { |t| t[category] = grouped_by_scope } + end + end + + def deny_access(category, restriction) + Rails.logger.info("Access denied for ip #{request.remote_ip} because of #{restriction[0]} restriction") + access_error!("authz.restrict.#{category}", restriction[1]) + end + + def session + request.session + end + + # error for blacklisted routes + def access_error!(text, code) + response.status = code + response.body = { 'errors': [text] }.to_json + end + + def remote_ip + # default behaviour, IP from HTTP_X_FORWARDED_FOR + ip = request.remote_ip + + if Barong::App.config.gateway == 'akamai' + # custom header that contains only client IP + true_client_ip = request.env['HTTP_TRUE_CLIENT_IP'] + # take IP from TRUE_CLIENT_IP only if its not nil or empty + ip = true_client_ip unless true_client_ip.nil? || true_client_ip.empty? + end + + ip + end +end diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 0000000..de6be79 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/app/helpers/salted_crc32.rb b/app/helpers/salted_crc32.rb new file mode 100644 index 0000000..8a00cf0 --- /dev/null +++ b/app/helpers/salted_crc32.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class SaltedCrc32 + def self.generate_hash(value) + Zlib::crc32(value + Barong::App.config.crc32_salt) + end +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 0000000..a009ace --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,2 @@ +class ApplicationJob < ActiveJob::Base +end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 0000000..26148f2 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +class ApplicationMailer < ActionMailer::Base +end diff --git a/app/mailers/postmaster.rb b/app/mailers/postmaster.rb new file mode 100644 index 0000000..95d2877 --- /dev/null +++ b/app/mailers/postmaster.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class Postmaster < ApplicationMailer + layout 'mailer' + + def process_payload(params) + @record = params[:record] + @changes = params[:changes] + @user = params[:user] + @logo = params[:logo] + + sender = "#{Barong::App.config.sender_name} <#{Barong::App.config.sender_email}>" + + email_options = { + subject: params[:subject], + template_name: params[:template_name], + from: sender, + to: @user.email + } + + mail(email_options) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb new file mode 100644 index 0000000..ec12680 --- /dev/null +++ b/app/models/ability.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Full list of roles abilities could be found on docs/roles.md +class Ability + class << self + def abilities + @abilities ||= YAML.load_file("#{Rails.root}/config/abilities.yml") + end + + def admin_permissions + abilities['admin_permissions'] + end + + def roles + abilities['roles'] + end + end +end diff --git a/app/models/activity.rb b/app/models/activity.rb new file mode 100644 index 0000000..43af5e3 --- /dev/null +++ b/app/models/activity.rb @@ -0,0 +1,52 @@ +class Activity < ApplicationRecord + RESULTS = %w[succeed failed denied].freeze + CATEGORIES = %w[admin user].freeze + + belongs_to :user + has_one :target, primary_key: :target_uid, foreign_key: :uid, class_name: 'User' + + validates :user_ip, presence: true, allow_blank: false + validates :user_agent, presence: true, trusty_agent: true + validates :topic, presence: true + validates :result, presence: true, inclusion: { in: RESULTS } + validates :category, presence: true, inclusion: { in: CATEGORIES } + validate :target_user + + # this method allows to use all the methods of ::Browser module (platofrm, modern?, version etc) + def browser + Browser.new(user_agent) + end + + private + + def target_user + errors.add(:target_uid, :invalid) if target_uid.present? && User.where(uid: target_uid).empty? + errors.add(:target_uid, :not_allowed) if target_uid.present? && category.present? && category == 'user' + end + + def readonly? + !new_record? + end +end + +# == Schema Information +# +# Table name: activities +# +# id :bigint not null, primary key +# user_id :bigint not null +# target_uid :string(255) +# category :string(255) +# user_ip :string(255) not null +# user_agent :string(255) not null +# topic :string(255) not null +# action :string(255) not null +# result :string(255) not null +# data :text(65535) +# created_at :datetime +# +# Indexes +# +# index_activities_on_target_uid (target_uid) +# index_activities_on_user_id (user_id) +# diff --git a/app/models/admin_ability.rb b/app/models/admin_ability.rb new file mode 100644 index 0000000..40a13f8 --- /dev/null +++ b/app/models/admin_ability.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AdminAbility + include CanCan::Ability + + def initialize(user) + return if Ability.admin_permissions[user.role].nil? + + # Iterate through user permissions + Ability.admin_permissions[user.role].each do |action, models| + # Iterate through a list of user model access + models.each do |model| + can action.to_sym, model == 'all' ? model.to_sym : model.constantize + end + end + end +end diff --git a/app/models/api_key.rb b/app/models/api_key.rb new file mode 100644 index 0000000..c4ea48a --- /dev/null +++ b/app/models/api_key.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +class APIKey < ApplicationRecord + self.table_name = :apikeys + + include Vault::EncryptedModel + + ALGORITHMS = ['HS256'].freeze + JWT_OPTIONS = { + verify_expiration: true, + verify_iat: true, + verify_jti: true, + sub: 'api_key_jwt', + verify_sub: true, + iss: 'external', + verify_iss: true, + algorithm: 'RS256' + }.freeze + + + vault_lazy_decrypt! + + vault_attribute :secret + + serialize :scope, Array + + belongs_to :key_holder_account, polymorphic: true + + validates :kid, :secret, presence: true + validates :kid, uniqueness: true + validates :algorithm, inclusion: { in: ALGORITHMS } + + before_validation :assign_kid, if: :hmac? + before_validation :validate_key_holder_state, on: :create + before_validation :validate_api_key_state, on: :update + + scope :active, -> { where(state: 'active') } + + def assign_kid + return unless kid.blank? + + loop do + self.kid = random_kid + break unless APIKey.where(kid: kid).any? + end + end + + def random_kid + SecureRandom.hex(8) + end + + def hmac? + self.algorithm.include?('HS') + end + + def active? + self.state == 'active' + end + + private + + def validate_key_holder_state + errors.add(:key_holder_account, :invalid, message: 'non active state for key holder account') unless key_holder_account.active? + end + + def validate_api_key_state + errors.add(:state, :invalid, message: 'cant activate api key with disabled key holder account') if active? && !key_holder_account.active? + end +end + +# == Schema Information +# +# Table name: apikeys +# +# id :bigint not null, primary key +# key_holder_account_id :bigint unsigned, not null +# key_holder_account_type :string(255) default("User"), not null +# kid :string(255) not null +# algorithm :string(255) not null +# scope :string(255) +# secret_encrypted :string(1024) +# state :string(255) default("active"), not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# idx_apikey_on_account (key_holder_account_type,key_holder_account_id) +# index_apikeys_on_kid (kid) UNIQUE +# diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 0000000..8a5259b --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,6 @@ +class ApplicationRecord < ActiveRecord::Base + + include Iso8601TimeFormat + + self.abstract_class = true +end diff --git a/app/models/city.rb b/app/models/city.rb new file mode 100644 index 0000000..2e46660 --- /dev/null +++ b/app/models/city.rb @@ -0,0 +1,32 @@ +class City < ApplicationRecord + belongs_to :province, required: true + has_one :profile + + validates :name, presence: true, + length: 1..255, + format: { + with: /\A[[:word:]\s\-']+\z/, + message: 'only allows letters, digits "-", "\'", and space' + } + +end + +# == Schema Information +# Schema version: 20210425062048 +# +# Table name: cities +# +# id :bigint not null, primary key +# name :string(255) +# province_id :bigint +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_cities_on_province_id (province_id) +# +# Foreign Keys +# +# fk_rails_ea36d40e0b (province_id => provinces.id) +# diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 0000000..04d76d5 --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class Comment < ApplicationRecord + belongs_to :user + + validates :title, :data, :author_uid, presence: true +end + +# == Schema Information +# +# Table name: comments +# +# id :bigint not null, primary key +# user_id :bigint unsigned, not null +# author_uid :string(16) not null +# title :string(64) not null +# data :text(65535) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_comments_on_user_id (user_id) +# diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 0000000..e69de29 diff --git a/app/models/concerns/data_is_json_validator.rb b/app/models/concerns/data_is_json_validator.rb new file mode 100644 index 0000000..2113376 --- /dev/null +++ b/app/models/concerns/data_is_json_validator.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Data json validation +class DataIsJsonValidator < ActiveModel::EachValidator + def validate_each(record, attribute, data) + return if data.nil? + + unless validate_data_is_json!(data) + record.errors.add(attribute, :invalid_format, message: 'data is not json compatible string') + end + end + + def validate_data_is_json!(data) + begin + JSON.parse(data) + true + rescue JSON::ParserError => e + false + end + end +end diff --git a/app/models/concerns/encryptable.rb b/app/models/concerns/encryptable.rb new file mode 100644 index 0000000..e41364d --- /dev/null +++ b/app/models/concerns/encryptable.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Encryptable + extend ActiveSupport::Concern + + class_methods do + def attr_encrypted(*attributes) + attributes.each do |attribute| + define_method("#{attribute}=".to_sym) do |value| + return if value.nil? + + self.public_send( + "#{attribute}_encrypted=".to_sym, + EncryptionService.encrypt(value) + ) + end + + define_method(attribute) do + value = self.public_send("#{attribute}_encrypted".to_sym) + EncryptionService.decrypt(value) if value.present? + end + end + end + end +end diff --git a/app/models/concerns/iso8601_time_format.rb b/app/models/concerns/iso8601_time_format.rb new file mode 100644 index 0000000..4d37651 --- /dev/null +++ b/app/models/concerns/iso8601_time_format.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Convert time to utc 8601 +module Iso8601TimeFormat + extend ActiveSupport::Concern + + def format_iso8601_time(time) + utc_time = time.respond_to?(:utc) ? time.utc : time + utc_time&.iso8601 + end +end diff --git a/app/models/concerns/model_caching.rb b/app/models/concerns/model_caching.rb new file mode 100644 index 0000000..77dc5c4 --- /dev/null +++ b/app/models/concerns/model_caching.rb @@ -0,0 +1,23 @@ +module ModelCaching + extend ActiveSupport::Concern + + def write_cache(action, value, expiration_time) + Rails.cache.write(cache_key_generator(action), value, expires_in: expiration_time.seconds) + end + + def read_cache(action) + Rails.cache.read(cache_key_generator(action)) + end + + def delete_cache(action) + Rails.cache.delete(cache_key_generator(action)) + end + + private + + def cache_key_generator(action) + class_name = self.class.name.to_s + object_id = id.to_s + "#{class_name}_#{object_id}_#{action}" + end +end diff --git a/app/models/concerns/trusty_agent_validator.rb b/app/models/concerns/trusty_agent_validator.rb new file mode 100644 index 0000000..1a83c77 --- /dev/null +++ b/app/models/concerns/trusty_agent_validator.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# User agent validator +class TrustyAgentValidator < ActiveModel::EachValidator + def validate_each(record, _attribute, value) + browser = Browser.new(value) + return if browser.known? + + return record.data = { note: 'Detected suspicious browser' }.to_json if record.data.nil? + + record.data = JSON.parse(record.data).merge(note: 'Detected suspicious browser').to_json + end +end diff --git a/app/models/data_storage.rb b/app/models/data_storage.rb new file mode 100644 index 0000000..7277ea6 --- /dev/null +++ b/app/models/data_storage.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# Data Storage model +class DataStorage < ApplicationRecord + BLACKLISTED_TITLES = %w[document label profile phone user].freeze + acts_as_eventable prefix: 'data_storage', on: %i[create update] + + belongs_to :user + + validates :title, :data, presence: true + validates_length_of :data, maximum: 5120 # maximum 5kb of data + validates :data, data_is_json: true + validates :title, uniqueness: { scope: :user_id, case_sensitive: false }, + inclusion: { in: UserStorageTitles.list }, exclusion: { in: BLACKLISTED_TITLES } + + def as_json_for_event_api + { + user: user.as_json_for_event_api, + title: title, + data: data, + created_at: format_iso8601_time(created_at), + updated_at: format_iso8601_time(updated_at) + } + end +end + +# == Schema Information +# +# Table name: data_storages +# +# id :bigint not null, primary key +# user_id :bigint unsigned, not null +# title :string(64) not null +# data :text(65535) not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_data_storages_on_user_id_and_title (user_id,title) UNIQUE +# diff --git a/app/models/document.rb b/app/models/document.rb new file mode 100644 index 0000000..15ee47d --- /dev/null +++ b/app/models/document.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# User document model +class Document < ApplicationRecord + include Encryptable + + # acts_as_eventable prefix: 'document', on: %i[create] + + mount_uploader :upload, Barong::App.config.uploader + + enum state: { pending: 0, verified: 1, rejected: 2, replaced: 3 } + attr_encrypted :doc_number + + belongs_to :user + + validates :doc_type, :upload, presence: true + validates :doc_type, inclusion: { in: DocumentTypes.list } + validates :doc_category, inclusion: { in: DocumentTypes.category_list } + validates :doc_expire, presence: true, if: -> { Barong::App.config.required_docs_expire } + validates :metadata, data_is_json: true + validate :exist_verified + + validates :doc_number, length: { maximum: 128 }, + format: { + with: /\A[A-Za-z0-9\-\s]+\z/, + message: 'only allows letters and digits' + }, if: proc { |a| a.doc_number.present? } + + validate :doc_expire_not_in_the_past, if: -> { Barong::App.config.required_docs_expire } + before_save :start_document_kyc_verification, :save_doc_number_index + + attr_writer :update_labels + + def exist_verified + errors.add(:document, 'already have verified') if user.documents.find_by(state: 'verified', doc_type: doc_type).present? + end + + def as_json_for_event_api + { + user: user.as_json_for_event_api, + upload: CGI::escape(upload.url), + doc_type: doc_type, + doc_number: doc_number, + doc_expire: doc_expire, + metadata: metadata, + created_at: format_iso8601_time(created_at), + updated_at: format_iso8601_time(updated_at) + } + end + + def sub_masked_doc_number + doc_number.sub(/(?<=\A.{2})(.*)(?=.{2}\z)/) { |match| '*' * match.length } if doc_number + end + + private + + def start_document_kyc_verification + KycService.document_step(self) + end + + def update_labels + @update_labels.nil? ? true : @update_labels + end + + def doc_expire_not_in_the_past + return if doc_expire.blank? + + errors.add(:doc_expire, :invalid) if doc_expire < Date.current + end + + def save_doc_number_index + self.doc_number_index = SaltedCrc32.generate_hash(doc_number) if doc_number.present? + end +end + +# == Schema Information +# Schema version: 20210714111301 +# +# Table name: documents +# +# id :bigint not null, primary key +# user_id :bigint unsigned, not null +# upload :string(255) +# doc_type :string(255) +# doc_expire :date +# doc_number_encrypted :string(255) +# doc_number_index :bigint +# doc_issue :date +# doc_category :string(255) +# identificator :string(255) +# metadata :text(65535) +# created_at :datetime not null +# updated_at :datetime not null +# state :integer default("pending"), not null +# +# Indexes +# +# index_documents_on_doc_number_index (doc_number_index) +# index_documents_on_user_id (user_id) +# diff --git a/app/models/label.rb b/app/models/label.rb new file mode 100644 index 0000000..ff43c88 --- /dev/null +++ b/app/models/label.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +# Resposible for storing configurations +class Label < ApplicationRecord + # acts_as_eventable prefix: 'label', on: %i[create update] + + belongs_to :user + + SCOPES = HashWithIndifferentAccess.new(public: 'public', private: 'private') + + SCOPES.keys.each do |name| + define_method "#{name}?" do + scope == SCOPES[name] + end + end + + scope :with_private_scope, -> { where(scope: 'private') } + + validates :user_id, :key, :value, :scope, presence: true + validates :scope, + inclusion: { in: SCOPES.keys } + + validates :key, + length: 3..255, + format: { with: /\A[a-z0-9_-]+\z/ }, + uniqueness: { scope: %i[user_id scope] } + + validates :value, + length: 3..255, + format: { with: /\A[a-z0-9_-]+\z/ } + + validate :regard_kyc_steps + after_commit :update_state_if_label_defined, :update_level_if_label_defined, on: %i[create update] + after_commit :run_owner_mobile, on: %i[create update] + after_destroy :update_state_if_label_defined, :destroy_level_if_label_deleted + + before_validation :normalize_fields + + def as_json_for_event_api + { + id: id, + key: key, + value: value, + description: description, + user: user.as_json_for_event_api + } + end + + def kyc_wanted_point + point = Level.find_by(key: key) + raise "not found this key: `#{key}` in levels" unless point.present? + + [point.id, Level.sum_ex_points(point.id)] + end + + private + + def normalize_fields + self.key = key.to_s.downcase.squish + self.value = value.to_s.downcase.squish + end + + def update_state_if_label_defined + return unless scope == 'private' || previous_changes[:scope]&.include?('private') + + user.update_state + end + + def update_level_if_label_defined + return unless scope == 'private' || previous_changes[:scope]&.include?('private') + + user.update_level + # TODO must be clear witch step need notification + # send_document_review_notification if key == 'document' + end + + def destroy_level_if_label_deleted + # just kyc labels + if value == 'verified' && Level::KYC_LEVEL_KEYS.include?(key) + raise "can not delete kyc key: `#{key}`" + end + + update_level_if_label_defined + end + + # TODO: Fix it when EventAPI will be added. + def send_document_review_notification + if value == 'verified' + EventAPI.notify('system.document.verified', record: as_json_for_event_api) + elsif value == 'rejected' + EventAPI.notify('system.document.rejected', record: as_json_for_event_api) + end + end + + # level confirm by labels + # labels create from LEVEL table by key + # every key in level table has points as id + # so user must not has label out of its now level + def regard_kyc_steps + return unless scope == 'private' || previous_changes[:scope]&.include?('private') + + # just kyc labels + return unless ::BarongConfig.list['kyc_levels'].values.flatten.include?(key) + + next_level = user.level + 1 + # this means user has latest level now and dont need check it more + return if next_level > Level::LEVEL_POINTS.keys&.last&.to_i + + ask_point, ex_points = kyc_wanted_point + # check wanted point in correct area + raise 'regard steps in KYC' if (ask_point + ex_points) > Level::LEVEL_POINTS.dig(next_level.to_s) + end + + # just trigger for check owner mobile + def run_owner_mobile + return unless user.access_phone.present? + + if value == 'verified' && key == 'selfie' + KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::OwnerMobileWorker.perform_async(user.access_phone.id) + end + end +end + +# == Schema Information +# Schema version: 20210218135634 +# +# Table name: labels +# +# id :bigint not null, primary key +# user_id :bigint unsigned, not null +# key :string(255) not null +# value :string(255) not null +# scope :string(255) default("public"), not null +# description :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_labels_on_user_id (user_id) +# index_labels_on_user_id_and_key_and_scope (user_id,key,scope) +# diff --git a/app/models/level.rb b/app/models/level.rb new file mode 100644 index 0000000..549efd1 --- /dev/null +++ b/app/models/level.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Label level mapping model +class Level < ApplicationRecord + validates :key, :value, :description, presence: true + validates :value, uniqueness: { scope: :key } + + # points calculate with sum ids of record + KYC_ONE_POINTS = ::BarongConfig.list['kyc_levels'][1].keys.sum + 1 + KYC_TWO_POINTS = KYC_ONE_POINTS + ::BarongConfig.list['kyc_levels'][2].keys.sum + KYC_THREE_POINTS = KYC_TWO_POINTS + ::BarongConfig.list['kyc_levels'][3].keys.sum + LEVEL_POINTS = { '1' => KYC_ONE_POINTS, '2' => KYC_TWO_POINTS, '3' => KYC_THREE_POINTS }.freeze + LEVEL_ID_BOUNDS = { '0' => 1, + '1' => ::BarongConfig.list['kyc_levels'][1].keys.last, + '2' => ::BarongConfig.list['kyc_levels'][2].keys.last, + '3' => ::BarongConfig.list['kyc_levels'][3].keys.last}.freeze + KYC_LEVEL_IDS = (1..::BarongConfig.list['kyc_levels'].values.last.keys.last).freeze + KYC_LEVEL_KEYS = ::BarongConfig.list['kyc_levels'].values.map(&:values).flatten + + scope :sum_ex_points, ->(wanted_id) { where('id < (?)', wanted_id).sum(:id) } + + class << self + def what_level(points) + return 0 if points < LEVEL_POINTS.dig('1') + + return 1 if points < LEVEL_POINTS.dig('2') + + 2 + end + end +end + +# == Schema Information +# +# Table name: levels +# +# id :bigint not null, primary key +# key :string(255) not null +# value :string(255) +# description :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/permission.rb b/app/models/permission.rb new file mode 100644 index 0000000..14af2d8 --- /dev/null +++ b/app/models/permission.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Permissions model for RBAC +class Permission < ApplicationRecord + validates :role, :verb, :action, :path, presence: true + + before_validation :upcase_action_verb + + private + + def upcase_action_verb + return if action.blank? || verb.blank? + + self.action.upcase! + self.verb.upcase! + end +end + +# == Schema Information +# +# Table name: permissions +# +# id :bigint not null, primary key +# action :string(255) not null +# role :string(255) not null +# verb :string(255) not null +# path :string(255) not null +# topic :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_permissions_on_topic (topic) +# diff --git a/app/models/phone.rb b/app/models/phone.rb new file mode 100644 index 0000000..0e9983c --- /dev/null +++ b/app/models/phone.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +# +# Class Phone +# +class Phone < ApplicationRecord + include Encryptable + + TWILIO_CHANNELS = %w[call sms].freeze + DEFAULT_COUNTRY_CODE_COUNT = 2 + + belongs_to :user + + enum state: { pending: 0, verified: 1, rejected: 2, replaced: 3 } + enum step: { access_phone: 0, owner_phone: 1, telephone: 2 } + enum category: { mobile: 0, landline: 1 } + + attr_encrypted :number + validates :number, phone: true + + before_create :generate_code + before_validation :parse_country + before_validation :sanitize_number + + before_save :save_number_index + after_commit :start_phone_kyc_verification, on: %i[create update] + scope :verified, -> { where.not(validated_at: nil) } + + # validate :validate_phone_number + + def validate_phone_number + return errors.add(:number, 'number is not a phone') if number.match(/\A..(9)\d+/) && self.phone? + end + + #FIXME: Clean code below + class << self + def sanitize(unsafe_phone) + unsafe_phone.to_s.gsub(/\D/, '') + end + + def parse(unsafe_phone) + Phonelib.parse self.sanitize(unsafe_phone) + end + + def valid?(unsafe_phone) + parse(unsafe_phone).valid? + end + + def valid_number(unsafe_phone) + parse(unsafe_phone).national.gsub(/\s+/, "").delete_prefix("0") + end + + def international(unsafe_phone) + parse(unsafe_phone).international(false) + end + + def find_by_number(number, attrs={}) + attrs.merge!(number_index: SaltedCrc32.generate_hash(number)) + find_by(attrs) + end + + def find_by_number!(number) + find_by!(number_index: SaltedCrc32.generate_hash(number)) + end + end + + def sub_masked_number + code_count = parse_code&.length + code_count = DEFAULT_COUNTRY_CODE_COUNT unless code_count + + if number.present? + number.sub(/(?<=\A.{#{code_count}})(.*)(?=.{4}\z)/) { |match| '*' * match.length } + else + number + end + end + + def check_owner_ship + jibit = JibitService.new + response = jibit.mobile_info(self.national_number, self.user.verified_profile.national_code) + + JSON.parse(response.body).dig('matched').present? + end + + def national_number + return if number.blank? + + ::Phonelib.parse(number).national.delete(' ').rjust(11, '0') + end + + private + + def start_phone_kyc_verification + ::KycService.phone_label_update(self) + end + + def generate_code + self.code = rand.to_s[2..6] + end + + def parse_country + # data = Phonelib.parse(number) + self.country = "IR" + end + + def parse_code + data = Phonelib.parse(number) + data.country_code + end + + def sanitize_number + self.number = Phone.sanitize(number) + end + + def save_number_index + self.number_index = SaltedCrc32.generate_hash(number) if number.present? + end +end + +# == Schema Information +# Schema version: 20210425062048 +# +# Table name: phones +# +# id :bigint not null, primary key +# user_id :integer unsigned, not null +# country :string(255) not null +# code :string(5) +# number_encrypted :string(255) not null +# number_index :bigint not null +# validated_at :datetime +# created_at :datetime not null +# updated_at :datetime not null +# category :integer not null +# state :integer default("pending") +# step :integer default("access_phone") +# +# Indexes +# +# index_phones_on_number_index (number_index) +# index_phones_on_user_id (user_id) +# diff --git a/app/models/profile.rb b/app/models/profile.rb new file mode 100644 index 0000000..4b881f1 --- /dev/null +++ b/app/models/profile.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +# Profile model +# in original version user has only one drafted or submitted at time but can has many verified and rejected +# but in my version user can only one verified profile too +# only drafted profile editable, so dont send confirm param in creation or update time +class Profile < ApplicationRecord + include Encryptable + + # acts_as_eventable prefix: 'profile', on: %i[create update] + + belongs_to :user + belongs_to :city, required: false + + enum state: { drafted: 0, submitted: 1, verified: 3, rejected: 4 } + + EDITABLE_PARAMS = { drafted: %w[first_name last_name dob address postcode city country metadata national_code] }.freeze + OPTIONAL_PARAMS = %w[first_name last_name dob address postcode city country national_code].freeze + + attr_encrypted :first_name + attr_encrypted :last_name + attr_encrypted :dob + attr_encrypted :address + attr_encrypted :national_code + + validates :first_name, presence: true, + length: 1..255, + format: { + with: /\A[[:word:]\s\-']+\z/, + message: 'only allows letters, digits "-", "\'", and space' + }, + if: proc { |a| a.first_name.present? } + validates :last_name, presence: true, + length: 1..255, + format: { + with: /\A[[:word:]\s\-']+\z/, + message: 'only allows letters, digits "-", "\'", and space' + }, + if: proc { |a| a.last_name.present? } + + validates :city, presence: false + + validate :validate_country_format, if: ->(p) { p.country.present? } + validates :postcode, length: 2..255, + format: { + with: /\A[[:word:]\s\-]+\z/, + message: 'only allows letters, digits, "-" and space' + }, + if: proc { |a| a.postcode.present? } + + validates :address, length: 1..255, + format: { + with: /\A[[:word:]\s\-\–\,\.~;\/:\#"\\&\')\(]+\z/, + message: 'only allows letters, digits "-", "–", "\'", ".", ",", "#", ":", ";", "&" and space' + }, + if: proc { |a| a.address.present? } + + validates :national_code, length: { is: 10 }, + format: { with: /\A\d+\z/, message: 'Ten Integer only. No sign allowed.' } + + validates :metadata, data_is_json: true + validate :profile_state!, on: :create + # validate :profile_update!, on: :update + validate :dob_not_in_the_future + + after_commit :start_profile_kyc_verification + + scope :kept, -> { joins(:user).where(users: { discarded_at: nil }) } + + before_validation do + squish_spaces + end + + def full_name + "#{first_name} #{last_name}" + end + + def reverse_full_name + "#{last_name} #{first_name}" + end + + def sub_masked_last_name + last_name.sub(/(?<=\A.{1})(.*)/) { |match| '*' * match.length } if last_name + end + + def sub_masked_national_code + national_code.sub(/(?<=\A.{5})(.*)/) { |match| '*' * match.length } if national_code + end + + def sub_masked_dob + dob.to_s.sub(/(?<=\A.{8})(.*)/) { |match| '*' * match.length } if dob + end + + def as_json_for_event_api + { + user: user.as_json_for_event_api, + first_name: first_name, + last_name: last_name, + dob: format_iso8601_time(dob), + address: address, + postcode: postcode, + city: city, + country: country, + metadata: metadata, + created_at: format_iso8601_time(created_at), + updated_at: format_iso8601_time(updated_at) + } + end + + + private + + def validate_country_format + return if ISO3166::Country.find_country_by_alpha2(country) || + ISO3166::Country.find_country_by_alpha3(country) + + errors.add(:country, 'must have alpha2 or alpha3 format') + end + + def squish_spaces + self.first_name = first_name&.squish + self.last_name = last_name&.squish + self.postcode = postcode&.squish + end + + def profile_state! + # No limits for storing drafted and rejected profiles + # return if state.in?(%w[drafted rejected]) + return if state.in?(%w[drafted rejected]) + + # This check is actual for profile states [drafted submitted] + # User cant create submitted or verified profile, when already had one of them + user_profiles_states = user.profiles.pluck(:state) + errors.add(:state, :exists, message: 'already exists') unless (user_profiles_states & %w[submitted verified]).empty? + end + + def profile_update! + # we cant update rejected or verified profile + errors.add(:state, :exists, message: 'just edit submitted') if %w[rejected verified].include?(state_was) + end + + def start_profile_kyc_verification + KycService.profile_step(self) + end + + def dob_not_in_the_future + return if dob.nil? + + self.dob = dob.to_date + return errors.add(:dob, :invalid_format, message: 'invalid date format') unless dob.is_a?(Date) + return errors.add(:dob, :invalid, message: 'cant be in future') if dob > Date.current + end +end + +# == Schema Information +# Schema version: 20210425062048 +# +# Table name: profiles +# +# id :bigint not null, primary key +# user_id :bigint +# author :string(255) +# applicant_id :string(255) +# first_name_encrypted :string(1024) +# last_name_encrypted :string(1024) +# dob_encrypted :string(255) +# address_encrypted :string(1024) +# postcode :string(255) +# city :string(255) +# country :string(255) +# state :integer default("drafted"), unsigned +# metadata :text(65535) +# created_at :datetime not null +# updated_at :datetime not null +# national_code_encrypted :string(255) +# city_id :bigint +# +# Indexes +# +# index_profiles_on_city_id (city_id) +# index_profiles_on_user_id (user_id) +# diff --git a/app/models/province.rb b/app/models/province.rb new file mode 100644 index 0000000..7c377aa --- /dev/null +++ b/app/models/province.rb @@ -0,0 +1,21 @@ +class Province < ApplicationRecord + has_many :cities + + validates :name, presence: true, + length: 1..255, + format: { + with: /\A[[:word:]\s\-']+\z/, + message: 'only allows letters, digits "-", "\'", and space' + } +end + +# == Schema Information +# Schema version: 20210420111532 +# +# Table name: provinces +# +# id :bigint not null, primary key +# name :string(255) +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/restriction.rb b/app/models/restriction.rb new file mode 100644 index 0000000..3fcbbce --- /dev/null +++ b/app/models/restriction.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +class Restriction < ApplicationRecord + # please, note that order in CATEGORIES contstant defines the ierarchy + CATEGORIES = %w[whitelist maintenance blacklist blocklogin].freeze + SCOPES = %w[continent country ip ip_subnet all] + # 423 Locked 403 Forbidden 401 Forbidden + DEFAULT_CODES = { continent: 423, country: 423, ip_subnet: 403, ip: 401, all: 401 }.stringify_keys.freeze + STATES = %w[enabled disabled] + SUBNET_REGEX = /\A([0-9]{1,3}\.){3}[0-9]{1,3}\/([0-9]|[1-2][0-9]|3[0-2])\z/ + + validates :scope, :value, :category, presence: true + validates :scope, inclusion: { in: SCOPES } + validates :state, inclusion: { in: STATES } + validates :category, inclusion: { in: CATEGORIES } + + validates_uniqueness_of :value, scope: %i[scope category] + + validates :value, if: -> { scope == 'ip' }, + format: { :with => Resolv::IPv4::Regex } + + validates :value, if: -> { scope == 'ip_subnet' }, + format: { :with => SUBNET_REGEX } + + before_validation :assign_code + + after_save :destroy_sessions + + private + + def destroy_sessions + if category == 'blocklogin' && state == 'enabled' + Rails.cache.delete_matched('*_session_id*') if state_previously_changed? || created_at_previously_changed? + end + end + + def assign_code + return unless code.blank? || category == 'whitelist' + + self.code = category == 'maintenance' ? 471 : DEFAULT_CODES[scope] + end +end + +# == Schema Information +# +# Table name: restrictions +# +# id :bigint not null, primary key +# category :string(255) not null +# scope :string(64) not null +# value :string(64) not null +# code :integer +# state :string(16) default("enabled"), not null +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/service_account.rb b/app/models/service_account.rb new file mode 100644 index 0000000..66d31a0 --- /dev/null +++ b/app/models/service_account.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# ServiceAccount model +class ServiceAccount < ApplicationRecord + UID_PREFIX = 'SI' + + belongs_to :user, foreign_key: "owner_id", optional: true + has_many :api_keys, as: :key_holder_account, dependent: :destroy, class_name: 'APIKey' + + validate :role_exists + validate :email_n_uid_uniqueness + validates :email, email: true, presence: true, uniqueness: true + validates :uid, presence: true, uniqueness: true + + scope :active, -> { where(state: 'active') } + + after_update :disable_api_keys + before_create :assign_state, if: -> { user.present? } + # System will assign user state only if there is no changes of state during update + before_update :assign_state, if: -> { user.present? && !state_changed? } + before_validation :assign_level, if: -> { user.present? } + before_validation :assign_uid + before_validation :assign_email + + def active? + self.state == 'active' + end + + def disable_api_keys + if state_previously_changed? && state == 'disabled' + api_keys.active.each do |key| + key.update(state: 'inactive') + end + end + end + + def as_payload + as_json(only: %i[uid email role level state]) + end + + def role_exists + return if Permission.pluck(:role).include?(role) + + errors.add(:role, 'doesnt_exist') + end + + def email_n_uid_uniqueness + errors.add('email_or_uid', 'not_uniq') if User.find_by(email: email) || User.find_by(uid: uid) + end + + private + + def assign_email + return unless email.blank? + + name, domain = user.email.split('@') + self.email = "#{name}+#{self.uid}@#{domain}" + end + + def assign_uid + return unless uid.blank? + + self.uid = UIDGenerator.generate(UID_PREFIX) + end + + def assign_state + self.state = user.state + end + + def update_state + !state_changed? + end + + def assign_level + self.level = user.level + end +end + +# == Schema Information +# Schema version: 20210218135634 +# +# Table name: service_accounts +# +# id :bigint not null, primary key +# uid :string(255) not null +# owner_id :bigint unsigned +# email :string(255) not null +# role :string(255) default("service_account"), not null +# level :integer default(0), not null +# state :string(255) default("pending"), not null +# created_at :datetime not null +# updated_at :datetime not null +# diff --git a/app/models/treasury.rb b/app/models/treasury.rb new file mode 100644 index 0000000..34199a8 --- /dev/null +++ b/app/models/treasury.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +# Treasury model +class Treasury < ApplicationRecord + + belongs_to :user + + enum state: { drafted: 0, submitted: 1, processing: 2, verified: 3, rejected: 4, disable: 5 } + enum kind: { card: 0, iban: 2 } + + validates :title, uniqueness: { scope: [:user_id, :kind], case_sensitive: false }, allow_blank: true + + validates :data, length: { is: 16 }, if: -> { card? }, allow_blank: false, + format: { with: /\A\d+\z/, message: 'Sixteen Integer only. No sign allowed.' } + validates :data, uniqueness: true, length: { is: 26 }, if: -> { iban? }, allow_blank: false, + format: { with: /\AIR\d+\z/i, message: 'Twenty-four Integer only. No sign allowed.' } + + validate :data, :one_data_verified? + + validates :title, presence: true + + after_commit :start_treasury_kyc_verification, on: %i[create update] + before_update :check_at_least_one_verified, if: -> { state == 'disable' } + before_destroy :check_at_least_one_verified + + + scope :active, -> { where.not(state: 'disable') } + + def one_data_verified? + errors.add(:data, 'this card is already verified') if Treasury.find_by(data: data, state: 'verified').present? + end + + def sub_masked_data + data.sub(/(?<=\A.{6})(.*)/) { |match| '*' * match.length } if data + end + + # update without callback + def ownership! + if ownership + update_column(:state, 'verified') + else + update_column(:state, 'rejected') + end + end + + def ownership + # full_name = if kind == 'card' + # user.profiles.last.reverse_full_name.delete(' ') + # else + # user.profiles.last.full_name.delete(' ') + # end + return unless user.verified_profile.present? + + full_name = user.verified_profile.full_name.delete(' ') + owner_name.delete(' ').percent_match(full_name) >= 80 + end + + def jresult + return nil unless result.present? + + JSON.parse(result) + end + + def old_owner_name + return nil unless jresult.present? + + kind == 'iban' ? "#{jresult.dig('firstName')} #{jresult.dig('lastName')}" : jresult.dig('ownerName') + end + + def owner_name + return unless jresult.present? + + if kind == 'iban' + # JIBIT return array in names of owner + names = jresult.dig('ibanInfo', 'owners')&.last + "#{names.dig('firstName')} #{names.dig('lastName')}" + else + jresult.dig('cardInfo', 'ownerName') + end + end + + def disable + update(state: 'disable') + end + + def enable + update(state: 'submitted') + end + + private + + def start_treasury_kyc_verification + ::KycService.treasury_label_update(self) + end + + def check_at_least_one_verified + # return if state_was == 'verified' + + raise 'verified treasury cant be deleted' if state == 'verified' + + verified_treasuries = Treasury.where(user: user, kind: kind, state: 'verified') + raise 'at least one verified treasury' if verified_treasuries.present? && verified_treasuries.count <= 1 + + # delete Corresponding label if not verified + treasury_label = user.labels.find_by(key: kind) + treasury_label.destroy unless treasury_label.value == 'verified' + end + +end + +# == Schema Information +# Schema version: 20210811064241 +# +# Table name: treasuries +# +# id :bigint not null, primary key +# user_id :bigint unsigned, not null +# title :string(128) +# data :string(255) not null +# result :text(65535) +# state :integer default("drafted"), unsigned, not null +# kind :integer default("card"), unsigned, not null +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_treasuries_on_user_id (user_id) +# diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..46644d0 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,287 @@ +# frozen_string_literal: true + +# User model +class User < ApplicationRecord + acts_as_eventable prefix: 'user', on: %i[create update] + + include ModelCaching + CHANGE_PASS_ATTEMPTS = 3 + has_secure_password + + has_many :profiles, dependent: :destroy + has_many :phones, dependent: :destroy + has_many :data_storages, dependent: :destroy + has_many :comments, dependent: :destroy + has_many :documents, dependent: :destroy + has_many :labels, dependent: :destroy + has_many :activities, dependent: :destroy + has_many :service_accounts, dependent: :destroy, foreign_key: 'owner_id' + has_many :api_keys, dependent: :destroy, as: :key_holder_account, class_name: 'APIKey' + has_many :treasuries, dependent: :destroy + + validates_length_of :data, maximum: 1024 + validate :role_exists + validate :referral_exists + validates :data, data_is_json: true + validates :email, email: true, presence: true, uniqueness: true + validates :uid, presence: true, uniqueness: true + validates :password, presence: true, if: :should_validate? + validate :validate_pass! + + scope :active, -> { where(state: 'active') } + scope :with_pending_or_replaced_docs, -> { self.joins(:labels).where(labels: + { key: 'document', value: ['pending', 'replaced'], scope: 'private' }) } + + before_validation :assign_uid + before_validation :generate_password, on: :create + after_update :disable_api_keys + after_update :disable_service_accounts + before_save :notfiy_new_level, if: :level_changed? + before_save :notify_changed_otp, if: :otp_changed? + + def notfiy_new_level + ::AMQP::Queue.enqueue_event("private" , self.uid, 'kyc', {msg: "new level is #{level}"}) + end + + def notify_changed_otp + ::AMQP::Queue.enqueue_event("private" , self.uid, 'kyc', {msg: "otp state is #{otp}"}) + end + + def generate_password + self.password = SecureRandom.base64(30) unless password + end + + def validate_pass! + return unless (new_record? && password.present?) || password.present? + + validation_result = PasswordStrengthChecker.validate!(password) + errors.add(:password, validation_result) unless validation_result == 'strong' + end + + def disable_service_accounts + if state != 'active' + service_accounts.each do |account| + account.update(state: state) + end + end + end + + def disable_api_keys + if otp_previously_changed? && otp == false || state_previously_changed? && state != 'active' + service_accounts.each do |service_account| + service_account.api_keys.active.each do |key| + key.update(state: 'inactive') + end + end + api_keys.active.each do |key| + key.update(state: 'inactive') + end + end + end + + def active? + self.state == 'active' + end + + def superadmin? + self.role == 'superadmin' + end + + def role_exists + return if Permission.pluck(:role).include?(role) + + errors.add(:role, 'doesnt_exist') + end + + # Check if refferal exist for assignment + def referral_exists + errors.add(:referral_id, 'doesnt_exist') if referral_id.present? && User.find_by(id: referral_id).blank? + end + + def referral_uid + user = User.find_by(id: referral_id) + return if user.nil? + + user.uid + end + + def role + super.inquiry + end + + def should_validate? + new_record? || password.present? + end + + def kyc_info + user_points = 0 + last_level = 0 + tags = labels.with_private_scope.map { |l| [l.key, l.value].join ':' } + levels = Level.all.order(id: :asc) + levels.each do |lvl| + # lvl.value always == 'verified' + # so just verified label accepted + break unless tags.include?(lvl.key + ':' + lvl.value) + + user_points += lvl.id + last_level = lvl.id + end + { points: user_points.to_i, step: last_level.to_i, level: ::Level.what_level(user_points).to_i } + end + + def update_level + update(level: Level.what_level(kyc_info.dig(:points))) unless level == kyc_info.dig(:level) + end + + def update_state + @resulting_state = 'pending' + + # check if user has all required labels for activation + @resulting_state = 'active' if labels_include?(BarongConfig.list['activation_requirements']) + + # FIXME BarongConfig should be a feature of Barong::App + BarongConfig.list['state_triggers']&.each do |state, triggers| + triggers.each { |trigger| + # #TODO please fix it + # if my key level is bank : user become banned + labels.pluck(:key).each { |label| @resulting_state = state if label.start_with?(trigger) } + } + end + update(state: @resulting_state) if @resulting_state != self.state + end + + # check if given key: values hash is a subset of private user labels + def labels_include?(labels_hash) + labels_hash <= private_labels_to_hash + end + + # Select all key-value pairs from user labels with private scope, merge in one hash + def private_labels_to_hash + key_value_arr = self.labels.with_private_scope.map do + |l| { l.key => l.value } + end + key_value_hash = key_value_arr.inject(:merge) + key_value_hash || {} + end + + def as_json_for_event_api + { + uid: uid, + email: email, + role: role, + level: level, + otp: otp, + state: state, + referral_uid: referral_uid, + created_at: format_iso8601_time(created_at), + updated_at: format_iso8601_time(updated_at) + } + end + + def as_payload + referral_uid = self.class.find_by(referral_id: referral_id).uid if referral_id.present? + as_json(only: %i[uid email role level state otp]).merge( + 'referral_uid' => referral_uid, + 'last_change_pass' => change_pass_time, + 'cards' => cards&.pluck('data'), + 'ibans' => ibans&.pluck('data') + ) + end + + def language + if data.blank? + Barong::App.config.default_language.upcase + else + JSON.parse(data)['language'].upcase || Barong::App.config.default_language.upcase + end + end + + def verified_profile + profiles&.find_by(state: 'verified') + end + + def submitted_profile + profiles&.find_by(state: 'submitted') + end + + def drafted_profile + profiles&.where(state: 'drafted').last + end + + def front_icard + documents.where(doc_type: 'Identity card', doc_category: 'front_side')&.last + end + + def selfie + documents.where(doc_type: 'Selfie', doc_category: 'front_side')&.last + end + + def cards + treasuries.where(kind: :card, state: :verified) + end + + def ibans + treasuries.where(kind: :iban, state: :verified) + end + + def submitted_treasuries + treasuries&.where(state: 'submitted') + end + + def last_password_activity + activities.where(action: ['password reset', 'password change'], result: 'succeed', topic: 'password') + .order('created_at DESC')&.last + end + + def change_pass_time + last_password_activity&.created_at + end + + def access_phone + phones.where(step: 'access_phone', category: 'mobile')&.last + end + + def mobile(state = nil) + return phones.where(category: 'mobile').last unless state.present? + + phones.where(category: 'mobile', state: state)&.last + end + + def landline + return phones.where(category: 'landline').last unless step.present? + + phones.where(category: 'landline', step: step).last + end + + private + + def assign_uid + return unless uid.blank? + + self.uid = UIDGenerator.generate(Barong::App.config.uid_prefix) + end +end + +# == Schema Information +# Schema version: 20210425062048 +# +# Table name: users +# +# id :bigint not null, primary key +# uid :string(255) not null +# email :string(255) not null +# password_digest :string(255) not null +# role :string(255) default("member"), not null +# data :text(65535) +# level :integer default(0), not null +# otp :boolean default(FALSE) +# state :string(255) default("pending"), not null +# referral_id :bigint +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_users_on_email (email) UNIQUE +# index_users_on_uid (uid) UNIQUE +# diff --git a/app/services/api_keys_verifier.rb b/app/services/api_keys_verifier.rb new file mode 100644 index 0000000..7848682 --- /dev/null +++ b/app/services/api_keys_verifier.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class APIKeysVerifier + def initialize(params = {}) + @kid = params[:kid] + @signature = params[:signature] + @nonce = params[:nonce] || nil + @api_key = APIKey.find_by!(kid: @kid) + end + + def verify_hmac_payload? + data = @nonce.to_s + @kid + algorithm = 'SHA' + @api_key.algorithm[2..4] + true_signature = OpenSSL::HMAC.hexdigest(algorithm, @api_key.secret, data) + true_signature == @signature + end +end diff --git a/app/services/captcha_service.rb b/app/services/captcha_service.rb new file mode 100644 index 0000000..86c6468 --- /dev/null +++ b/app/services/captcha_service.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'digest' +require 'net/http' + +module CaptchaService + # Google recaptcha verifier + class RecaptchaVerifier + include Recaptcha::Adapters::ControllerMethods + + attr_reader :request + + def initialize(request:) + @request = request + end + + def response_valid?(skip_remote_ip:, response:) + # according to changes in v2 support https://github.com/ambethia/recaptcha#recaptcha-v2-api-and-usage + # method comes from Recaptcha::Adapters::ControllerMethods + # https://github.com/ambethia/recaptcha/blob/master/lib/recaptcha/adapters/controller_methods.rb#L10 + verify_recaptcha(secret_key: Barong::App.config.recaptcha_secret_key, + model: User, + skip_remote_ip: skip_remote_ip, + response: response) + end + end + + # Geetest.com captcha verifier + class GeetestVerifier + def initialize + @api = 'http://api.geetest.com' + @validate_path = '/validate.php' + @register_path = '/register.php' + @geetest_id = Barong::App.config.geetest_id + @geetest_key = Barong::App.config.geetest_key + end + + def validate(response) + md5 = Digest::MD5.hexdigest(@geetest_key + 'geetest' + response['geetest_challenge']) + if response['geetest_validate'] == md5 + back = begin + post(@api + @validate_path, seccode: response['geetest_seccode']) + rescue StandardError + '' + end + return back == Digest::MD5.hexdigest(response['geetest_seccode']) + end + false + end + + def register + challenge = get(@api + @register_path + "?gt=#{@geetest_id}") + { gt: @geetest_id, challenge: Digest::MD5.hexdigest(challenge + @geetest_key) } + rescue StandardError + '' + end + + def get(uri) + Net::HTTP.get_response(URI(uri)).body + end + + def post(uri, data) + Net::HTTP.post_form(URI(uri), data).body + end + end +end diff --git a/app/services/encryption_service.rb b/app/services/encryption_service.rb new file mode 100644 index 0000000..b5b5211 --- /dev/null +++ b/app/services/encryption_service.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +class EncryptionService + # Example: 202040 + # Week number starts from 0 + def self.current_salt + Time.now.strftime('%Y%W') + end + + def self.pack(salt, value) + [salt, value].join('.') + end + + def self.unpack(str) + raise "Invalid encrypted value: #{str}" unless str =~ (/(\w*)\.(.*)/) + [$1, $2] + end + + def self.encrypt(value) + # Get current salt + salt = current_salt + # Get or generate new master key from salt + current_key = get_master_key(salt) + # Encrypt attribute value + encrypted_key = encryptor(current_key).encrypt_and_sign(value) + # Add salt before encrypted value + pack(salt, encrypted_key) + end + + def self.decrypt(value) + # Unpack salt and encrypted_key + salt, encrypted_key = unpack(value) + # Get master key from salt + master_key = get_master_key(salt) + # Decrypt encrypted value for attribute + encryptor(master_key).decrypt_and_verify(encrypted_key) + end + + private + + def self.encryptor(key) + ActiveSupport::MessageEncryptor.new(key) + end + + def self.get_master_key(salt) + @cache ||= {} + # Delete keys with expired date + delete_expired_keys + + unless @cache[salt] + # Initialize hash for specific salt + @cache[salt] = {} + + # Put key value from key generator + @cache[salt]['key'] = ActiveSupport::KeyGenerator.new( + ENV.fetch('SECRET_KEY_BASE') + ).generate_key(salt, ActiveSupport::MessageEncryptor.key_len) + + # Put expire date for specific key + @cache[salt]['expire_date'] = 1.week.from_now + end + + @cache[salt]['key'] + end + + def self.delete_expired_keys + return unless @cache.is_a?(Hash) + + # Iterate through all @cache values + @cache.each do |salt, values| + # Delete key if expire date expired + @cache.delete(salt) if values['expire_date'].present? && values['expire_date'] < Time.now + end + end +end diff --git a/app/services/event_mailer.rb b/app/services/event_mailer.rb new file mode 100644 index 0000000..1c9df27 --- /dev/null +++ b/app/services/event_mailer.rb @@ -0,0 +1,219 @@ +# frozen_string_literal: true + +require 'bunny' +require 'ostruct' + +class EventMailer + Error = Class.new(StandardError) + + class VerificationError < Error; end + + def initialize(events, exchanges, keychain) + @exchanges = exchanges + @keychain = keychain + @events = events + + Kernel.at_exit { unlisten } + end + + def call + listen + end + + private + + def listen + unlisten + + @bunny_session = Bunny::Session.new(rabbitmq_credentials).tap do |session| + session.start + Kernel.at_exit { session.stop } + end + + @bunny_channel = @bunny_session.channel + # Delete old queue if some exists + @bunny_channel.queue_delete('barong.postmaster.event.mailer') if @bunny_session.queue_exists?('barong.postmaster.event.mailer') + + # Define fanout exchanges which will broadcast + # all the messages they receives to all the queues they know + retry_exchange = @bunny_channel.fanout('barong.event.mailer.retry.exchange') + main_exchange = @bunny_channel.fanout('barong.event.mailer.main.exchange') + + queue = @bunny_channel.queue('barong.event.mailer.main', auto_delete: false, durable: true, + arguments: { + :'x-dead-letter-exchange' => retry_exchange.name, + } + ) + queue.bind(main_exchange) + + retry_queue = @bunny_channel.queue('barong.event.mailer.retry', auto_delete: false, durable: true, + arguments: { + :'x-dead-letter-exchange' => main_exchange.name, + :'x-message-ttl' => 120000 # will trigger retry every 2 minutes + }) + retry_queue.bind(retry_exchange) + + @events.each do |event| + exchange_name = @exchanges[event[:exchange].to_sym][:name] + exchange = @bunny_channel.direct(exchange_name) + + queue.bind(exchange, routing_key: event[:key]) + end + + Rails.logger.info { 'Listening for events.' } + queue.subscribe(manual_ack: true, block: true, &method(:handle_message)) + end + + def unlisten + if @bunny_session || @bunny_channel + Rails.logger.info { 'No longer listening for events.' } + end + + @bunny_channel&.work_pool&.kill + @bunny_session&.stop + ensure + @bunny_channel = nil + @bunny_session = nil + end + + def algorithm_verification_options(signer) + { algorithms: @keychain[signer][:algorithm] } + end + + def jwt_public_key(signer) + OpenSSL::PKey.read(Base64.urlsafe_decode64(@keychain[signer][:value])) + end + + def rabbitmq_credentials + if Barong::App.config.event_api_rabbitmq_url.present? + Barong::App.config.event_api_rabbitmq_url + else + { + host: Barong::App.config.event_api_rabbitmq_host, + port: Barong::App.config.event_api_rabbitmq_port, + username: Barong::App.config.event_api_rabbitmq_username, + password: Barong::App.config.event_api_rabbitmq_password + } + end + end + + def handle_message(delivery_info, _metadata, payload) + Rails.logger.info { "Start handling a message" } + Rails.logger.info { "\nPayload: \n #{payload} \n\n Metadata: \n #{_metadata} \n\n Delivery info: \n #{delivery_info} \n" } + exchange = @exchanges.select { |_, ex| ex[:name] == delivery_info[:exchange] } + + # In case of retry message + # we should get exchange name from _metadata info + if exchange.empty? + exchange_name = _metadata[:headers]['x-death'][1]['exchange'] + exchange = @exchanges.select { |_, ex| ex[:name] == exchange_name } + end + + exchange_id = exchange.keys.first.to_s + signer = exchange[exchange_id.to_sym][:signer] + + result = verify_jwt(payload, signer.to_sym) + + raise VerificationError, "Failed to verify signature from #{signer}." \ + unless result[:verified].include?(signer.to_sym) + + config = @events.select do |event| + event[:key] == delivery_info[:routing_key] && + event[:exchange] == exchange_id + end.first + + event = result[:payload].fetch(:event) + obj = JSON.parse(event.to_json, object_class: OpenStruct) + + user = User.includes(:profiles).find_by(uid: obj.record.user.uid) + language = user.language.downcase.to_sym + Rails.logger.info { "User #{user.email} has '#{language}' email language" } + template_config = config[:templates].transform_keys(&:downcase) + + unless template_config.keys.include?(language) + Rails.logger.error { "Language #{language} is not supported. Skipping." } + return + end + + if config[:expression].present? && skip_event(event, config[:expression]) + Rails.logger.info { "Event #{obj.name} skipped" } + return + end + + params = { + logo: Barong::App.config.smtp_logo_link, + subject: template_config[language][:subject], + template_name: template_config[language][:template_path], + record: obj.record, + changes: obj.changes, + user: user + } + + Postmaster.process_payload(params).deliver_now + + # Acknowledges a message + # Acknowledged message is completely removed from the queue + @bunny_channel.ack(delivery_info.delivery_tag) + rescue StandardError => e + Rails.logger.error { e.inspect } + + if e.is_a?(JWT::ExpiredSignature) || e.is_a?(JWT::VerificationError) || e.is_a?(VerificationError) + # Acknowledges a message + @bunny_channel.ack(delivery_info.delivery_tag) + else + # Rejects a message + # A rejected message dropped by RabbitMQ and goes to dead letter exchange queue + @bunny_channel.reject(delivery_info.delivery_tag) + end + + unlisten if db_connection_error?(e) + end + + def verify_jwt(payload, signer) + options = algorithm_verification_options(signer) + JWT::Multisig.verify_jwt JSON.parse(payload), { signer => jwt_public_key(signer) }, + options.compact + end + + def skip_event(event, expression) + # valid operators: and / or / not + operator = expression.keys.first.downcase + # { field_name: field_value } + values = expression[operator] + + # return array of boolean [false, true] + res = values.keys.map do |field_name| + safe_dig(event, field_name.to_s.split('.')) == values[field_name] + end + + # all? works as AND operator, any? works as OR operator + return false if (operator == :and && res.all?) || (operator == :or && res.any?) || + (operator == :not && !res.all?) + + return true if operator == :not && res.all? + + true + end + + def db_connection_error?(exception) + exception.is_a?(Mysql2::Error::ConnectionError) || exception.cause.is_a?(Mysql2::Error) + end + + def safe_dig(hash, keypath, default = nil) + stringified_hash = JSON.parse(hash.to_json) + stringified_keypath = keypath.map(&:to_s) + + stringified_keypath.reduce(stringified_hash) do |accessible, key| + return default unless accessible.is_a? Hash + return default unless accessible.key? key + + accessible[key] + end + end + + class << self + def call(*args) + new(*args).call + end + end +end diff --git a/app/services/jibit_service.rb b/app/services/jibit_service.rb new file mode 100644 index 0000000..939c7c8 --- /dev/null +++ b/app/services/jibit_service.rb @@ -0,0 +1,110 @@ +class JibitService + + attr_reader :base_url + attr_reader :api_key + attr_reader :api_secret + attr_reader :conn + attr_accessor :response + attr_accessor :access_token + attr_accessor :refresh_token + attr_accessor :called_method + + # one day + JIBIT_EXPIRE_TIME = 86400 + def initialize + @base_url = Barong::App.config.jibit_api_url + @api_key = Barong::App.config.jibit_api_key + @api_secret = Barong::App.config.jibit_api_secret + @conn = Faraday.new(base_url) + conn.headers['Content-Type'] = 'application/json' + @access_token = read_access_token + end + + def retry_options + { max: 5, + retry_statuses: [401, 409, 500] } + end + + def read_access_token + return Rails.cache.read('jibit_access_token') if Rails.cache.read('jibit_access_token').present? + + generate_access_token + + raise 'please try again(jibit not create token)' unless access_token.present? + + Rails.cache.write('jibit_access_token', access_token, expires_in: JIBIT_EXPIRE_TIME) + access_token + end + + def generate_access_token + @called_method = __method__.to_s + @response = conn.post('tokens/generate') do |req| + req.body = { apiKey: api_key, secretKey: api_secret }.to_json + end + @access_token, @refresh_token = parse('accessToken', 'refreshToken') + end + + def generate_refresh_token + @called_method = __method__.to_s + @response = conn.post('tokens/refresh') do |req| + req.body = { accessToken: access_token, refreshToken: refresh_token }.to_json + end + @access_token, @refresh_token = parse('accessToken', 'refreshToken') + end + + def old_iban_info(iban) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.post('services/ibanInfo') do |req| + req.body = { iban: iban }.to_json + end + end + + def iban_info(iban) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.get('ibans') do |req| + req.params[:value] = iban + end + end + + def old_card_info(card_number) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.post('services/cardInfo') do |req| + req.body = { cardNumber: card_number }.to_json + end + end + + def card_info(card_number) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.get('cards') do |req| + req.params[:number] = card_number + end + end + + def old_mobile_info(number, national_code) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.post('services/matchNationalCodeAndMobileNumber') do |req| + req.body = { nationalCode: national_code, mobile: number }.to_json + end + end + + def mobile_info(number, national_code) + @called_method = __method__.to_s + conn.authorization :Bearer, access_token + @response = conn.get('services/matching') do |req| + req.params[:nationalCode] = national_code + req.params[:mobileNumber] = number + end + end + + private + + def parse(*keys) + parsed_data = JSON.parse(response.body) + keys.map { |key| parsed_data.dig(key) } + end +end diff --git a/app/services/kave_negar_sms_service.rb b/app/services/kave_negar_sms_service.rb new file mode 100644 index 0000000..1add7cc --- /dev/null +++ b/app/services/kave_negar_sms_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# kavenegar sms sender +class KaveNegarSmsService + class << self + def send_confirmation(user) + Rails.logger.info("Sending SMS to #{user.phones.last.number}") + totp = TOTPServiceAction.new('register-mobile') + totp.create(user.uid, user.email, period: '120') + user.write_cache('register_mobile', 'true', 120) + send_sms(user.mobile('pending').number.to_s, totp.read_code(user.uid)) + end + + def send_call_confirmation(user) + Rails.logger.info("Sending call to #{user.phones.find_by(category: 'landline').number}") + totp = TOTPServiceAction.new('register-phone') + totp.create(user.uid, user.email, period: '120') + user.write_cache('register_phone', 'true', 120) + send_call(user.phones.find_by(category: 'landline').number.to_s, totp.read_code(user.uid)) + end + + def send_call(number, code) + client = KaveRestApi::Lookup.new({ receptor: number, + token: code, + template: 'phone', + type: 'call' # or call + }) + + client.call.valid? ? true : false + end + + def send_sms(number, code) + client = KaveRestApi::Lookup.new({ receptor: number, + token: code, + template: 'mobile', + type: 'sms' # or call + }) + + client.call.valid? ? true : false + end + + end +end diff --git a/app/services/kyc_service.rb b/app/services/kyc_service.rb new file mode 100644 index 0000000..caa4931 --- /dev/null +++ b/app/services/kyc_service.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class KycService + # passport front + selfie OR driver license front and back + selfie IR id card front and back + selfie + REQUIRED_DOC_AMOUNT = { 'Passport': 2, 'Driver license': 3, 'Identity card': 3 }.freeze + + class << self + def profile_step(profile) + user = profile.user + profile_label = user.labels.find_by(key: :profile) + if profile_label.nil? # first profile everw + user.labels.create(key: :profile, value: profile.state, scope: :private) + else + profile_label.update(value: profile.state) # re-submitted profile + end + return unless profile.state == 'verified' + + # verify treasury after profile verified + KYC.const_get(Barong::App.config.kyc_provider.capitalize, false)::TreasuryWorker.perform_async(profile.id) + end + + def document_step(document) + user = document.user + user_document_label = user.labels.find_by(key: document.doc_type.downcase) + if user_document_label.nil? # first document ever + user.labels.create(key: document.doc_type.downcase, value: document.state, scope: :private) + else + return if user_document_label.value == 'verified' + + user_document_label.update(value: document.state) # re-submitted document + end + end + + + def kycaid_callback(params) + 422 + end + + def treasury_label_update(treasury) + user = treasury&.user + return unless user.present? + + treasury_label = user.labels.find_by(key: treasury.kind) + if treasury_label.nil? # first profile ever + user.labels.create(key: treasury.kind, value: treasury.state, scope: :private) + else + treasury.ownership! if treasury.state == 'processing' && treasury.result.present? + # user must have just one kind of treasury and new treasury like card, must not change before verified card + treasury_label.update(value: treasury.reload.state) unless treasury_label.value == 'verified' + end + end + + def phone_label_update(phone) + user = phone&.user + return unless user.present? + + phone_label = user.labels.find_by(key: phone.step) + if phone_label.nil? + user.labels.create(key: phone.step, value: phone.state, scope: :private) + else + phone_label.update(value: phone.state) + end + end + end +end diff --git a/app/services/mock_phone_verify_service.rb b/app/services/mock_phone_verify_service.rb new file mode 100644 index 0000000..887d08f --- /dev/null +++ b/app/services/mock_phone_verify_service.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# twilio sms sender +class MockPhoneVerifyService + class << self + def send_confirmation(phone, _channel) + Rails.logger.info("Sending SMS to #{phone.number}") + + send_sms(number: phone.number, + content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code)) + end + + def send_sms(number:, content:) + from_phone = Barong::App.config.twilio_phone_number + client = Barong::MockSMS.new('', '') + client.messages.create(from: from_phone, to: '+' + number, body: content) + end + + # always return true + def verify_code?(number:, code:, user:) + user.phones.find_by_number(number).present? + end + end +end diff --git a/app/services/password_strength_checker.rb b/app/services/password_strength_checker.rb new file mode 100644 index 0000000..cd66a6d --- /dev/null +++ b/app/services/password_strength_checker.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# password entropy calculation +class PasswordStrengthChecker + class < error + Rails.logger.error { error } + raise Error, error.message + end + + def read(key) + with_human_error do + Vault.logical.read(key) + end + end + + def write!(key, params) + with_human_error do + Vault.logical.write(key, value: params) + end + end + + def delete!(key) + with_human_error do + Vault.logical.delete(key) + end + end + end +end diff --git a/app/services/totp_service.rb b/app/services/totp_service.rb new file mode 100644 index 0000000..caf3f6d --- /dev/null +++ b/app/services/totp_service.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +class TOTPService + Error = Class.new(StandardError) + + class < e + Rails.logger.error { e } + + raise Error, '2FA server is under maintenance' if e.message.include?('connection refused') + + raise Error, 'This code was already used. Wait until the next time period' if e.message.include?('code already used') + + raise e + end + + def totp_key(uid) + "totp/keys/#{Vault.application}_#{uid}" + end + + def totp_code_key(uid) + "totp/code/#{Vault.application}_#{uid}" + end + + def read_data(key) + with_human_error do + vault.read(key) + end + end + + def read_code(uid) + read_data(totp_code_key(uid)).data[:code] + end + + + def write_data(key, params) + with_human_error do + vault.write(key, params) + end + end + + def delete_data(key) + with_human_error do + vault.delete(key) + end + end + + def vault + Vault.logical + end + end +end diff --git a/app/services/totp_service_action.rb b/app/services/totp_service_action.rb new file mode 100644 index 0000000..1f80f42 --- /dev/null +++ b/app/services/totp_service_action.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +class TOTPServiceAction + Error = Class.new(StandardError) + + ISSUER_NAME = 'Barong' + attr_reader :action + + def initialize(action) + raise 'Please determine action for totp code' if action.blank? + + @action = action + end + + def server_available? + read_data('sys/health').present? + rescue StandardError + false + end + + def otp_secret(otp) + CGI.parse(URI.parse(otp.data[:url]).query)['secret'][0] + end + + def safe_create(uid, email) + return if exist?(uid) + + create(uid, email) + end + + def create(uid, email, period: '120') + write_data(totp_key(uid), + generate: true, + issuer: ::Barong::App.config.app_name, + period: period, account_name: email, qr_size: 300) + end + + def exist?(uid) + read_data(totp_key(uid)).present? + end + + def validate?(uid, code) + return false unless exist?(uid) + + result = write_data(totp_code_key(uid), code: code) + return false unless result + + result.data[:valid] + end + + def safe_validate?(uid, code) + return false unless exist?(uid) + + code == read_code(uid) + end + + + def delete(uid) + delete_data(totp_key(uid)) + end + + def with_human_error + raise ArgumentError, 'Block is required' unless block_given? + + yield + rescue Vault::VaultError => e + Rails.logger.error { e } + + false + end + + def totp_key(uid) + "totp/keys/#{Vault.application}_#{@action}_#{uid}" + end + + def totp_code_key(uid) + "totp/code/#{Vault.application}_#{@action}_#{uid}" + end + + def read_data(key) + with_human_error do + vault.read(key) + end + end + + def read_code(uid) + read_data(totp_code_key(uid)).data[:code] + end + + def write_data(key, params) + with_human_error { vault.write(key, params) } + end + + def delete_data(key) + with_human_error do + vault.delete(key) + end + end + + def vault + Vault.logical + end + +end diff --git a/app/services/twilio_sms_send_service.rb b/app/services/twilio_sms_send_service.rb new file mode 100644 index 0000000..67be41e --- /dev/null +++ b/app/services/twilio_sms_send_service.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# twilio sms sender +class TwilioSmsSendService + class << self + def send_confirmation(phone, _channel) + Rails.logger.info("Sending SMS to #{phone.number}") + + send_sms(number: phone.number, + content: Barong::App.config.sms_content_template.gsub(/{{code}}/, phone.code)) + end + + def send_sms(number:, content:) + from_phone = Barong::App.config.twilio_phone_number + client = Barong::App.config.twilio_client + client.messages.create( + from: from_phone, + to: '+' + number, + body: content + ) + end + + # returns true if given code matches number in DB + def verify_code?(number:, code:, user:) + user.phones.find_by_number(number, code: code) + end + end +end diff --git a/app/services/twilio_verify_service.rb b/app/services/twilio_verify_service.rb new file mode 100644 index 0000000..0f380c8 --- /dev/null +++ b/app/services/twilio_verify_service.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# twilio process verification +class TwilioVerifyService + class << self + def send_confirmation(phone, channel) + Rails.logger.info("Sending code to #{phone.number} via #{channel}") + + send_code(number: phone.number, channel: channel) + end + + def send_code(number:, channel:) + verify_client.services(@service_sid) + .verifications + .create(to: '+' + number, channel: channel) + end + + # return true if twilio accepts given code for the given number + def verify_code?(number:, code:, user:) + status = verify_client.services(@service_sid) + .verification_checks + .create(to: '+' + number, code: code) + .status + + status == 'approved' + end + + def verify_client + client = Barong::App.config.twilio_client + @service_sid = Barong::App.config.twilio_service_sid + + client.verify + end + end +end diff --git a/app/services/uid_generator.rb b/app/services/uid_generator.rb new file mode 100644 index 0000000..20e700d --- /dev/null +++ b/app/services/uid_generator.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class UIDGenerator + def self.generate(prefix = 'ID') + loop do + uid = "%s%s" % [prefix.upcase, SecureRandom.hex(5).upcase] + return uid if User.where(uid: uid).empty? && ServiceAccount.where(uid: uid).empty? + end + end +end diff --git a/app/uploaders/ali_uploader.rb b/app/uploaders/ali_uploader.rb new file mode 100644 index 0000000..9f49144 --- /dev/null +++ b/app/uploaders/ali_uploader.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'aliyun/oss' + +class AliUploader < UploadUploader + def url + bucket.object_url(Barong::App.config.storage_bucket_name + '/' + path) + end + + def bucket + @_bucket = client.get_bucket(Barong::App.config.storage_bucket_name) + end + + def client + @_client ||= Aliyun::OSS::Client.new( + endpoint: "oss-#{Barong::App.config.storage_region}.aliyuncs.com", + access_key_id: Barong::App.config.storage_access_key, + access_key_secret: Barong::App.config.storage_secret_key, + ) + end +end diff --git a/app/uploaders/upload_uploader.rb b/app/uploaders/upload_uploader.rb new file mode 100644 index 0000000..840c3ab --- /dev/null +++ b/app/uploaders/upload_uploader.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# It's for upload document for Document model +class UploadUploader < CarrierWave::Uploader::Base + # if Rails.env.production? + # storage :fog + # else + # storage :file + # end + + # TODO just for alpha is file + storage :file + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end + + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_whitelist + Barong::App.config.upload_extension_whitelist + end + + def size_range + # default is 1..10.megabytes + Barong::App.config.upload_size_min_range..Barong::App.config.upload_size_max_range.megabytes + end + + # Override default 'publicly visible' policy of fog + def fog_public + false # (default is true, which is not recommended for KYC documents or any user info + end + + # Set the expire time of authentification signature + def fog_authenticated_url_expiration + Barong::App.config.upload_auth_url_expiration.minutes # (default is 1.minute) + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 0000000..37813fb --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,15 @@ + + + + Barong + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag 'application', media: 'all' %> + <%= javascript_include_tag 'application' %> + + + + <%= yield %> + + diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 0000000..c104d40 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,127 @@ + + + + + + + + + <%= yield %> + + + diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb new file mode 100644 index 0000000..37f0bdd --- /dev/null +++ b/app/views/layouts/mailer.text.erb @@ -0,0 +1 @@ +<%= yield %> diff --git a/app/views/postmaster/deposit_accepted.en.html.erb b/app/views/postmaster/deposit_accepted.en.html.erb new file mode 100644 index 0000000..fce3b35 --- /dev/null +++ b/app/views/postmaster/deposit_accepted.en.html.erb @@ -0,0 +1,55 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/email_confirmation.en.html.erb b/app/views/postmaster/email_confirmation.en.html.erb new file mode 100644 index 0000000..6407e59 --- /dev/null +++ b/app/views/postmaster/email_confirmation.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/email_confirmation.ru.html.erb b/app/views/postmaster/email_confirmation.ru.html.erb new file mode 100644 index 0000000..723d918 --- /dev/null +++ b/app/views/postmaster/email_confirmation.ru.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/email_confirmation_code.en.html.erb b/app/views/postmaster/email_confirmation_code.en.html.erb new file mode 100644 index 0000000..0ed9586 --- /dev/null +++ b/app/views/postmaster/email_confirmation_code.en.html.erb @@ -0,0 +1,97 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/email_otp_disable_confirmation_code.en.html.erb b/app/views/postmaster/email_otp_disable_confirmation_code.en.html.erb new file mode 100644 index 0000000..324ba08 --- /dev/null +++ b/app/views/postmaster/email_otp_disable_confirmation_code.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/email_otp_enable_confirmation_code.en.html.erb b/app/views/postmaster/email_otp_enable_confirmation_code.en.html.erb new file mode 100644 index 0000000..3b26008 --- /dev/null +++ b/app/views/postmaster/email_otp_enable_confirmation_code.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/label.en.html.erb b/app/views/postmaster/label.en.html.erb new file mode 100644 index 0000000..4c16b5c --- /dev/null +++ b/app/views/postmaster/label.en.html.erb @@ -0,0 +1,76 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/new_beneficiary.en.html.erb b/app/views/postmaster/new_beneficiary.en.html.erb new file mode 100644 index 0000000..4a96406 --- /dev/null +++ b/app/views/postmaster/new_beneficiary.en.html.erb @@ -0,0 +1,55 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/otp_confirmation_code.en.html.erb b/app/views/postmaster/otp_confirmation_code.en.html.erb new file mode 100644 index 0000000..5fe4bbb --- /dev/null +++ b/app/views/postmaster/otp_confirmation_code.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/password_confirmation_code.en.html.erb b/app/views/postmaster/password_confirmation_code.en.html.erb new file mode 100644 index 0000000..bc77694 --- /dev/null +++ b/app/views/postmaster/password_confirmation_code.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/password_reset.en.html.erb b/app/views/postmaster/password_reset.en.html.erb new file mode 100644 index 0000000..5cff7fd --- /dev/null +++ b/app/views/postmaster/password_reset.en.html.erb @@ -0,0 +1,97 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/password_reset.ru.html.erb b/app/views/postmaster/password_reset.ru.html.erb new file mode 100644 index 0000000..8385bfd --- /dev/null +++ b/app/views/postmaster/password_reset.ru.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/session_create.en.html.erb b/app/views/postmaster/session_create.en.html.erb new file mode 100644 index 0000000..a652830 --- /dev/null +++ b/app/views/postmaster/session_create.en.html.erb @@ -0,0 +1,61 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/session_failed.en.html.erb b/app/views/postmaster/session_failed.en.html.erb new file mode 100644 index 0000000..7c4e3ef --- /dev/null +++ b/app/views/postmaster/session_failed.en.html.erb @@ -0,0 +1,61 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/withdraw_confirmation_code.en.html.erb b/app/views/postmaster/withdraw_confirmation_code.en.html.erb new file mode 100644 index 0000000..e32ae46 --- /dev/null +++ b/app/views/postmaster/withdraw_confirmation_code.en.html.erb @@ -0,0 +1,73 @@ + + + + + + + + + + + diff --git a/app/views/postmaster/withdraw_succeed.en.html.erb b/app/views/postmaster/withdraw_succeed.en.html.erb new file mode 100644 index 0000000..1286073 --- /dev/null +++ b/app/views/postmaster/withdraw_succeed.en.html.erb @@ -0,0 +1,55 @@ + + + + + + + + + + + diff --git a/app/workers/kyc/local/owner_mobile_worker.rb b/app/workers/kyc/local/owner_mobile_worker.rb new file mode 100644 index 0000000..8d63582 --- /dev/null +++ b/app/workers/kyc/local/owner_mobile_worker.rb @@ -0,0 +1,18 @@ +require 'sidekiq' +module KYC + module Local + class OwnerMobileWorker + include Sidekiq::Worker + + def perform(mobile_id) + mobile = Phone.find_by(id: mobile_id) + return unless mobile.user.verified_profile.present? + + jibit = JibitService.new + response = jibit.mobile_info(mobile.national_number, mobile.user.verified_profile.national_code) + + mobile.update(step: 'owner_phone') if JSON.parse(response.body).dig('matched').present? + end + end + end +end diff --git a/app/workers/kyc/local/treasury_worker.rb b/app/workers/kyc/local/treasury_worker.rb new file mode 100644 index 0000000..b39342e --- /dev/null +++ b/app/workers/kyc/local/treasury_worker.rb @@ -0,0 +1,22 @@ +require 'sidekiq' +module KYC + module Local + class TreasuryWorker + include Sidekiq::Worker + + def perform(profile_id) + profile = Profile.find_by(id: profile_id) + return if profile.blank? || profile.state != 'verified' + + treasuries = profile.user.submitted_treasuries + return unless treasuries.present? + + treasuries.each do |treasury| + jibit = JibitService.new + response = treasury.kind == 'iban' ? jibit.iban_info(treasury.data) : jibit.card_info(treasury.data) + treasury.update(result: response&.body, state: 'processing') + end + end + end + end +end diff --git a/bin/bundle b/bin/bundle new file mode 100644 index 0000000..f19acf5 --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/cloudflare b/bin/cloudflare new file mode 100644 index 0000000..317ee54 --- /dev/null +++ b/bin/cloudflare @@ -0,0 +1,26 @@ +#!/usr/bin/env ruby +require 'erb' +require 'pathname' +require 'fileutils' +# Open Source license key provided by Openware has some download rate and amount limits +# We strongly suggest you to create your oun key and pass via --build-arg MAXMINDDB_LICENSE_KEY +# All the guidance on how to create license key you can find here - https://blog.maxmind.com/2019/12/18/significant-changes-to-accessing-and-using-geolite2-databases/ +MAXMINDDB_LICENSE_KEY='T6ElPBlyOOuCyjzw'.freeze +MAXMINDDB_LINK="https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&suffix=tar.gz&license_key=#{MAXMINDDB_LICENSE_KEY}" + +root = Pathname.new(File.expand_path('../../', __FILE__)) +# Download MaxMind Country DB +system "mkdir -p #{root}/geolite; + wget -O #{root}/geolite.tar.gz \"#{MAXMINDDB_LINK}\"; + tar xzf #{root}/geolite.tar.gz -C #{root}/geolite --strip-components 1; + rm -rf #{root}/geolite.tar.gz" + +# Download list of Cloudflare IP Ranges (v4 and v6) +system "curl https://www.cloudflare.com/ips-v4 >> #{root}/config/cloudflare_ips.yml; + echo '\n' >> #{root}/config/cloudflare_ips.yml; + curl https://www.cloudflare.com/ips-v6 >> #{root}/config/cloudflare_ips.yml" + + +system 'nohup ./bin/mailer --config=config/mailer.yml run >/dev/null 2>&1 &' + +system 'echo @@@ Geolite and Cloadfalre and Mailer finished @@@' diff --git a/bin/gendocs b/bin/gendocs new file mode 100644 index 0000000..66a7ddf --- /dev/null +++ b/bin/gendocs @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +set -e + +RAILS_ENV=development + +if [ -f tmp/pids/server.pid ]; then + kill $(cat tmp/pids/server.pid) +fi + +if [ -n "${V}" ]; then +cat > config/initializers/versioning.rb << EOF +# encoding: UTF-8 +# frozen_string_literal: true +# This file is auto-generated from the current state of VCS. +# Instead of editing this file, please use bin/gendocs. +module Barong + class Application + GIT_TAG = '${V}' + GIT_SHA = '$(git rev-parse --short HEAD)' + BUILD_DATE = '$(date --rfc-3339=seconds)' + VERSION = GIT_TAG + end +end +EOF +fi + +bundle install +bin/init_config +bundle exec rake db:drop db:create db:migrate db:seed +npm i -g swagger-markdown +npm i -g prettyjson-cli +bundle exec rails s -d +while [ ! -f tmp/pids/server.pid ] +do + sleep 1 +done +sleep 5 # Wait additional time until Rails starts listen port. + +curl http://localhost:3000/api/v2/swagger > tmp/user_api_v2_swagger.json +curl http://localhost:3000/api/v2/management/swagger > tmp/management_api_v2_swagger.json +curl http://localhost:3000/api/v2/admin/swagger > tmp/admin_api_v2_swagger.json + +mkdir -p docs/api/swagger + +swagger-markdown -i tmp/user_api_v2_swagger.json -o ./docs/api/barong_user_api_v2.md +swagger-markdown -i tmp/management_api_v2_swagger.json -o ./docs/api/barong_management_api_v2.md +swagger-markdown -i tmp/admin_api_v2_swagger.json -o ./docs/api/barong_admin_api_v2.md + +ruby -r json -e "puts JSON.pretty_generate(JSON.parse(File.read('tmp/user_api_v2_swagger.json')))" \ + > docs/api/swagger/user_api.json +ruby -r json -e "puts JSON.pretty_generate(JSON.parse(File.read('tmp/management_api_v2_swagger.json')))" \ + > docs/api/swagger/management_api.json +ruby -r json -e "puts JSON.pretty_generate(JSON.parse(File.read('tmp/admin_api_v2_swagger.json')))" \ + > docs/api/swagger/admin_api.json + +kill $(cat tmp/pids/server.pid) diff --git a/bin/init_config b/bin/init_config new file mode 100644 index 0000000..786772d --- /dev/null +++ b/bin/init_config @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +require 'erb' +require 'pathname' +require 'fileutils' + +src_root = Pathname.new(File.expand_path('../../config/templates', __FILE__)) +dest_root = Pathname.new(File.expand_path('../../config', __FILE__)) + +Dir[src_root.join('**/*.erb').to_s].each do |f| + dest = dest_root.join(Pathname.new(f).relative_path_from(src_root)).sub(/\.erb\z/, '') + FileUtils.mkpath(dest.dirname) + puts "Copy #{f} -> #{dest}" + File.write(dest, ERB.new(File.read(f)).result) +end diff --git a/bin/init_vault b/bin/init_vault new file mode 100644 index 0000000..efbb4ea --- /dev/null +++ b/bin/init_vault @@ -0,0 +1,7 @@ +DOCKER_VAULT_ID=`docker ps | grep vault | awk '{ print $1 }'` + +docker exec ${DOCKER_VAULT_ID} sh -c \ + "vault secrets disable secret \ + && vault secrets enable -path=secret -version=1 kv \ + && vault secrets enable totp" + diff --git a/bin/install_plugins b/bin/install_plugins new file mode 100644 index 0000000..164499c --- /dev/null +++ b/bin/install_plugins @@ -0,0 +1,18 @@ +#!/usr/bin/env ruby + +require 'fileutils' +require 'yaml' + +Dir['vendor/plugins/*'].each { |d| FileUtils.rm_rf(d) } + +plugins = YAML.load_file('config/plugins.yml').yield_self { |ary| ary || [] }.map do |plugin| + [ %[git clone #{plugin.fetch('git')} vendor/plugins/#{plugin.fetch('name')}], + %[cd vendor/plugins/#{plugin.fetch('name')} && git checkout #{plugin.fetch('commit')}], + ].each do |command| + Kernel.system(command) + Kernel.exit(false) unless $?.success? + end + plugin +end + +Kernel.puts "\n#{plugins.count} plugin(s) installed to vendor/plugins." diff --git a/bin/link_config b/bin/link_config new file mode 100644 index 0000000..a2fa955 --- /dev/null +++ b/bin/link_config @@ -0,0 +1,15 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +require 'pathname' +require 'fileutils' + +src_root = Pathname.new('/opt/barong/config') +dest_root = Pathname.new(File.expand_path('../../config', __FILE__)) + +Dir[src_root.join('**/*.yml').to_s].each do |f| + dest = dest_root.join(Pathname.new(f).relative_path_from(src_root)) + Kernel.puts "Link #{f} -> #{dest}" + FileUtils.ln_s(f, dest, force: true) +end diff --git a/bin/mailer b/bin/mailer new file mode 100644 index 0000000..8b92c3d --- /dev/null +++ b/bin/mailer @@ -0,0 +1,28 @@ +#!/usr/bin/env ruby + +# frozen_string_literal: true + +require File.expand_path('../../config/environment', __FILE__) + +class Mailer + extend GLI::App + + program_desc 'Notification Hub' + + flag :config, desc: 'Path to mailer config file', default_value: 'config/mailer.yml' + + command :stop do |c| + c.action do + end + end + + command :run do |c| + c.desc 'Run processing email events' + c.action do |global_options, _options, _args| + config = YAML.load_file(global_options[:config]).deep_symbolize_keys + EventMailer.call(config[:events], config[:exchanges], config[:keychain]) + end + end +end + +exit Mailer.run(ARGV) diff --git a/bin/rails b/bin/rails new file mode 100644 index 0000000..5badb2f --- /dev/null +++ b/bin/rails @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100644 index 0000000..d87d5f5 --- /dev/null +++ b/bin/rake @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100644 index 0000000..94fd4d7 --- /dev/null +++ b/bin/setup @@ -0,0 +1,36 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + # puts "\n== Copying sample files ==" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' + # end + + puts "\n== Preparing database ==" + system! 'bin/rails db:setup' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/bin/update b/bin/update new file mode 100644 index 0000000..58bfaed --- /dev/null +++ b/bin/update @@ -0,0 +1,31 @@ +#!/usr/bin/env ruby +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = File.expand_path('..', __dir__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + # Install JavaScript dependencies if using Yarn + # system('bin/yarn') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000..8583ebc --- /dev/null +++ b/config.ru @@ -0,0 +1,19 @@ +# This file is used by Rack-based servers to start the application. + +require_relative 'config/environment' +require 'rack/cors' +# Load CORS::Validations module +require_relative 'lib/barong/cors/validations' + +use Rack::Cors do + allow do + origins Barong::CORS::Validations.validate_origins(Barong::App.config.api_cors_origins) + resource '/api/*', + methods: %i[get post delete put patch options head], + headers: :any, + credentials: Barong::App.config.api_cors_allow_credentials, + max_age: Barong::CORS::Validations.validate_max_age(Barong::App.config.api_cors_max_age) + end +end + +run Rails.application diff --git a/config/abilities.yml b/config/abilities.yml new file mode 100644 index 0000000..0a1eb9b --- /dev/null +++ b/config/abilities.yml @@ -0,0 +1,49 @@ +roles: + - superadmin + - admin + - compliance + - support + +admin_permissions: + superadmin: + manage: + - User + - Activity + - Ability + - APIKey + - Profile + - Permission + - Label + - Restriction + - Level + - Document + admin: + read: + - Activity + - Level + - APIKey + - Permission + - Document + manage: + - User + - Activity + - Profile + - Label + - Document + compliance: + read: + - Level + - User + - Activity + manage: + - Label + update: + - Profile + support: + read: + - User + - Activity + - APIKey + - Profile + - Label + - Level diff --git a/config/amqp.yml b/config/amqp.yml new file mode 100644 index 0000000..eb24c03 --- /dev/null +++ b/config/amqp.yml @@ -0,0 +1,111 @@ +connect: + host: <%= ENV.fetch('BARONG_EVENT_API_RABBITMQ_HOST', 'localhost') %> + port: <%= ENV.fetch('RABBITMQ_PORT', '5672') %> + username: <%= ENV.fetch('RABBITMQ_USER', 'guest') %> + password: <%= ENV.fetch('RABBITMQ_PASSWORD', 'guest') %> + +exchange: + trade: + name: peatio.trade + type: headers + notification: + name: peatio.notification + type: direct + orderbook: + name: peatio.orderbook + type: fanout + events: + name: peatio.events + type: direct + matching: + name: peatio.matching + type: direct + finex-spot: + name: finex.orderapi + type: direct + opendax: + name: finex.orderapi + type: direct + +queue: + matching: + name: peatio.matching + durable: true + # You can set queue maximum length (see https://www.rabbitmq.com/maxlength.html). + # For order matching queue it is recommended to set limit about to 10000 per market. + # So in case you have 10 markets you would set 1000000. But this value is relative and depends on your business (market load). + # + # max_length: 10000 + # + # Alternatively you can pass custom arguments here (for example in case your use RabbitMQ plugins): + # + # arguments: + # x-max-length: 10000 + # + # When you change any parameter of queue definition it is required to drop it in RabbitMQ admin panel. + # In case you want this to be done transparently and automatically I recommend to set auto_delete to true. + # + # auto_delete: true + # + # In such case queue will be deleted automatically once all workers are disconnected. This should ensure queue + # is created from scratch with new configuration at next redeployment. + new_trade: + name: peatio.trade.new + durable: true + order_processor: + name: peatio.order.processor + durable: true + market_ticker: + name: peatio.trade.market_ticker + pusher_market: + name: peatio.pusher.market + pusher_member: + name: peatio.pusher.member + withdraw_coin: + name: peatio.withdraw.coin + deposit_collection_fees: + name: peatio.deposit.collection_fees + deposit_collection: + name: peatio.deposit.collection + deposit_coin_address: + name: peatio.deposit.coin.address + durable: true + influx_writer: + name: peatio.trade.writer + trade_error: + name: peatio.trades.errors + # Queue which used by third-party trading engine for updating balances and + # order submit consuming. + events_processor: + name: peatio.events.processor + +binding: + matching: + queue: matching + clean_start: true + exchange: matching + trade_executor: + queue: new_trade + exchange: matching + order_processor: + queue: order_processor + exchange: matching + withdraw_coin: + queue: withdraw_coin + deposit_coin_address: + queue: deposit_coin_address + influx_writer: + queue: influx_writer + exchange: trade + trade_error: + queue: trade_error + exchange: matching + events_processor: + queue: events_processor + exchange: events + +channel: + trade_executor: + prefetch: 5 + order_processor: + prefetch: 5 diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 0000000..592edb5 --- /dev/null +++ b/config/application.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require_relative 'boot' +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Barong + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 5.2 + + # Configure Sentry as early as possible. + if ENV["BARONG_SENTRY_DSN_BACKEND"].present? + require "sentry-raven" + Raven.configure { |config| config.dsn = ENV["BARONG_SENTRY_DSN_BACKEND"] } + end + + # Adding Grape API + # Eager loading all app/ folder + config.eager_load_paths += Dir[Rails.root.join('app')] + config.eager_load_paths += Dir[Rails.root.join('lib/barong')] + + # Setup the logger + config.logger = Logger.new(STDOUT) + + # Load lib folder files to be visible in specs + config.paths.add 'lib', eager_load: false, autoload: true + + # Settings in config/environments/* take precedence over those specified here. + # Application configuration can go into files in config/initializers + # -- all .rb files in that directory are automatically loaded after loading + # the framework and any gems in your application. + # + env_file = File.join(Rails.root, 'barong.env') + Dotenv.load(env_file) if File.exists?(env_file) + end +end diff --git a/config/authz_rules.yml b/config/authz_rules.yml new file mode 100644 index 0000000..af19148 --- /dev/null +++ b/config/authz_rules.yml @@ -0,0 +1,18 @@ +# +# pass for whitelisted (public) routes +# block for blacklisted routes +# +rules: + pass: + - api/v2/barong/public + - api/v2/barong/identity + - api/v2/peatio/public + - api/v2/peatio/coinmarketcap + - api/v2/peatio/coingecko + - api/v2/ranger/public + - api/v2/applogic/public + - api/v2/arke/public + - api/v2/finex/public + block: + - api/v2/barong/management + - api/v2/peatio/management diff --git a/config/backend.yml b/config/backend.yml new file mode 100644 index 0000000..40608b7 --- /dev/null +++ b/config/backend.yml @@ -0,0 +1,76 @@ +version: '3' + +services: + + db: + image: mysql:5.7 + volumes: + - db_data:/var/lib/mysql + environment: + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" + ports: + - "3306:3306" + + redis: + image: redis:4.0 + volumes: + - redis_data:/data + ports: + - "6379:6379" + + vault: + image: vault:0.11.4 + ports: + - "8200:8200" + environment: + SKIP_SETCAP: 1 + VAULT_TOKEN: changeme + VAULT_DEV_ROOT_TOKEN_ID: changeme + VAULT_ADDR: http://vault:8200 + + gateway: + image: quay.io/datawire/ambassador:0.40.0 + ports: + - "8080:80" + volumes: + - ./gateway:/ambassador/ambassador-config/:ro + entrypoint: + - sh + - -exc + - | + # Allow accessing host's 0.0.0.0 so we can connect to a local peatio + # from the gateway container + ip -4 route list match 0/0 | awk '{print $$3" barong.local"}' >> /etc/hosts + # Continue with the default entrypoint + ./entrypoint.sh + + minio: + image: minio/minio + volumes: + - minio-data:/data + ports: + - "9000:9000" + environment: + MINIO_ACCESS_KEY: changemeEXAMPLE + MINIO_SECRET_KEY: changemeEXAMPLEKEY + command: server /data + + rabbitmq: + image: rabbitmq:3.7.6-management + volumes: + - rabbitmq_data:/var/lib/rabbitmq + ports: + - "5672:5672" + - "15672:15672" + + mailcatcher: + image: schickling/mailcatcher + ports: + - "1080:1080" + - "1025:1025" + +volumes: + db_data: + redis_data: + rabbitmq_data: + minio-data: diff --git a/config/barong.yml b/config/barong.yml new file mode 100644 index 0000000..cb27f63 --- /dev/null +++ b/config/barong.yml @@ -0,0 +1,53 @@ +activation_requirements: + email: 'verified' +state_triggers: + banned: + - ban + - fraud + deleted: + - delete + locked: + - suspicious + - lock +document_types: + - Passport + - Identity card + - Driver license + - Utility Bill + - Residental + - Institutional + - Poa + - Selfie + +document_categories: + - front_side + - selfie + - back_side + +user_storage_titles: + # - personal ## example + # - company ## example +captcha_protected_endpoints: + - user_create + - session_create + - password_reset + - email_confirmation +profile_verification_roles: + - superadmin + # - admin + # - kyc_worker + +kyc_levels: + 1: + 2: profile + 3: access_phone + 4: card + 5: iban + 2: + 6: poa + 7: telephone + 8: selfie + 9: owner_phone + 3: + 10: vip + diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 0000000..b9e460c --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,4 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 0000000..cc39bef --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,10 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: <%= ENV.fetch("BARONG_REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: barong_production diff --git a/config/credentials.yml.enc b/config/credentials.yml.enc new file mode 100644 index 0000000..713e9cb --- /dev/null +++ b/config/credentials.yml.enc @@ -0,0 +1 @@ +I6tY6I6n09+uAR8G6yiX24k+SKHfx0ZTISnXmX9kupVSymzL02I41OiGqH5qWOvwmLpdvrr/BSIcqDms1tMSzgei+6IGZNyuY+nIwG9rTce33ucrMUS+9/RH8MFaSGyi7k04K3dWgf0/b/9xm9Qcv3dK7mnB46MwbsDRXf0gwbxS9YcgDzdRA5nXlgrQn/hTxwIHv4I8k6FCa9FFvrywCtVql1guo6RIzvm+iAYmHa3Ef89twIYaCUFNhJXknEUHleCCLLpWSqQ7B0gPauerlTrv+yJByPaDin1CsbNbMeykgZoL0WewauicKzQB52LWNSC0i+6cLa98XWmLiSDQDy2OsuQ+pOr0J3UzYhoj5LhiikT71sisLrRRr6CjBsIfN6Gh7SSThjnvquqpkelGlJERtsdgJFyF2ayOTUkIviKL1KaH+xF+dliTQKNfFrrA5fDQKl0vIfTwCuOGZXSnF7xHrj98kLk4Z62hQe2mhk4fznK907d+sHXd7Sdo4mZr2Mz4M9KCN7Ez734pQ1ua0cB2mOwj7x4P7Z5HLpw+AcGEnZegqJ/ZbEgvhaRx227lSh49LpIr62kH7RJi00e6gOPOJuETwRvOCD6wmb0y55n1kISoKQIX+zQuiUwo2aF2kPp2eFYvCvWd4yF0t01oe2pkYA0imFfJoitHAIo0Vw6A9W1DHx3oLVsouG2d4X3BZ1Rh1dp22K0mvcpSSrVfUGcyIWhgr8pTJpghH19pjrXv9zzRAAVH0pNJF42cjb312SowIrqjIEMaI9ved+poaF1pHdrXEbV8oacrVHtBSH/oxVTqhImVgnpW0UgPJn63wzYpCHo4Sqby3E36AZoca7hTPuHiLSYSCWcTLW9j+35RL4+FIExfG6RGWcIKHmEOEYllDdPziL7M5hPPS8vLOEMS/m82KOwO/FArSTjbcRaeJANC6hUdld/Z1CuGA180gfjpzdV0iNv483n3w0r0+vYxWvqHFS9ZJdyekP1bj2ni4928Ra4AV5vvz4Fu78w2vKl4+ZpEQO26i0oIPbCFyIvBzv2bML2D7cFeWVISzzmZc+azHhq9wzldAsW+PXwr5E4F2CRpcPIkOR8Nobed9koKCCNhHTao9YMzgX2svNQVLVnU4XUB1HIhfE31vZdU/UJ0p8d8s/3WZZ5eTVsir+GwrQ2vNaKBcLLUXaHwlImkQ9Jl6Zp3T9eLLm6o5jR9lZFluiNalZXCpG2+DzUc3TXwyPOhOuUx/2rFoKzyBXHFbvzPi8Vq7YtusKAZ1Cnp/bPB9HUpQWPvnITowUT8HI8qOKntnyokIJSk7WfvB2sME3FZQ6xwAcSJZqHm4PZYoLH6YTTdy/X7/ioFxEA8ekxtjVh09+IIfd6kIbeeO5x5G4Ps7DayDxeENLwwZK561kwy4fXLfqXH9oOWBdMpitGnEotB2U3JK72wu8+m1rLOzjKpW43zoj8HxLCBnJl7ybFvOaeblJPrsKj8LHWN/nyZKG6O0ji3fBw6g3oJlj56Zhamdd0Nu3hYygUmmPfD999mhnx+DOBxD+C2x1SQfinRGHssmiWbkqoSBBY50FC+Fkrfvno4pdfykcPG+Tc3HFYwy3hNZ538Vp6zQSCdEAAcp/qFKv+qNrH665q4qzpSlvB6BSTA6YWlu3+OGjKGOpY2qsxhWmvJ0SC7UCnGk79bBnoLyaSbcLtmC7lNGm3QlACqrvZBrprXP6ExRrwwkBSdJL9tJUEdKbdxwGx4YL4EffDDa5xUdJm/+os4QFyjj4wb/3fcH6iFlXEeCgEGo8Woegb2kDH6Xe+VqlL/kIFfbBTi/kIcs5m4+NeslTZyOQrrE4iZ4zXZncWs7xQwK5m2+2jFm3hAjBVfgiV1lUQQfiladUoEDu5rVoMFA2RSHY9Pm4nMo2LN8WLMS2mLiO+l4Ak3uiM/DskhdNmZxXvEhsV2eHtEYChtiaUEY0etsMmrTzVmg4Mzw7Ua7ioN8aV6fDrpswjHeUUG5HeYq7oVLUDeid4qwH9jpSHBA6eOGq7YbCZvwQFjhxdRIW/dAEiW5YZY0rIgFiBYFG2PVWI3ivD+/1jRkjy1yYYK7E4cNMSrIVnA38ZFEF7lvuWqA95T5Gezr1DMIve+GZQg0o9IBwK+C7LwPqgk6ntIwTSV43xuY+YuSxOc5ZDznbo9XVPvOBeEB+bJBHN/+IIpK/0dVmPKe5m10vrNt6VEGZKLHuTftljRYl4jE4MipfLQeVH+PIvLqbstBORdvPhRuFPp45BTpSGFZJaO0lZbzGQtEVDozCpClMRkfFP2pWbeikzNDMoELwTl6PSUAaNq9lUjytn23pGJLs4ExYnVMcvjzl6xtSEurQQRu4ZIO+/en4e/KHEiwhgeUuoIcJSBYmEvGsSjsNBLKmNQrVxQzF4TCLt6iGXqpk/p5YtqaVenOTF1oZm7lj9JL2u4xpXCjcjPX0uJ7Li5YZOiavDDvnJgvl0A9P9In19XcP1zHXnP6l3OXOUTE6Tv18m6/O1rV2VpQkK+gIPrYztFvBOaC7/sWdpbqsqS3DTfeuCUIA7sBfB85jSl+uukhhd3EOCjucaT8BrvcMHx0hmHAwDOedZTEdZl2K9fhR0Uhf1i+d4M60/lyQqKjG1BzcmIXIZ/z4HNB4/oCBwsZVSYMH8cM2NYb+EOxHUW2cBqPwLcmt2JfrcMZDP5YkdtqfuvQiCSzYMcssfw/i7uAan2HlSdeiguglsinJZ1efBcTD4RsPld3jCjraseIjha96j8W8Wnk7SQElbFQFoS5MLbGhWLzkMpyiRPCZrT5r8SGPdtwqEKXNF2RVWFsOvXsDicv2mv09nb4ppFem7jgwGwj0Wi2Mv7x7ZxAW/EKt1fWTtVdMgse8wJZnbOAn4Xl5G/ll3m6jytmG4rHdy6HEQL56gEYencQViWFbuqJYsPZDRTOuc6vwngTgqaoNpGC/UpbaCOxwurjw0+wszsjKtF9YC/aIxddAypHwmAA/zq/gbLpwAyilI8K8ZDUcHQbH14vubz5Kn65SQv44VbwOe4k6HPrnkWjrEN+IOukv0NJhIcln0yOBwAjru4q2A1aNmeJaoCPpmqMb+95/vC5BNKyFJnyr6of9bDIlNdQABH8vBzFwyUcPN+SXCZbRDiDmL4+Iw4lNxfV2XGDCob3K9Jh2Fo85xsNUkCuVsvU5iBD9YoctB+oWcGd5mDFNJsX0M+Ft5rBbznYv/syk8lurM27L1RWKMz6KbGEQoK4ov2Jz+PlkxrX7WzznolxXtwEGZdU2LeVbqUj5HGYnntWDjoF9AASLLFDo7V67wyHu9ZL+YXEOVgsAbNjJfyqJInmPMdGHyjHpBXqGse+l44qBPsI2fwdj0NbkkFM+sScBrx9S3gfVB3iOOUcHo5MkvbmsauTRwwg67YzhMTbFGSSjiX4QI5iwDP--JL81g7CbhrdSINC1--OBdY+ZocetafB3jJoROfRA== \ No newline at end of file diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 0000000..e1b7f66 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,20 @@ +default: &default + adapter: <%= ENV.fetch('DATABASE_ADAPTER', 'mysql2') %> + encoding: utf8 + pool: <%= ENV.fetch("RAILS_MAX_THREADS", 6).to_i * ENV.fetch("WEB_CONCURRENCY", 1).to_i %> + host: <%= ENV.fetch('DATABASE_HOST', '127.0.0.1') %> + port: <%= ENV.fetch('DATABASE_PORT', 3306) %> + username: <%= ENV.fetch('DATABASE_USER', 'root') %> + password: <%= ENV.fetch('DATABASE_PASS', 'changeme') %> + +development: + <<: *default + database: barong_development + +test: + <<: *default + database: barong_test + +production: + <<: *default + database: <%= ENV.fetch('DATABASE_NAME', 'barong_production') %> diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 0000000..426333b --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,5 @@ +# Load the Rails application. +require_relative 'application' + +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 0000000..060b64a --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,63 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Standard rails dev caching behaviour + # if Rails.root.join('tmp', 'caching-dev.txt').exist? + # config.action_controller.perform_caching = true + # config.cache_store = :memory_store + # config.public_file_server.headers = { + # 'Cache-Control' => "public, max-age=#{2.days.to_i}" + # } + # else + # config.action_controller.perform_caching = false + # config.cache_store = :null_store + # end + + # Using cache for sessions and permissions forces to use redis cache_store as mandatory store + # Here we use ENV.fetch instead of Barong::App.config, because environment/* files loads before lib and initializers + config.cache_store = :redis_cache_store, { driver: :hiredis, url: ENV.fetch('BARONG_REDIS_URL', 'redis://localhost:6379/1') } + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false + + config.action_mailer.perform_caching = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Use an evented file watcher to asynchronously detect changes in source code, + # routes, locales, etc. This feature depends on the listen gem. + config.file_watcher = ActiveSupport::EventedFileUpdateChecker + config.web_console.whitelisted_ips = '172.0.0.0/16' +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 0000000..be8e803 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,106 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options) + config.active_storage.service = :local + + # Mount Action Cable outside main process or domain + # config.action_cable.mount_path = nil + # config.action_cable.url = 'wss://example.com/cable' + # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = ENV.fetch('LOG_LEVEL', 'info') + + # Prepend all log lines with the following tags. + config.log_tags = [ :request_id ] + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Using cache for sessions and permissions forces to use redis cache_store as mandatory store + # Here we use ENV.fetch instead of Barong::App.config, because environment/* files loads before lib and initializers + if ENV.true?('BARONG_REDIS_CLUSTER') + config.cache_store = :redis_cache_store, { driver: :hiredis, cluster: [ENV.fetch('BARONG_REDIS_URL')], password: ENV.fetch('BARONG_REDIS_PASSWORD') } + else + config.cache_store = :redis_cache_store, { driver: :hiredis, url: ENV.fetch('BARONG_REDIS_URL', 'redis://localhost:6379/1') } + end + + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "barong_#{Rails.env}" + + config.action_mailer.perform_caching = false + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + config.action_mailer.raise_delivery_errors = true + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = JSONLogFormatter.new + + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + output = STDERR if ENV["RAILS_LOG_TO_STDERR"].present? + output = STDOUT if ENV["RAILS_LOG_TO_STDOUT"].present? + + unless output.nil? + logger = ActiveSupport::Logger.new(output) + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + + config.logger.formatter = config.log_formatter + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 0000000..042680e --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,46 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + config.log_level = :fatal + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" + } + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Store uploaded files on the local file system in a temporary directory + config.active_storage.service = :test + + config.action_mailer.perform_caching = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true +end diff --git a/config/initializers/api_pagination.rb b/config/initializers/api_pagination.rb new file mode 100644 index 0000000..15060b7 --- /dev/null +++ b/config/initializers/api_pagination.rb @@ -0,0 +1,27 @@ +ApiPagination.configure do |config| + # If you have more than one gem included, you can choose a paginator. + config.paginator = :kaminari # or :will_paginate + + # By default, this is set to 'Total' + # config.total_header = 'X-Total' + + # By default, this is set to 'Per-Page' + # config.per_page_header = 'X-Per-Page' + + # Optional: set this to add a header with the current page number. + config.page_header = 'Page' + + # Optional: set this to add other response format. Useful with tools that define :jsonapi format + # config.response_formats = [:json, :xml, :jsonapi] + + # Optional: what parameter should be used to set the page option + config.page_param = :page + + # Optional: what parameter should be used to set the per page option + config.per_page_param = :limit + + # Optional: Include the total and last_page link header + # By default, this is set to true + # Note: When using kaminari, this prevents the count call to the database + # config.include_total = false +end diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 0000000..89d2efa --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 0000000..4b828e8 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path. +# Rails.application.config.assets.paths << Emoji.images_path +# Add Yarn node_modules folder to the asset load path. +Rails.application.config.assets.paths << Rails.root.join('node_modules') + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in the app/assets +# folder are already added. +# Rails.application.config.assets.precompile += %w( admin.js admin.css ) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000..59385cd --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/barong.rb b/config/initializers/barong.rb new file mode 100644 index 0000000..c9cfe50 --- /dev/null +++ b/config/initializers/barong.rb @@ -0,0 +1,157 @@ +# frozen_string_literal: true + + +# 1/ check if ENV key exist then validate and set +# 2/ if no check in credentials then validate and set +# 3/ if no generate display warning, raise error in production, and set + +require 'barong/app' +require 'barong/keystore' +require 'barong/string' + +private_key_path = ENV['JWT_PRIVATE_KEY_PATH'] + +if !private_key_path.nil? + pkey = Barong::KeyStore.open!(private_key_path) + Rails.logger.error('Loading private key from: ' + private_key_path) + +elsif Rails.application.credentials.has?(:private_key) + pkey = Barong::KeyStore.read!(Rails.application.credentials.private_key) + Rails.logger.info('Loading private key from credentials.yml.enc') + +elsif !Rails.env.production? + # Generates private key + key = Barong::KeyStore.generate + pkey = key.to_pem + pub_key = key.public_key.to_pem + + # Save private/public keys + Barong::KeyStore.save!(pkey, 'config/rsa-key') + Barong::KeyStore.save!(pub_key, 'config/rsa-key.pub') + + Rails.logger.warn('Warning !! Generating private key') +else + raise 'Private key not found or invalid' +end + +kstore = Barong::KeyStore.new(pkey) + +# Define default value for secret_key_base in test and development mode +ENV['SECRET_KEY_BASE'] = '' unless Rails.env.production? + +Barong::App.define do |config| + # General configuration --------------------------------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#general-configuration + config.set(:app_name, 'Fibitex') + config.set(:domain, 'fibitex.com') + config.set(:uid_prefix, 'ID', regex: /^[A-z]{2,6}$/) + config.set(:session_name, '_dalan_session') + config.set(:session_expire_time, '1800', type: :integer) + config.set(:kyc_provider, 'kycaid', values: %w[kycaid local]) + config.set(:required_docs_expire, 'false', type: :bool) + config.set(:doc_num_limit, '10', type: :integer) + config.set(:geoip_lang, 'en', values: %w[en de es fr ja ru]) + config.set(:csrf_protection, 'true', type: :bool) + config.set(:apikey_nonce_lifetime, '50000', type: :integer) + config.set(:gateway, 'cloudflare', values: %w[akamai cloudflare]) + config.set(:jwt_expire_time, '3600', type: :integer) + config.set(:profile_double_verification, 'false', type: :bool) + config.set(:crc32_salt, '') + config.set(:api_data_masking_enabled, 'true', type: :bool) + config.set(:first_registration_superadmin, 'true', type: :bool) + config.set(:mgn_api_keys_user, 'false', type: :bool) + config.set(:mgn_api_keys_sa, 'false', type: :bool) + + + # Password configuration ----------------------------------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#password-configuration + config.set(:password_regexp, '^(?=.*[[:lower:]])(?=.*[[:upper:]])(?=.*[[:digit:]])(?=.*[[:graph:]]).{8,80}$', type: :regexp) + config.set(:password_min_entropy, '14', type: :integer) + config.set(:password_use_dictionary, 'true', type: :bool) + + # CAPTCHA configuration --------------------------------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#captcha-configuration + config.set(:captcha, 'none', values: %w[none recaptcha geetest]) + config.set(:geetest_id, '') + config.set(:geetest_key, '') + config.set(:recaptcha_site_key, '') + config.set(:recaptcha_secret_key, '') + + # Dependencies configuration (vault, redis, rabbitmq) --------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#dependencies-configuration-vault-redis-rabbitmq + config.set(:event_api_rabbitmq_host, 'localhost') + config.set(:event_api_rabbitmq_port, '5672') + config.set(:event_api_rabbitmq_username, 'guest') + config.set(:event_api_rabbitmq_password, 'guest') + config.set(:vault_address, 'http://localhost:8200') + config.set(:vault_token, '') + config.set(:redis_cluster, 'false', type: :bool) + config.set(:redis_url, 'redis://localhost:6379/1') + config.set(:redis_password, '') + config.set(:vault_app_name, 'barong') + + # CORS configuration ----------------------------------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#api-cors-configuration + config.set(:api_cors_origins, '*') + config.set(:api_cors_max_age, '3600') + config.set(:api_cors_allow_credentials, 'false', type: :bool) + + # Config files configuration ---------------------------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#config-files-configuration + config.set(:config, 'config/barong.yml', type: :path) + config.set(:maxminddb_path, 'geolite/GeoLite2-Country.mmdb', type: :path) + config.set(:seeds_file, Rails.root.join('config', 'seeds.yml'), type: :path) + config.set(:authz_rules_file, Rails.root.join('config', 'authz_rules.yml'), type: :path) + + # SMTP configuration ------------------------------------------------ + # https://github.com/openware/barong/blob/master/docs/general/env_configuration.md#smtp-configuration + config.set(:sender_email, 'noreply@barong.io') + config.set(:sender_name, 'Barong') + config.set(:smtp_password, '') + config.set(:smtp_port, 1025) + config.set(:smtp_host, 'localhost') + config.set(:smtp_user, '') + config.set(:smtp_logo_link, 'https://storage.cloud.google.com/public_peatio/logo.png') + config.set(:default_language, 'en') + config.set(:smtp_domain, 'test.com') + config.set(:smtp_enable_starttls_auto, 'true', type: :bool) + config.set(:smtp_openssl_verify_mode, 'peer') + config.set(:smtp_auth, 'plain') + + # KYCAID ------------------------------------------------------------ + config.set(:kycaid_authorization_token, '') + config.set(:kycaid_sandbox_mode, 'true', type: :bool) + config.set(:kycaid_api_endpoint, 'https://api.kycaid.com/') + + # Auth0 configuration ----------------------------------------------- + config.set(:auth0_domain, '') + config.set(:auth0_client_id, '') + + # jibit configuration ---------------------------------------- + config.set(:jibit_api_url, '') + config.set(:jibit_api_key, '') + config.set(:jibit_api_secret, '') +end + +# KYCAID configuring +KYCAID.configure do |config| + config.authorization_token = Barong::App.config.kycaid_authorization_token + config.sandbox_mode = Barong::App.config.kycaid_sandbox_mode + config.api_endpoint = Barong::App.config.kycaid_api_endpoint +end + +ActionMailer::Base.smtp_settings = { + address: Barong::App.config.smtp_host, + port: Barong::App.config.smtp_port, + user_name: Barong::App.config.smtp_user, + authentication: Barong::App.config.smtp_auth, + domain: Barong::App.config.smtp_domain, + enable_starttls_auto: Barong::App.config.smtp_enable_starttls_auto, + openssl_verify_mode: Barong::App.config.smtp_openssl_verify_mode, + password: Barong::App.config.smtp_password +} + +Barong::GeoIP.lang = Barong::App.config.geoip_lang + +Rails.application.config.x.keystore = kstore +Barong::App.config.keystore = kstore diff --git a/config/initializers/barong_config.rb b/config/initializers/barong_config.rb new file mode 100644 index 0000000..d9a744d --- /dev/null +++ b/config/initializers/barong_config.rb @@ -0,0 +1,17 @@ +# FIXME BarongConfig should be a feature of Barong::App +class BarongConfig + class << self + + def list + @hash ||= read_from_yaml + end + + private + + def read_from_yaml + conf = YAML.load_file(Barong::App.config.config) + conf['activation_requirements'] = {'email' => 'verified'} unless conf['activation_requirements'] + conf + end + end +end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb new file mode 100644 index 0000000..5f64789 --- /dev/null +++ b/config/initializers/carrierwave.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'carrierwave/storage/abstract' +require 'carrierwave/storage/file' +require 'carrierwave/storage/fog' + +Barong::App.define do |config| + # Storage configuration + # https://www.openware.com/sdk/docs/barong/configuration.html#storage-configuration + + config.set(:storage_provider, 'local') + config.set(:storage_bucket_name, 'local') + config.set(:storage_access_key, '') + config.set(:storage_secret_key, '') + config.set(:storage_endpoint, '') # optional (AWS, AliCloud) + config.set(:storage_signature_version, '4') # optional (AWS) + config.set(:storage_region, '') # optional (AWS, AliCloud) + config.set(:storage_pathstyle, 'false', type: :bool) # optional (AWS, AliCloud) + # Carrierwave defaults configuration + config.write(:uploader, UploadUploader) + config.set(:upload_size_min_range, '1', type: :integer) # in megabytes + config.set(:upload_size_max_range, '10', type: :integer) # in megabytes + config.set(:upload_auth_url_expiration, '1', type: :integer) # in minutes + config.set(:upload_extension_whitelist, 'jpg, jpeg, png, pdf', type: :array) +end + +CarrierWave.configure do |config| + if 'Google'.casecmp?(Barong::App.config.storage_provider) + config.fog_credentials = { + provider: 'Google', + google_storage_access_key_id: Barong::App.config.storage_access_key, + google_storage_secret_access_key: Barong::App.config.storage_secret_key + } + config.fog_directory = Barong::App.config.storage_bucket_name + elsif 'AWS'.casecmp?(Barong::App.config.storage_provider) + config.fog_credentials = { + provider: 'AWS', + aws_signature_version: Barong::App.config.storage_signature_version, + aws_access_key_id: Barong::App.config.storage_access_key, + aws_secret_access_key: Barong::App.config.storage_secret_key, + region: Barong::App.config.storage_region, + endpoint: Barong::App.config.storage_endpoint, + path_style: Barong::App.config.storage_pathstyle + } + config.fog_directory = Barong::App.config.storage_bucket_name + elsif 'AliCloud'.casecmp?(Barong::App.config.storage_provider) + Barong::App.write(:uploader, AliUploader) + config.fog_credentials = { + provider: 'aliyun', + aliyun_accesskey_id: Barong::App.config.storage_access_key, + aliyun_accesskey_secret: Barong::App.config.storage_secret_key, + aliyun_oss_bucket: Barong::App.config.storage_bucket_name, + aliyun_region_id: Barong::App.config.storage_region, + aliyun_oss_endpoint: "oss-#{Barong::App.config.storage_region}.aliyuncs.com" + } + config.fog_directory = Barong::App.config.storage_bucket_name + else + config.storage :file + end +end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb new file mode 100644 index 0000000..d3bcaa5 --- /dev/null +++ b/config/initializers/content_security_policy.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. + +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 0000000..ccd9e0d --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.signed_cookie_digest = 'SHA256' + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/document_types.rb b/config/initializers/document_types.rb new file mode 100644 index 0000000..aa92226 --- /dev/null +++ b/config/initializers/document_types.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +# document types definitions +class DocumentTypes + class << self + def list + @list ||= YAML.load_file(Barong::App.config.config)['document_types'] + end + + def category_list + @category_list ||= YAML.load_file(Barong::App.config.config)['document_categories'] + end + end +end diff --git a/config/initializers/event_api.rb b/config/initializers/event_api.rb new file mode 100644 index 0000000..0d57ace --- /dev/null +++ b/config/initializers/event_api.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require_dependency 'barong/event_api' + +ActiveSupport.on_load(:active_record) do + ActiveRecord::Base.include ::EventAPI::ActiveRecord::Extension +end diff --git a/config/initializers/exception_reporting.rb b/config/initializers/exception_reporting.rb new file mode 100644 index 0000000..a098138 --- /dev/null +++ b/config/initializers/exception_reporting.rb @@ -0,0 +1,25 @@ +def catch_and_report_exception(options = {}) + begin + yield + nil + rescue options.fetch(:class) { StandardError } => e + report_exception(e) + e + end +end + +def report_exception(exception, report_to_ets = true) + report_exception_to_screen(exception) + report_exception_to_ets(exception) if report_to_ets +end + +def report_exception_to_screen(exception) + Rails.logger.unknown exception.inspect + Rails.logger.unknown exception.backtrace.join("\n") if exception.respond_to?(:backtrace) +end + +def report_exception_to_ets(exception) + Raven.capture_exception(exception) if defined?(Raven) +rescue => ets_exception + report_exception(ets_exception, false) +end \ No newline at end of file diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 0000000..25469a2 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += %i[password upload] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 0000000..ac033bf --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/jwt.rb b/config/initializers/jwt.rb new file mode 100644 index 0000000..718ca93 --- /dev/null +++ b/config/initializers/jwt.rb @@ -0,0 +1,26 @@ +require 'yaml' +require 'openssl' + +(YAML.load_file('config/management_api.yml') || {}).deep_symbolize_keys!.tap do |x| + x.fetch(:keychain).each do |id, key| + key = OpenSSL::PKey.read(Base64.urlsafe_decode64(key.fetch(:value))) + if key.private? + raise ArgumentError, 'keychain.' + id.to_s + ' was set to private key, ' \ + 'however it should be public (in config/management_api.yml).' + end + x[:keychain][id][:value] = key + end + + x.fetch(:scopes).values.each do |scope| + %i[permitted_signers mandatory_signers].each do |list| + scope[list] = scope.fetch(list, []).map(&:to_sym) + scope[list] = scope.fetch(list, []).map(&:to_sym) + if list == :mandatory_signers && scope[list].empty? + raise ArgumentError, 'scopes.' + scope.to_s + '.' + list.to_s + ' is empty, ' \ + 'however it should contain at least one value (in config/management_api.yml).' + end + end + end + + API::V2::Management::JWTAuthenticationMiddleware.security_configuration = x +end diff --git a/config/initializers/kavenegar-ruby_initializer.rb b/config/initializers/kavenegar-ruby_initializer.rb new file mode 100644 index 0000000..89e4b3e --- /dev/null +++ b/config/initializers/kavenegar-ruby_initializer.rb @@ -0,0 +1,18 @@ +KaveRestApi.configure do |config| + + # To completely ignore debug mode events(No Errors) uncomment this line *optional + # config.debugmode = false #by default it's true + + # It is recommended that you pull your API keys from environment settings. *required + config.api_key = ENV.fetch("KAVENEGAR_API_KEY", 'changeme') + + # Default response format is json (you can use xml too). *optional + config.format = 'json' + + #If you don't set your sender number in your request, this is the default number used instead *required + config.default_sender = ENV.fetch("KAVENEGAR_SENDER", 'changeme') + + # You can remove problematic emojis (like android emojis) and replace with standard emojis listed here:(https://www.webpagefx.com/tools/emoji-cheat-sheet/) + # config.strip_emoji = 'false' # can include false , true and matcher + +end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 0000000..dc18996 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/open_uri.rb b/config/initializers/open_uri.rb new file mode 100644 index 0000000..3d915e4 --- /dev/null +++ b/config/initializers/open_uri.rb @@ -0,0 +1,4 @@ +# Don't allow downloaded files to be created as StringIO. Create a tempfile instead + +OpenURI::Buffer.send :remove_const, 'StringMax' if OpenURI::Buffer.const_defined?('StringMax') +OpenURI::Buffer.const_set 'StringMax', 0 diff --git a/config/initializers/phone.rb b/config/initializers/phone.rb new file mode 100644 index 0000000..e603316 --- /dev/null +++ b/config/initializers/phone.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require_dependency 'barong/mock_sms' + +Barong::App.define do |config| + # Twilio configuration ---------------------- + # https://www.openware.com/sdk/docs/barong/configuration.html#twilio-configuration + + # config.write(:twilio_provider, TwilioSmsSendService) + + # config.set(:phone_verification, 'mock') + # config.set(:twilio_phone_number, '+15005550000') + # config.set(:twilio_account_sid, '') + # config.set(:twilio_auth_token, '') + # config.set(:twilio_service_sid, '') + # config.set(:sms_content_template, 'Your verification code for Barong: {{code}}') + + +end + +# sid = Barong::App.config.twilio_account_sid +# token = Barong::App.config.twilio_auth_token +# service_sid = Barong::App.config.twilio_service_sid + +# case Barong::App.config.phone_verification +# when 'twilio_sms' +# raise 'Invalid twilio config' if sid.to_s.empty? || token.to_s.empty? + +# client = Twilio::REST::Client.new(sid, token) +# Barong::App.write(:twilio_provider, TwilioSmsSendService) + +# when 'twilio_verify' +# raise 'Invalid twilio config' if sid.to_s.empty? || token.to_s.empty? + +# client = Twilio::REST::Client.new(sid, token) +# service = client.verify.services.create(friendly_name: Barong::App.config.app_name) unless service_sid.present? +# Barong::App.write(:twilio_provider, TwilioVerifyService) +# when 'mock' +# if Rails.env.production? +# Rails.logger.info("WARNING! Don't use mock phone verification service in production") +# end +# Barong::App.write(:twilio_provider, MockPhoneVerifyService) + +# else +# raise "Unknown phone verification service #{Barong::App.config.phone_verification}" +# end + +# Barong::App.set(:twilio_client, client) if client +# Barong::App.set(:twilio_service_sid, service.sid) if service +# Phonelib.strict_check = true diff --git a/config/initializers/redis.rb b/config/initializers/redis.rb new file mode 100644 index 0000000..fea17aa --- /dev/null +++ b/config/initializers/redis.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +begin + if Rails.env.production? + redis_url = ENV.fetch('BARONG_REDIS_URL', 'redis://localhost:6379/1') + r = Redis.new(url: redis_url) + r.ping + end +rescue Redis::CannotConnectError + Rails.logger.fatal("Error connecting to Redis on #{redis_url} (Errno::ECONNREFUSED)") + raise 'FATAL: connection to Redis refused' +end diff --git a/config/initializers/redis_store.rb b/config/initializers/redis_store.rb new file mode 100644 index 0000000..e697705 --- /dev/null +++ b/config/initializers/redis_store.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Rack + module Session + # redis store configuration class to act as session_store (cache_store) + class Redis + def set_session(env, session_id, new_session, options) + with_lock env, false do + with do |c| + new_options = if env['api_v2.session_lifetime'] + x = ActionDispatch::Request::Session::Options.new \ + options.instance_variable_get(:@by), + options.instance_variable_get(:@env), + options.instance_variable_get(:@delegate) + x[:expire_after] = env['api_v2.session_lifetime'] + x + else + options + end + c.set(session_id, new_session, new_options) + end + session_id + end + end + end + end +end diff --git a/config/initializers/sessions_store.rb b/config/initializers/sessions_store.rb new file mode 100644 index 0000000..e36efa6 --- /dev/null +++ b/config/initializers/sessions_store.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Use cache_store as session_store for Rails sessions. Key default is '_barong_session' +Rails.application.config.session_store :cache_store, key: Barong::App.config.session_name, expire_after: 24.hours.seconds diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb new file mode 100644 index 0000000..8ef303e --- /dev/null +++ b/config/initializers/sidekiq.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +Sidekiq.configure_server do |config| + config.redis = { url: ENV.fetch('BARONG_REDIS_URL', 'redis://localhost:6379/1') } +end + +Sidekiq.configure_client do |config| + config.redis = { url: ENV.fetch('BARONG_REDIS_URL', 'redis://localhost:6379/1') } +end diff --git a/config/initializers/trusted_proxies.rb b/config/initializers/trusted_proxies.rb new file mode 100644 index 0000000..4312bab --- /dev/null +++ b/config/initializers/trusted_proxies.rb @@ -0,0 +1,8 @@ +# Extend default list of trusted proxies with generic private and cloudflare proxy list + +# Cloudflare proxies list +# config/cloudflare_ips.yml fetches every time you build an image. Check Dockerfile l54, l55 +cloudflare_ips = File.read('config/cloudflare_ips.yml').split(/\R+/) +extend_proxies = cloudflare_ips.map { |proxy| IPAddr.new(proxy) } + +Rails.application.config.action_dispatch.trusted_proxies = ActionDispatch::RemoteIp::TRUSTED_PROXIES + extend_proxies diff --git a/config/initializers/user_storage_titles.rb b/config/initializers/user_storage_titles.rb new file mode 100644 index 0000000..2be9296 --- /dev/null +++ b/config/initializers/user_storage_titles.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# whitelisted data storage titles definitions +class UserStorageTitles + class << self + def list + @list ||= YAML.load_file(Barong::App.config.config)['user_storage_titles'] || [] + end + end +end diff --git a/config/initializers/vault.rb b/config/initializers/vault.rb new file mode 100644 index 0000000..f4129ed --- /dev/null +++ b/config/initializers/vault.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'vault/rails' + +Vault::Rails.configure do |config| + config.enabled = Rails.env.production? + config.address = Barong::App.config.vault_address + config.token = Barong::App.config.vault_token + config.ssl_verify = false + config.timeout = 60 + config.application = Barong::App.config.vault_app_name +end + +if Barong::App.config.vault_token.to_s != '' + def renew_process + token = Vault.auth_token.lookup(Vault.token) + time = token.data[:ttl] * (1 + rand) * 0.1 + Rails.logger.debug '[VAULT] Token will renew in %.0f sec' % time + sleep(time) + Vault.auth_token.renew(token.data[:id]) + Rails.logger.info '[VAULT] Token renewed' + end + + # where connect to vault + token = Vault.auth_token.lookup(Vault.token) + + if token.data[:renewable] + Rails.logger.info '[VAULT] Starting token renew thread' + Thread.new do + loop do + renew_process + rescue StandardError => e + report_exception(e) + sleep 60 + end + end + else + Rails.logger.info '[VAULT] Token is not renewable' + end +else + Rails.logger.warn 'Environment variable BARONG_VAULT_TOKEN is missing' +end diff --git a/config/initializers/versioning.rb b/config/initializers/versioning.rb new file mode 100644 index 0000000..49dca88 --- /dev/null +++ b/config/initializers/versioning.rb @@ -0,0 +1,12 @@ +# encoding: UTF-8 +# frozen_string_literal: true +# This file is auto-generated from the current state of VCS. +# Instead of editing this file, please use bin/gendocs. +module Barong + class Application + GIT_TAG = '2.7.0' + GIT_SHA = '3d7fe81' + BUILD_DATE = '2020-12-08 15:09:53+02:00' + VERSION = GIT_TAG + end +end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000..bbfc396 --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 0000000..decc5a8 --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,33 @@ +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at http://guides.rubyonrails.org/i18n.html. + +en: + hello: "Hello world" diff --git a/config/management_api_v1.yml b/config/management_api_v1.yml new file mode 100644 index 0000000..b58b77a --- /dev/null +++ b/config/management_api_v1.yml @@ -0,0 +1,54 @@ +# Be sure to restart your server when you modify this file. +# +# This file keeps all the security configuration variables for «Management API v1». +# +# Keeps all the public keys used to validate signatures. +# All values must be presented in PEM format and be URL-safe Base64 encoded. +# +# Example: +# +# keychain: +# backend-1.mycompany.example: +# algorithm: RS256 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3UjNPT1RQbzZvZE8wM3hXVDRNawp6TXJuM2pQS2pVdW0rVkc5dUZWODZNejVnMm1ueXdSRDc4MEY4aXVaZm41SGtROFpTUlFHYlRHNnB1dlVWWDFCClA0MWIrUW52VHFtWFhHcE9aSklzV3V2cHA4dHpZenFOejUvcTRRdUZQWDlrczdtaVV2dkNzbmo5S21Wb08yMU4KUVgyOWZUNkRJYldkUnJvWU1IOHloVmRrSjRVQnhYeHlSWmZ4VnN4UFVwckNodEgxN1JwNnQvYVRTR0VZNndQNwpKbEVCZi9Gb0djQk15OU5BOWhqZFMyMWxGcmVYeXdaUzZYdmhrN3dydGJWT2didU5EajdVeWhjS0RCaHA4c2VjCkV4TlB6d2p4ckhGTzhZaitFejBCMmZKQ1FDWW9SVG1kTzVEQS9kRTFHQmtqeXRCZjhDdGVIdExXcmZIU2g5em0KNlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== +# backend-2.mycompany.example: +# algorithm: HS384 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFyZm53OXpGMDRCaFlZTk5mblVKawppK2YwWUE2RitDdlZtMGozOEV1c2E2ZHdkRnBaMEFhK0dzRjlEWGpuOXgzTjdpZzlxNnFmbTN5TzdJbmxqZmdZCmp4eU12MmdXcTNTZmhySmZpUWd3dmh2NHJiMzJiTmc3ckxPTVJmenVDeUQ2aFBQU2FueTM3ZnhSNmxKR3E5SUcKUTRJa3JPNmZIOGozUllQVDBGUVlJcXg1a2pNbU9wczFlV2xTR1RYbDZWSDNtVWxxTWVMSjJjL1NMZ3Y3dUxDagpmMVpDTXFELzB4VERWZ0ZxbzJSSlRucEcvUzlXRHdpZ3U0aTdyY1VkeDcrQUMvK2lNSGpNL1VZekRtWHJDQkpaCjBnSUVPV1ZwM2dMMzNocmRZci9uNU11SFRiMXpJUHdJQW1ySHpmWFpwOEQxNVVUdGtENktXU1FINWQ5SnAvVncKN3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== +# backend-3.mycompany.example: +# algorithm: RS512 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUEzL2VIUUd1V2NFUHhwY2pxaVN5SQpLYmFlTFhnNHhrTFcwMU12eHlPVENMeHNrbVk5T0ViVTBQa2ptaFk1bWhiOXpmVjJqMVBmbXdXQmp5bXJ2alJJCmlWMDdxczA1OUx3UGhyL0JZc0lnYk13bWVEZUp5VHlxTmFvckNkVklhamxMVHhGSklNOGtXTUg1TUcrR2U0b2wKc1VWblEwRDNZdURvNCtPSzJiSDBOY3M5ZEFTWGx4T0lsenpZTlFkUTNCSUxZbFE3MW9CRDNmTWRyaXQwSktVYwphb25jTXJ0RlB2WlRuZjZROUhVM2VpR3NxTHhWczVZRTBGUlVsd0Z4TjZzbnZNQW9rNEpRT3A5cUExUi96WHMwCnRWdUVpeEJ1Sm5jSjZSYlp5UVp5YWg3YzAxd1NLcks1UTRudVh3VlhXUkhvd0gxOTZHMU9TODhoYmVZNHNneTkKdForZ3NsdlZIYWJmK1JqK0JtZmNOYmx2T1lQWlI4K3dVQ2pOWW43L1BWRjQ0NDRzUCtTZkEyejdvaU1OVStrVwpGVHM5dVdRNGxzQ0RIQnd6ZXNFMDc0dmZLMHF4c0NNN0hWeHhVc2gvNGx3aGppZTgrTFNqdzliWTlrcjJxWWFmClRMRnpoVWl2clJMaEpWdnZkQmRRYlBIUFVybnJ2TnFqblRYOWtFcWR4bktrbWluWmhtbU1pakpKa1dGREgwbmgKNmR4ZlZwZ2puYXUzWmoyZkNSNFV6aXNXK25MaExUc1Fadzd0b0NaVUo2aWV6N0psOEpJYWdBVWJ5OW8ycUN4ZgpWcTBwK3pwRlZKUFZzelVQNkZMTzh3Tjd5aThVT3l4T3VmdmEvOHgySmRJTzJ4emlPc2ZLVnhIZTFZRkZtWVdtCnZKaWZ1ZGUrdCtBbS9EM1FMSjlpK2JNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo= +# +# +keychain: {} + +# +# Keeps all the JWT verification options. +# The documentation is available at https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names. +# Check lib/jwt/default_options.rb for the default options. +# +jwt: {} + +# +# Keep all the API security scopes. +# +# The API security scope consists of scope name, list of permitted and mandatory signers. +# The scope name is associated with API actions. For example, write_labels is associated with +# ability to create private labels, edit and remove them. The scopes which have «write» in the name are supposed +# to be dangerous so they must require more signatures then read-only scopes +# +# Each scope must include list of permitted and mandatory signers. +# Barong validates JWT signatures against permitted keys and doesn't trust +# JWTs which don't include signatures from all mandatory signers. +# +# Example: +# +# scopes: +# write_labels: +# permitted_signers: ['backend-1.mycompany.example', 'backend-2.mycompany.example'] +# mandatory_signers: ['backend-1.mycompany.example', 'backend-2.mycompany.example'] +# +# The available scopes include: +# – write_labels +# - otp_sign +# +scopes: {} diff --git a/config/plugins.yml b/config/plugins.yml new file mode 100644 index 0000000..d2068d2 --- /dev/null +++ b/config/plugins.yml @@ -0,0 +1,22 @@ +# Be sure to run "bin/install_plugins" when you modify this file. +# +# This file is used for listing Barong plugins. +# The plugins must be listed as an array. Each element of array described the plugin using the next variables: +# +# * name +# Mandatory. Specify plugin name. The name must be compatible with filesystem rules. +# +# * git +# Mandatory. Specify Git repository URL. +# +# * commit +# Mandatory. Specify commit hash which Barong will checkout via "bin/install_plugins". +# +# * require +# Optional. Specify relative path to file from vendor/plugins which should be required by Barong. +# You may set this value to "false" if you don't want Barong to require the plugin. + +# Example configuration: +#- name: peatio-plugin-example +# git: https://github.com/rubykube/peatio-plugin-example.git +# commit: 11edd2e7c0bef229516f42fde79615a68a008d45 diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 0000000..033b41a --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,37 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers: a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum; this matches the default thread size of Active Record. +# + +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { 1 } +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 6 } + +threads min_threads_count, max_threads_count + +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +workers ENV.fetch("WEB_CONCURRENCY") { 1 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# +# preload_app! + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 0000000..2edee4b --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,4 @@ +Rails.application.routes.draw do + match '/api/v2/auth/*path', to: AuthorizeController.action(:authorize), via: :all + mount API::Base, at: '/api' +end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 0000000..9fa7863 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w[ + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +].each { |path| Spring.watch(path) } diff --git a/config/storage.yml b/config/storage.yml new file mode 100644 index 0000000..d32f76e --- /dev/null +++ b/config/storage.yml @@ -0,0 +1,34 @@ +test: + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] diff --git a/config/templates/management_api_v1.yml.erb b/config/templates/management_api_v1.yml.erb new file mode 100644 index 0000000..b58b77a --- /dev/null +++ b/config/templates/management_api_v1.yml.erb @@ -0,0 +1,54 @@ +# Be sure to restart your server when you modify this file. +# +# This file keeps all the security configuration variables for «Management API v1». +# +# Keeps all the public keys used to validate signatures. +# All values must be presented in PEM format and be URL-safe Base64 encoded. +# +# Example: +# +# keychain: +# backend-1.mycompany.example: +# algorithm: RS256 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUF3UjNPT1RQbzZvZE8wM3hXVDRNawp6TXJuM2pQS2pVdW0rVkc5dUZWODZNejVnMm1ueXdSRDc4MEY4aXVaZm41SGtROFpTUlFHYlRHNnB1dlVWWDFCClA0MWIrUW52VHFtWFhHcE9aSklzV3V2cHA4dHpZenFOejUvcTRRdUZQWDlrczdtaVV2dkNzbmo5S21Wb08yMU4KUVgyOWZUNkRJYldkUnJvWU1IOHloVmRrSjRVQnhYeHlSWmZ4VnN4UFVwckNodEgxN1JwNnQvYVRTR0VZNndQNwpKbEVCZi9Gb0djQk15OU5BOWhqZFMyMWxGcmVYeXdaUzZYdmhrN3dydGJWT2didU5EajdVeWhjS0RCaHA4c2VjCkV4TlB6d2p4ckhGTzhZaitFejBCMmZKQ1FDWW9SVG1kTzVEQS9kRTFHQmtqeXRCZjhDdGVIdExXcmZIU2g5em0KNlFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== +# backend-2.mycompany.example: +# algorithm: HS384 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFyZm53OXpGMDRCaFlZTk5mblVKawppK2YwWUE2RitDdlZtMGozOEV1c2E2ZHdkRnBaMEFhK0dzRjlEWGpuOXgzTjdpZzlxNnFmbTN5TzdJbmxqZmdZCmp4eU12MmdXcTNTZmhySmZpUWd3dmh2NHJiMzJiTmc3ckxPTVJmenVDeUQ2aFBQU2FueTM3ZnhSNmxKR3E5SUcKUTRJa3JPNmZIOGozUllQVDBGUVlJcXg1a2pNbU9wczFlV2xTR1RYbDZWSDNtVWxxTWVMSjJjL1NMZ3Y3dUxDagpmMVpDTXFELzB4VERWZ0ZxbzJSSlRucEcvUzlXRHdpZ3U0aTdyY1VkeDcrQUMvK2lNSGpNL1VZekRtWHJDQkpaCjBnSUVPV1ZwM2dMMzNocmRZci9uNU11SFRiMXpJUHdJQW1ySHpmWFpwOEQxNVVUdGtENktXU1FINWQ5SnAvVncKN3dJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg== +# backend-3.mycompany.example: +# algorithm: RS512 +# value: LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQ0lqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FnOEFNSUlDQ2dLQ0FnRUEzL2VIUUd1V2NFUHhwY2pxaVN5SQpLYmFlTFhnNHhrTFcwMU12eHlPVENMeHNrbVk5T0ViVTBQa2ptaFk1bWhiOXpmVjJqMVBmbXdXQmp5bXJ2alJJCmlWMDdxczA1OUx3UGhyL0JZc0lnYk13bWVEZUp5VHlxTmFvckNkVklhamxMVHhGSklNOGtXTUg1TUcrR2U0b2wKc1VWblEwRDNZdURvNCtPSzJiSDBOY3M5ZEFTWGx4T0lsenpZTlFkUTNCSUxZbFE3MW9CRDNmTWRyaXQwSktVYwphb25jTXJ0RlB2WlRuZjZROUhVM2VpR3NxTHhWczVZRTBGUlVsd0Z4TjZzbnZNQW9rNEpRT3A5cUExUi96WHMwCnRWdUVpeEJ1Sm5jSjZSYlp5UVp5YWg3YzAxd1NLcks1UTRudVh3VlhXUkhvd0gxOTZHMU9TODhoYmVZNHNneTkKdForZ3NsdlZIYWJmK1JqK0JtZmNOYmx2T1lQWlI4K3dVQ2pOWW43L1BWRjQ0NDRzUCtTZkEyejdvaU1OVStrVwpGVHM5dVdRNGxzQ0RIQnd6ZXNFMDc0dmZLMHF4c0NNN0hWeHhVc2gvNGx3aGppZTgrTFNqdzliWTlrcjJxWWFmClRMRnpoVWl2clJMaEpWdnZkQmRRYlBIUFVybnJ2TnFqblRYOWtFcWR4bktrbWluWmhtbU1pakpKa1dGREgwbmgKNmR4ZlZwZ2puYXUzWmoyZkNSNFV6aXNXK25MaExUc1Fadzd0b0NaVUo2aWV6N0psOEpJYWdBVWJ5OW8ycUN4ZgpWcTBwK3pwRlZKUFZzelVQNkZMTzh3Tjd5aThVT3l4T3VmdmEvOHgySmRJTzJ4emlPc2ZLVnhIZTFZRkZtWVdtCnZKaWZ1ZGUrdCtBbS9EM1FMSjlpK2JNQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQo= +# +# +keychain: {} + +# +# Keeps all the JWT verification options. +# The documentation is available at https://github.com/jwt/ruby-jwt#support-for-reserved-claim-names. +# Check lib/jwt/default_options.rb for the default options. +# +jwt: {} + +# +# Keep all the API security scopes. +# +# The API security scope consists of scope name, list of permitted and mandatory signers. +# The scope name is associated with API actions. For example, write_labels is associated with +# ability to create private labels, edit and remove them. The scopes which have «write» in the name are supposed +# to be dangerous so they must require more signatures then read-only scopes +# +# Each scope must include list of permitted and mandatory signers. +# Barong validates JWT signatures against permitted keys and doesn't trust +# JWTs which don't include signatures from all mandatory signers. +# +# Example: +# +# scopes: +# write_labels: +# permitted_signers: ['backend-1.mycompany.example', 'backend-2.mycompany.example'] +# mandatory_signers: ['backend-1.mycompany.example', 'backend-2.mycompany.example'] +# +# The available scopes include: +# – write_labels +# - otp_sign +# +scopes: {} diff --git a/config/templates/plugins.yml.erb b/config/templates/plugins.yml.erb new file mode 100644 index 0000000..d2068d2 --- /dev/null +++ b/config/templates/plugins.yml.erb @@ -0,0 +1,22 @@ +# Be sure to run "bin/install_plugins" when you modify this file. +# +# This file is used for listing Barong plugins. +# The plugins must be listed as an array. Each element of array described the plugin using the next variables: +# +# * name +# Mandatory. Specify plugin name. The name must be compatible with filesystem rules. +# +# * git +# Mandatory. Specify Git repository URL. +# +# * commit +# Mandatory. Specify commit hash which Barong will checkout via "bin/install_plugins". +# +# * require +# Optional. Specify relative path to file from vendor/plugins which should be required by Barong. +# You may set this value to "false" if you don't want Barong to require the plugin. + +# Example configuration: +#- name: peatio-plugin-example +# git: https://github.com/rubykube/peatio-plugin-example.git +# commit: 11edd2e7c0bef229516f42fde79615a68a008d45 diff --git a/db/migrate/20181101143041_create_all_tables.rb b/db/migrate/20181101143041_create_all_tables.rb new file mode 100644 index 0000000..21a1965 --- /dev/null +++ b/db/migrate/20181101143041_create_all_tables.rb @@ -0,0 +1,81 @@ +class CreateAllTables < ActiveRecord::Migration[5.2] + def change + + create_table :users do |t| + t.string :uid, null: false + t.string :email, null: false + t.string :password_digest, null: false + t.string :role, default: "member", null: false + t.integer :level, default: 0, null: false + t.boolean :otp, default: false + t.string :state, default: "pending", null: false + t.timestamps + end + add_index :users, :uid, unique: true + add_index :users, :email, unique: true + + create_table :apikeys do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :kid, null: false + t.string :algorithm, null: false + t.string :scope + t.string :state, default: "active", null: false + t.timestamps + t.index [:user_id] + end + + create_table :documents do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :upload + t.string :doc_type + t.string :doc_number + t.date :doc_expire + t.text :metadata + t.timestamps + t.index [:user_id] + end + + create_table :labels do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :key, null: false + t.string :value, null: false + t.string :scope, default: "public", null: false + t.timestamps + t.index [:user_id] + t.index [:user_id, :key, :scope] + end + + create_table :levels do |t| + t.string :key, null: false + t.string :value + t.string :description + t.timestamps + end + + create_table :phones do |t| + t.integer :user_id, null: false, unsigned: true + t.string :country, null: false + t.string :number, null: false + t.string :code, limit: 5 + t.datetime :validated_at + t.timestamps + t.index [:user_id] + t.index [:number] + end + + create_table :profiles do |t| + t.bigint :user_id + t.string :first_name + t.string :last_name + t.date :dob + t.string :address + t.string :postcode + t.string :city + t.string :country + t.text :metadata + t.timestamps + t.index [:user_id] + end + + end +end diff --git a/db/migrate/20181115100105_create_activities.rb b/db/migrate/20181115100105_create_activities.rb new file mode 100644 index 0000000..1d5ca42 --- /dev/null +++ b/db/migrate/20181115100105_create_activities.rb @@ -0,0 +1,15 @@ +class CreateActivities < ActiveRecord::Migration[5.2] + def change + create_table :activities do |t| + t.references :user, null: false + t.string :user_ip, null: false + t.string :user_agent, null: false + t.string :topic, null: false + t.string :action, null: false + t.string :result, null: false + t.text :data, null: true + + t.timestamp :created_at # avoid updated_at as records not supposed to be updated + end + end +end diff --git a/db/migrate/20190108115333_add_referral_id_to_users_table.rb b/db/migrate/20190108115333_add_referral_id_to_users_table.rb new file mode 100644 index 0000000..2ad4e79 --- /dev/null +++ b/db/migrate/20190108115333_add_referral_id_to_users_table.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Add new field due to referral - affiliate relation +class AddReferralIdToUsersTable < ActiveRecord::Migration[5.2] + def change + add_column :users, :referral_id, :bigint, after: :state + end +end diff --git a/db/migrate/20190318133453_create_permissions.rb b/db/migrate/20190318133453_create_permissions.rb new file mode 100644 index 0000000..44956fc --- /dev/null +++ b/db/migrate/20190318133453_create_permissions.rb @@ -0,0 +1,13 @@ +class CreatePermissions < ActiveRecord::Migration[5.2] + def change + create_table :permissions do |t| + + t.string :action, null: false + t.string :role, null: false + t.string :verb, null: false + t.string :path, null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20190529114214_changes_in_activities_table.rb b/db/migrate/20190529114214_changes_in_activities_table.rb new file mode 100644 index 0000000..200cb9f --- /dev/null +++ b/db/migrate/20190529114214_changes_in_activities_table.rb @@ -0,0 +1,10 @@ +class ChangesInActivitiesTable < ActiveRecord::Migration[5.2] + def change + add_column :activities, :target_uid, :string, after: :user_id + add_column :activities, :category, :string, after: :target_uid + add_column :permissions, :topic, :string, after: :path + + add_index :activities, :target_uid + add_index :permissions, :topic + end +end diff --git a/db/migrate/20190813112503_create_restrictions.rb b/db/migrate/20190813112503_create_restrictions.rb new file mode 100644 index 0000000..2fe163d --- /dev/null +++ b/db/migrate/20190813112503_create_restrictions.rb @@ -0,0 +1,12 @@ +class CreateRestrictions < ActiveRecord::Migration[5.2] + def change + create_table :restrictions do |t| + + t.string :scope, limit: 64, null: false + t.string :value, limit: 64, null: false + t.string :state, limit: 16, default: 'enabled', null: false + + t.timestamps + end + end +end diff --git a/db/migrate/20190827080317_add_data_field_to_users_table.rb b/db/migrate/20190827080317_add_data_field_to_users_table.rb new file mode 100644 index 0000000..2771543 --- /dev/null +++ b/db/migrate/20190827080317_add_data_field_to_users_table.rb @@ -0,0 +1,5 @@ +class AddDataFieldToUsersTable < ActiveRecord::Migration[5.2] + def change + add_column :users, :data, :text, null: true, after: :role + end +end diff --git a/db/migrate/20190902032709_add_state_profiles.rb b/db/migrate/20190902032709_add_state_profiles.rb new file mode 100644 index 0000000..8258806 --- /dev/null +++ b/db/migrate/20190902032709_add_state_profiles.rb @@ -0,0 +1,5 @@ +class AddStateProfiles < ActiveRecord::Migration[5.2] + def change + add_column :profiles, :state, :integer, unsigned: true, limit: 1, after: :country + end +end diff --git a/db/migrate/20191122151630_create_data_storages.rb b/db/migrate/20191122151630_create_data_storages.rb new file mode 100644 index 0000000..b43c19e --- /dev/null +++ b/db/migrate/20191122151630_create_data_storages.rb @@ -0,0 +1,12 @@ +class CreateDataStorages < ActiveRecord::Migration[5.2] + def change + create_table :data_storages do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :title, limit: 64, null: false + t.text :data, limit: 5120, null: false + + t.timestamps + t.index [:user_id, :title], unique: true + end + end +end diff --git a/db/migrate/20191210090006_add_description_to_labels.rb b/db/migrate/20191210090006_add_description_to_labels.rb new file mode 100644 index 0000000..bf24a35 --- /dev/null +++ b/db/migrate/20191210090006_add_description_to_labels.rb @@ -0,0 +1,5 @@ +class AddDescriptionToLabels < ActiveRecord::Migration[5.2] + def change + add_column :labels, :description, :string, null: true, after: :scope + end +end diff --git a/db/migrate/20200318152130_add_default_state_to_profiles_table.rb b/db/migrate/20200318152130_add_default_state_to_profiles_table.rb new file mode 100644 index 0000000..0134757 --- /dev/null +++ b/db/migrate/20200318152130_add_default_state_to_profiles_table.rb @@ -0,0 +1,5 @@ +class AddDefaultStateToProfilesTable < ActiveRecord::Migration[5.2] + def change + change_column_default :profiles, :state, 'drafted' + end +end diff --git a/db/migrate/20200429082843_add_code_and_type_in_restrictions_table.rb b/db/migrate/20200429082843_add_code_and_type_in_restrictions_table.rb new file mode 100644 index 0000000..ac37912 --- /dev/null +++ b/db/migrate/20200429082843_add_code_and_type_in_restrictions_table.rb @@ -0,0 +1,6 @@ +class AddCodeAndTypeInRestrictionsTable < ActiveRecord::Migration[5.2] + def change + add_column :restrictions, :code, :integer, null: true, after: :value + add_column :restrictions, :category, :string, null: false, after: :id + end +end diff --git a/db/migrate/20200507104423_add_encrypted_secret.rb b/db/migrate/20200507104423_add_encrypted_secret.rb new file mode 100644 index 0000000..c0056a5 --- /dev/null +++ b/db/migrate/20200507104423_add_encrypted_secret.rb @@ -0,0 +1,5 @@ +class AddEncryptedSecret < ActiveRecord::Migration[5.2] + def change + add_column :apikeys, :secret_encrypted, :string, limit: 1024, after: :scope + end +end diff --git a/db/migrate/20200514123908_add_identificator_to_documents_table.rb b/db/migrate/20200514123908_add_identificator_to_documents_table.rb new file mode 100644 index 0000000..f4f3eb6 --- /dev/null +++ b/db/migrate/20200514123908_add_identificator_to_documents_table.rb @@ -0,0 +1,7 @@ +class AddIdentificatorToDocumentsTable < ActiveRecord::Migration[5.2] + def change + add_column :documents, :identificator, :string, after: :doc_expire + add_column :documents, :doc_issue, :date, before: :doc_expire + add_column :profiles, :applicant_id, :string, after: :user_id + end +end diff --git a/db/migrate/20200602075906_add_author_to_profiles.rb b/db/migrate/20200602075906_add_author_to_profiles.rb new file mode 100644 index 0000000..6e076b0 --- /dev/null +++ b/db/migrate/20200602075906_add_author_to_profiles.rb @@ -0,0 +1,5 @@ +class AddAuthorToProfiles < ActiveRecord::Migration[5.2] + def change + add_column :profiles, :author, :string, null: true, after: :user_id + end +end diff --git a/db/migrate/20200609144734_create_comments.rb b/db/migrate/20200609144734_create_comments.rb new file mode 100644 index 0000000..ed16c36 --- /dev/null +++ b/db/migrate/20200609144734_create_comments.rb @@ -0,0 +1,13 @@ +class CreateComments < ActiveRecord::Migration[5.2] + def change + create_table :comments do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :author_uid, limit: 16, null: false + t.string :title, limit: 64, null: false + t.text :data, limit: 5120, null: false + + t.timestamps + t.index :user_id + end + end +end diff --git a/db/migrate/20200701115721_add_doc_category_to_documents.rb b/db/migrate/20200701115721_add_doc_category_to_documents.rb new file mode 100644 index 0000000..93605a3 --- /dev/null +++ b/db/migrate/20200701115721_add_doc_category_to_documents.rb @@ -0,0 +1,5 @@ +class AddDocCategoryToDocuments < ActiveRecord::Migration[5.2] + def change + add_column :documents, :doc_category, :string + end +end diff --git a/db/migrate/20200902125225_make_api_key_kid_unique.rb b/db/migrate/20200902125225_make_api_key_kid_unique.rb new file mode 100644 index 0000000..55de1c2 --- /dev/null +++ b/db/migrate/20200902125225_make_api_key_kid_unique.rb @@ -0,0 +1,18 @@ +class MakeApiKeyKidUnique < ActiveRecord::Migration[5.2] + class APIKey < ActiveRecord::Base + self.table_name = :apikeys + end + + def up + duplicated_records = APIKey.select(:kid).group(:kid).having("count(*) > 1") + duplicated_records.each do |api_key| + APIKey.where(kid: api_key.kid).destroy_all + end + + add_index :apikeys, :kid, unique: true unless index_exists?(:apikeys, :kid) + end + + def down + remove_index :apikeys, :kid if index_exists?(:apikeys, :kid) + end +end diff --git a/db/migrate/20200904153034_add_service_accounts_and_related_changes.rb b/db/migrate/20200904153034_add_service_accounts_and_related_changes.rb new file mode 100644 index 0000000..c023ed3 --- /dev/null +++ b/db/migrate/20200904153034_add_service_accounts_and_related_changes.rb @@ -0,0 +1,50 @@ +class AddServiceAccountsAndRelatedChanges < ActiveRecord::Migration[5.2] + class APIKey < ActiveRecord::Base + self.table_name = :apikeys + end + + def up + create_table :service_accounts do |t| + t.string :uid, null: false + t.bigint :owner_id, null: false, unsigned: true + t.string :email, null: false + t.string :role, default: "service_account", null: false + t.integer :level, default: 0, null: false + t.string :state, default: "pending", null: false + + t.timestamps + end + + add_column :apikeys, :key_holder_account_id, :bigint, null: false, unsigned: true, after: :id + add_column :apikeys, :key_holder_account_type, :string, null: false, default: "User", after: :key_holder_account_id + + APIKey.find_each do |api_key| + api_key.key_holder_account_type = 'User' + api_key.key_holder_account_id = api_key.user_id + api_key.save! + end + + remove_column :apikeys, :user_id + add_index :apikeys, [:key_holder_account_type, :key_holder_account_id], name: :idx_apikey_on_account unless index_exists?(:service_accounts, [:key_holder_account_type, :key_holder_account_id]) + end + + def down + add_column :apikeys, :user_id, :bigint, unsigned: true, null: false, after: :key_holder_account_id + + APIKey.find_each do |api_key| + if api_key.key_holder_account_type == 'ServiceAccount' + api_key.destroy! + next + end + + api_key.user_id = api_key.key_holder_account_id + api_key.save! + end + + remove_column :apikeys, :key_holder_account_id + remove_column :apikeys, :key_holder_account_type + + remove_index :service_accounts, column: [:key_holder_account_type, :key_holder_account_id] if index_exists?(:service_accounts, [:key_holder_account_type, :key_holder_account_id]) + drop_table :service_accounts + end +end diff --git a/db/migrate/20200929143711_add_encrypted_values_to_phones_profiles_documents.rb b/db/migrate/20200929143711_add_encrypted_values_to_phones_profiles_documents.rb new file mode 100644 index 0000000..c22e573 --- /dev/null +++ b/db/migrate/20200929143711_add_encrypted_values_to_phones_profiles_documents.rb @@ -0,0 +1,57 @@ +class AddEncryptedValuesToPhonesProfilesDocuments < ActiveRecord::Migration[5.2] + def up + # Phones Table + add_column :phones, :number_encrypted, :string, null: false, after: :code + add_column :phones, :number_index, :bigint, null: false, after: :number_encrypted + add_index :phones, [:number_index] + + remove_column :phones, :number, :string + + # Profiles Table + add_column :profiles, :first_name_encrypted, :string, limit: 1024, after: :applicant_id + add_column :profiles, :last_name_encrypted, :string, limit: 1024, after: :first_name_encrypted + add_column :profiles, :dob_encrypted, :string, after: :last_name_encrypted + add_column :profiles, :address_encrypted, :string, limit: 1024, after: :dob_encrypted + + + remove_column :profiles, :first_name, :string + remove_column :profiles, :last_name, :string + remove_column :profiles, :dob, :date + remove_column :profiles, :address, :string + + # Documents table + add_column :documents, :doc_number_encrypted, :string, after: :doc_expire + add_column :documents, :doc_number_index, :bigint, after: :doc_number_encrypted + add_index :documents, [:doc_number_index] + + remove_column :documents, :doc_number, :string + end + + def down + # Phones Table + add_column :phones, :number, :string, null: false, after: :country + + remove_index :phones, column: :number_index + remove_column :phones, :number_encrypted, :string + remove_column :phones, :number_index, :bigint + + # Profiles Table + add_column :profiles, :first_name, :string, after: :applicant_id + add_column :profiles, :last_name, :string, after: :first_name + add_column :profiles, :dob, :date, after: :last_name + add_column :profiles, :address, :string, after: :dob + + + remove_column :profiles, :first_name_encrypted, :string + remove_column :profiles, :last_name_encrypted, :string + remove_column :profiles, :dob_encrypted, :string + remove_column :profiles, :address_encrypted, :string + + # Documents table + add_column :documents, :doc_number, :string, after: :doc_type + + remove_index :documents, column: :doc_number_index + remove_column :documents, :doc_number_encrypted, :string + remove_column :documents, :doc_number_index, :bigint + end +end diff --git a/db/migrate/20201001112814_change_document_fields_position.rb b/db/migrate/20201001112814_change_document_fields_position.rb new file mode 100644 index 0000000..d185780 --- /dev/null +++ b/db/migrate/20201001112814_change_document_fields_position.rb @@ -0,0 +1,11 @@ +class ChangeDocumentFieldsPosition < ActiveRecord::Migration[5.2] + def up + change_column :documents, :doc_issue, :date, after: :doc_number_index + change_column :documents, :doc_category, :string, after: :doc_issue + end + + def down + change_column :documents, :doc_issue, :date, after: :updated_at + change_column :documents, :doc_category, :string, after: :doc_issue + end +end diff --git a/db/migrate/20210218135634_change_owner_id_requirement.rb b/db/migrate/20210218135634_change_owner_id_requirement.rb new file mode 100644 index 0000000..78b58ea --- /dev/null +++ b/db/migrate/20210218135634_change_owner_id_requirement.rb @@ -0,0 +1,9 @@ +class ChangeOwnerIdRequirement < ActiveRecord::Migration[5.2] + def up + change_column :service_accounts, :owner_id, :bigint, null: true, unsigned: true + end + + def down + change_column :service_accounts, :owner_id, :bigint, null: false, unsigned: true + end +end diff --git a/db/migrate/20210413065736_add_national_code_to_profiles.rb b/db/migrate/20210413065736_add_national_code_to_profiles.rb new file mode 100644 index 0000000..858ba4c --- /dev/null +++ b/db/migrate/20210413065736_add_national_code_to_profiles.rb @@ -0,0 +1,5 @@ +class AddNationalCodeToProfiles < ActiveRecord::Migration[5.2] + def change + add_column :profiles, :national_code_encrypted, :string + end +end diff --git a/db/migrate/20210418085508_create_treasuries.rb b/db/migrate/20210418085508_create_treasuries.rb new file mode 100644 index 0000000..5c1c867 --- /dev/null +++ b/db/migrate/20210418085508_create_treasuries.rb @@ -0,0 +1,21 @@ +class CreateTreasuries < ActiveRecord::Migration[5.2] + def down + drop_table :treasuries + end + + def up + create_table :treasuries do |t| + t.bigint :user_id, null: false, unsigned: true + t.string :title, limit: 128, null: true + t.string :data, null: false + t.text :result + t.integer :state, default: 0, null: false, unsigned: true + t.integer :kind, default: 0, null: false, unsigned: true + t.timestamps + end + add_index :treasuries, :user_id + add_index :treasuries, :data, unique: true + add_index :treasuries, %i[user_id title], unique: true + end +end + diff --git a/db/migrate/20210420111532_create_provinces.rb b/db/migrate/20210420111532_create_provinces.rb new file mode 100644 index 0000000..cca2f28 --- /dev/null +++ b/db/migrate/20210420111532_create_provinces.rb @@ -0,0 +1,12 @@ +class CreateProvinces < ActiveRecord::Migration[5.2] + def down + drop_table :provinces + end + + def up + create_table :provinces do |t| + t.string :name + t.timestamps + end + end +end diff --git a/db/migrate/20210420111629_create_cities.rb b/db/migrate/20210420111629_create_cities.rb new file mode 100644 index 0000000..86582d2 --- /dev/null +++ b/db/migrate/20210420111629_create_cities.rb @@ -0,0 +1,14 @@ +class CreateCities < ActiveRecord::Migration[5.2] + def down + drop_table :cities + end + def up + create_table :cities do |t| + t.string :name + t.references :province, foreign_key: true + t.timestamps + end + end +end + + diff --git a/db/migrate/20210420113539_change_city_in_profile.rb b/db/migrate/20210420113539_change_city_in_profile.rb new file mode 100644 index 0000000..0825440 --- /dev/null +++ b/db/migrate/20210420113539_change_city_in_profile.rb @@ -0,0 +1,5 @@ +class ChangeCityInProfile < ActiveRecord::Migration[5.2] + def change + add_reference :profiles, :city + end +end diff --git a/db/migrate/20210425062048_add_step_to_phone.rb b/db/migrate/20210425062048_add_step_to_phone.rb new file mode 100644 index 0000000..cb447c6 --- /dev/null +++ b/db/migrate/20210425062048_add_step_to_phone.rb @@ -0,0 +1,13 @@ +class AddStepToPhone < ActiveRecord::Migration[5.2] + def up + add_column :phones, :category, :integer, null: false + add_column :phones, :state, :integer, null: true, default: 0 + add_column :phones, :step, :integer, null: true, default: 0 + end + + def down + remove_column :phones, :category + remove_column :phones, :state + remove_column :phones, :step + end +end diff --git a/db/migrate/20210714111301_add_state_to_documents.rb b/db/migrate/20210714111301_add_state_to_documents.rb new file mode 100644 index 0000000..0e62cb2 --- /dev/null +++ b/db/migrate/20210714111301_add_state_to_documents.rb @@ -0,0 +1,9 @@ +class AddStateToDocuments < ActiveRecord::Migration[5.2] + def up + add_column :documents, :state, :integer, null: false, default: 0 + end + + def down + remove_column :documents, :state + end +end diff --git a/db/migrate/20210811064241_remove_index_data_from_treasuries.rb b/db/migrate/20210811064241_remove_index_data_from_treasuries.rb new file mode 100644 index 0000000..b40237d --- /dev/null +++ b/db/migrate/20210811064241_remove_index_data_from_treasuries.rb @@ -0,0 +1,7 @@ +class RemoveIndexDataFromTreasuries < ActiveRecord::Migration[5.2] + def change + remove_index :treasuries, name: 'index_treasuries_on_data' + remove_index :treasuries, name: 'index_treasuries_on_user_id_and_title' + end + +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..6fa47c9 --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,213 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2021_08_11_064241) do + + create_table "activities", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false + t.string "target_uid" + t.string "category" + t.string "user_ip", null: false + t.string "user_agent", null: false + t.string "topic", null: false + t.string "action", null: false + t.string "result", null: false + t.text "data" + t.timestamp "created_at" + t.index ["target_uid"], name: "index_activities_on_target_uid" + t.index ["user_id"], name: "index_activities_on_user_id" + end + + create_table "apikeys", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "key_holder_account_id", null: false, unsigned: true + t.string "key_holder_account_type", default: "User", null: false + t.string "kid", null: false + t.string "algorithm", null: false + t.string "scope" + t.string "secret_encrypted", limit: 1024 + t.string "state", default: "active", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["key_holder_account_type", "key_holder_account_id"], name: "idx_apikey_on_account" + t.index ["kid"], name: "index_apikeys_on_kid", unique: true + end + + create_table "cities", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "name" + t.bigint "province_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["province_id"], name: "index_cities_on_province_id" + end + + create_table "comments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false, unsigned: true + t.string "author_uid", limit: 16, null: false + t.string "title", limit: 64, null: false + t.text "data", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_comments_on_user_id" + end + + create_table "data_storages", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false, unsigned: true + t.string "title", limit: 64, null: false + t.text "data", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id", "title"], name: "index_data_storages_on_user_id_and_title", unique: true + end + + create_table "documents", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false, unsigned: true + t.string "upload" + t.string "doc_type" + t.date "doc_expire" + t.string "doc_number_encrypted" + t.bigint "doc_number_index" + t.date "doc_issue" + t.string "doc_category" + t.string "identificator" + t.text "metadata" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "state", default: 0, null: false + t.index ["doc_number_index"], name: "index_documents_on_doc_number_index" + t.index ["user_id"], name: "index_documents_on_user_id" + end + + create_table "labels", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false, unsigned: true + t.string "key", null: false + t.string "value", null: false + t.string "scope", default: "public", null: false + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id", "key", "scope"], name: "index_labels_on_user_id_and_key_and_scope" + t.index ["user_id"], name: "index_labels_on_user_id" + end + + create_table "levels", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "key", null: false + t.string "value" + t.string "description" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "permissions", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "action", null: false + t.string "role", null: false + t.string "verb", null: false + t.string "path", null: false + t.string "topic" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["topic"], name: "index_permissions_on_topic" + end + + create_table "phones", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.integer "user_id", null: false, unsigned: true + t.string "country", null: false + t.string "code", limit: 5 + t.string "number_encrypted", null: false + t.bigint "number_index", null: false + t.datetime "validated_at" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "category", null: false + t.integer "state", default: 0 + t.integer "step", default: 0 + t.index ["number_index"], name: "index_phones_on_number_index" + t.index ["user_id"], name: "index_phones_on_user_id" + end + + create_table "profiles", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id" + t.string "author" + t.string "applicant_id" + t.string "first_name_encrypted", limit: 1024 + t.string "last_name_encrypted", limit: 1024 + t.string "dob_encrypted" + t.string "address_encrypted", limit: 1024 + t.string "postcode" + t.string "city" + t.string "country" + t.integer "state", limit: 1, default: 0, unsigned: true + t.text "metadata" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "national_code_encrypted" + t.bigint "city_id" + t.index ["city_id"], name: "index_profiles_on_city_id" + t.index ["user_id"], name: "index_profiles_on_user_id" + end + + create_table "provinces", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "restrictions", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "category", null: false + t.string "scope", limit: 64, null: false + t.string "value", limit: 64, null: false + t.integer "code" + t.string "state", limit: 16, default: "enabled", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "service_accounts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "uid", null: false + t.bigint "owner_id", unsigned: true + t.string "email", null: false + t.string "role", default: "service_account", null: false + t.integer "level", default: 0, null: false + t.string "state", default: "pending", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + create_table "treasuries", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.bigint "user_id", null: false, unsigned: true + t.string "title", limit: 128 + t.string "data", null: false + t.text "result" + t.integer "state", default: 0, null: false, unsigned: true + t.integer "kind", default: 0, null: false, unsigned: true + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_treasuries_on_user_id" + end + + create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + t.string "uid", null: false + t.string "email", null: false + t.string "password_digest", null: false + t.string "role", default: "member", null: false + t.text "data" + t.integer "level", default: 0, null: false + t.boolean "otp", default: false + t.string "state", default: "pending", null: false + t.bigint "referral_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["uid"], name: "index_users_on_uid", unique: true + end + + add_foreign_key "cities", "provinces" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 0000000..0d8ce92 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require_dependency 'barong/seed' + +seed = Barong::Seed.new +seed.seed_levels +seed.seed_permissions +seed.seed_users + +seed.seed_superadmin +seed.seed_restrictions +# seed.seed_provinces +# seed.seed_cities +puts seed.inspect diff --git a/db/structure.sql b/db/structure.sql new file mode 100644 index 0000000..39610e0 --- /dev/null +++ b/db/structure.sql @@ -0,0 +1,266 @@ + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8 */; +/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; +/*!40103 SET TIME_ZONE='+00:00' */; +/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; +/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; +/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; +DROP TABLE IF EXISTS `activities`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `activities` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) NOT NULL, + `target_uid` varchar(255) DEFAULT NULL, + `category` varchar(255) DEFAULT NULL, + `user_ip` varchar(255) NOT NULL, + `user_agent` varchar(255) NOT NULL, + `topic` varchar(255) NOT NULL, + `action` varchar(255) NOT NULL, + `result` varchar(255) NOT NULL, + `data` text, + `created_at` timestamp NULL DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_activities_on_user_id` (`user_id`), + KEY `index_activities_on_target_uid` (`target_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `apikeys`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `apikeys` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `kid` varchar(255) NOT NULL, + `algorithm` varchar(255) NOT NULL, + `scope` varchar(255) DEFAULT NULL, + `state` varchar(255) NOT NULL DEFAULT 'active', + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_apikeys_on_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `ar_internal_metadata`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `ar_internal_metadata` ( + `key` varchar(255) NOT NULL, + `value` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `comments`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `comments` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `author_uid` varchar(16) NOT NULL, + `title` varchar(64) NOT NULL, + `data` text NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_comments_on_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `data_storages`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `data_storages` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `title` varchar(64) NOT NULL, + `data` text NOT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `index_data_storages_on_user_id_and_title` (`user_id`,`title`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `documents`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `documents` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `upload` varchar(255) DEFAULT NULL, + `doc_type` varchar(255) DEFAULT NULL, + `doc_number` varchar(255) DEFAULT NULL, + `doc_expire` date DEFAULT NULL, + `identificator` varchar(255) DEFAULT NULL, + `metadata` text, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + `doc_issue` date DEFAULT NULL, + `doc_category` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `index_documents_on_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `labels`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `labels` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) unsigned NOT NULL, + `key` varchar(255) NOT NULL, + `value` varchar(255) NOT NULL, + `scope` varchar(255) NOT NULL DEFAULT 'public', + `description` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_labels_on_user_id` (`user_id`), + KEY `index_labels_on_user_id_and_key_and_scope` (`user_id`,`key`,`scope`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `levels`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `levels` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `key` varchar(255) NOT NULL, + `value` varchar(255) DEFAULT NULL, + `description` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `permissions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `permissions` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `action` varchar(255) NOT NULL, + `role` varchar(255) NOT NULL, + `verb` varchar(255) NOT NULL, + `path` varchar(255) NOT NULL, + `topic` varchar(255) DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_permissions_on_topic` (`topic`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `phones`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `phones` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `country` varchar(255) NOT NULL, + `number` varchar(255) NOT NULL, + `code` varchar(5) DEFAULT NULL, + `validated_at` datetime DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_phones_on_user_id` (`user_id`), + KEY `index_phones_on_number` (`number`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `profiles`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `profiles` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `user_id` bigint(20) DEFAULT NULL, + `author` varchar(255) DEFAULT NULL, + `applicant_id` varchar(255) DEFAULT NULL, + `first_name` varchar(255) DEFAULT NULL, + `last_name` varchar(255) DEFAULT NULL, + `dob` date DEFAULT NULL, + `address` varchar(255) DEFAULT NULL, + `postcode` varchar(255) DEFAULT NULL, + `city` varchar(255) DEFAULT NULL, + `country` varchar(255) DEFAULT NULL, + `state` tinyint(3) unsigned DEFAULT '0', + `metadata` text, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + KEY `index_profiles_on_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `restrictions`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `restrictions` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `category` varchar(255) NOT NULL, + `scope` varchar(64) NOT NULL, + `value` varchar(64) NOT NULL, + `code` int(11) DEFAULT NULL, + `state` varchar(16) NOT NULL DEFAULT 'enabled', + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `schema_migrations`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `schema_migrations` ( + `version` varchar(255) NOT NULL, + PRIMARY KEY (`version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +DROP TABLE IF EXISTS `users`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8 */; +CREATE TABLE `users` ( + `id` bigint(20) NOT NULL AUTO_INCREMENT, + `uid` varchar(255) NOT NULL, + `email` varchar(255) NOT NULL, + `password_digest` varchar(255) NOT NULL, + `role` varchar(255) NOT NULL DEFAULT 'member', + `data` text, + `level` int(11) NOT NULL DEFAULT '0', + `otp` tinyint(1) DEFAULT '0', + `state` varchar(255) NOT NULL DEFAULT 'pending', + `referral_id` bigint(20) DEFAULT NULL, + `created_at` datetime NOT NULL, + `updated_at` datetime NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `index_users_on_uid` (`uid`), + UNIQUE KEY `index_users_on_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +/*!40101 SET character_set_client = @saved_cs_client */; +/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; + +/*!40101 SET SQL_MODE=@OLD_SQL_MODE */; +/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; +/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; +/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; + +INSERT INTO `schema_migrations` (version) VALUES +('20181101143041'), +('20181115100105'), +('20190108115333'), +('20190318133453'), +('20190529114214'), +('20190813112503'), +('20190827080317'), +('20190902032709'), +('20191122151630'), +('20191210090006'), +('20200318152130'), +('20200429082843'), +('20200514123908'), +('20200602075906'), +('20200609144734'), +('20200701115721'); + + diff --git a/docs/api/barong_admin_api_v2.md b/docs/api/barong_admin_api_v2.md new file mode 100644 index 0000000..23521e0 --- /dev/null +++ b/docs/api/barong_admin_api_v2.md @@ -0,0 +1,976 @@ +# Barong +RESTful AdminAPI for barong OAuth server + +## Version: 2.7.0 + +### Security +**BearerToken** + +|basic|*Basic*| +|---|---| +|Description|Bearer Token authentication| +|Name|Authorization| +|In|header| + +### /api/v2/barong/admin/users/data_storage + +#### DELETE +##### Description + +Deletes user's data storage record + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | query | user uniq id | Yes | string | +| title | query | data storage uniq title | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 204 | Deletes user's data storage record | [API_V2_Admin_Entities_UserWithKYC](#api_v2_admin_entities_userwithkyc) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/users/{uid} + +#### GET +##### Description + +Returns user info + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | path | user uniq id | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns user info | [API_V2_Admin_Entities_UserWithKYC](#api_v2_admin_entities_userwithkyc) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/users/documents/pending + +#### GET +##### Description + +Returns array of users with pending or replaced documents as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| extended | query | When true endpoint returns full information about users | No | boolean | +| uid | query | | No | string | +| email | query | | No | string | +| role | query | | No | string | +| first_name | query | | No | string | +| last_name | query | | No | string | +| country | query | | No | string | +| level | query | | No | integer | +| state | query | | No | string | +| range | query | | No | string | +| from | query | An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved. | No | integer | +| to | query | An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved. | No | integer | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of users with pending or replaced documents as paginated collection | [API_V2_Entities_User](#api_v2_entities_user) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/users + +#### PUT +##### Description + +Update user attributes + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| email | formData | User Email | No | string | +| state | formData | user state | No | string | +| otp | formData | user 2fa status | No | boolean | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | User attributes were created | +| 401 | Invalid bearer token | + +#### GET +##### Description + +Returns array of users as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| extended | query | When true endpoint returns full information about users | No | boolean | +| uid | query | | No | string | +| email | query | | No | string | +| role | query | | No | string | +| country | query | | No | string | +| level | query | | No | integer | +| state | query | | No | string | +| range | query | | No | string | +| ordering | query | If set, returned values will be sorted in specific order, defaults to 'asc'. | No | string | +| order_by | query | Name of the field, which result will be ordered by. | No | string | +| from | query | An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved. | No | integer | +| to | query | An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved. | No | integer | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of users as paginated collection | [API_V2_Entities_User](#api_v2_entities_user) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/users/role + +#### POST +##### Description + +Update user role + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| role | formData | user role | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | User role was created | +| 401 | Invalid bearer token | + +### /api/v2/barong/admin/users/update + +#### POST +##### Description + +Update user attributes + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| state | formData | user state | No | string | +| otp | formData | user 2fa status | No | boolean | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | User attributes were updated | +| 401 | Invalid bearer token | + +### /api/v2/barong/admin/users/labels + +#### DELETE +##### Description + +Deletes label for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | query | user uniq id | Yes | string | +| key | query | label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | Yes | string | +| scope | query | label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Label was deleted | +| 401 | Invalid bearer token | + +#### PUT +##### Description + +Update user label scope + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| key | formData | Label key. | Yes | string | +| scope | formData | label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | Yes | string | +| description | formData | label description. [A-Za-z0-9_-] should be used. max - 255 characters. | No | string | +| value | formData | Label value. | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Label was updated | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | +| 422 | Validation errors | + +#### POST +##### Description + +Add label for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| key | formData | label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | Yes | string | +| value | formData | label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | Yes | string | +| description | formData | label description. [A-Za-z0-9_-] should be used. max - 255 characters. | No | string | +| scope | formData | Label scope: 'public' or 'private'. Default is public | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Label was created | +| 401 | Invalid bearer token | + +#### GET +##### Description + +Returns array of users as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | query | Label key | Yes | string | +| value | query | Label value | Yes | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of users as paginated collection | [API_V2_Entities_User](#api_v2_entities_user) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/users/labels/update + +#### POST +##### Description + +Update user label value + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| key | formData | Label key. | Yes | string | +| scope | formData | label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | Yes | string | +| value | formData | Label value. | Yes | string | +| description | formData | label description. [A-Za-z0-9_-] should be used. max - 255 characters. | No | string | +| replace | formData | When true label will be created if not exist | No | boolean | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Label was updated | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | +| 422 | Validation errors | + +### /api/v2/barong/admin/users/labels/list + +#### GET +##### Description + +Returns existing labels keys and values + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Returns existing labels keys and values | +| 401 | Invalid bearer token | + +### /api/v2/barong/admin/users/comments + +#### DELETE +##### Description + +Delete user's comment + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | query | comment uniq id | Yes | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 204 | Delete user's comment | [API_V2_Admin_Entities_UserWithKYC](#api_v2_admin_entities_userwithkyc) | +| 401 | Invalid bearer token | | + +#### PUT +##### Description + +Edit user's comment + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | formData | comment uniq id | Yes | integer | +| title | formData | comment title | No | string | +| data | formData | comment data | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Edit user's comment | [API_V2_Admin_Entities_UserWithKYC](#api_v2_admin_entities_userwithkyc) | +| 401 | Invalid bearer token | | + +#### POST +##### Description + +Adds new user's comment + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | user uniq id | Yes | string | +| title | formData | comment uniq title | Yes | string | +| data | formData | comment data | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Adds new user's comment | [API_V2_Admin_Entities_UserWithKYC](#api_v2_admin_entities_userwithkyc) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/api_keys + +#### GET +##### Description + +List all api keys for selected account. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | query | user uniq id | Yes | string | +| ordering | query | If set, returned values will be sorted in specific order, defaults to 'asc'. | No | string | +| order_by | query | Name of the field, which result will be ordered by. | No | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List all api keys for selected account. | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/permissions + +#### PUT +##### Description + +Update Permission + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | formData | Permission id | Yes | integer | +| role | formData | permission field - role | No | string | +| verb | formData | permission field - request verb | No | string | +| path | formData | permission field - request path | No | string | +| action | formData | | No | string | +| topic | formData | | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Permission was updated | +| 401 | Invalid bearer token | + +#### DELETE +##### Description + +Deletes permission + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | query | permission id | Yes | integer | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Permission was deleted | +| 401 | Invalid bearer token | + +#### POST +##### Description + +Create permission + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| role | formData | | Yes | string | +| verb | formData | | Yes | string | +| path | formData | | Yes | string | +| action | formData | | Yes | string | +| topic | formData | | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Permission was created | +| 401 | Invalid bearer token | + +#### GET +##### Description + +Returns array of permissions as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of permissions as paginated collection | [API_V2_Entities_Permission](#api_v2_entities_permission) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/activities/admin + +#### GET +##### Description + +Returns array of activities as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| topic | query | | No | string | +| action | query | | No | string | +| uid | query | | No | string | +| email | query | | No | string | +| from | query | An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved. | No | integer | +| to | query | An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved. | No | integer | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | +| target_uid | query | | No | string | +| range | query | | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of activities as paginated collection | [API_V2_Admin_Entities_AdminActivity](#api_v2_admin_entities_adminactivity) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/activities + +#### GET +##### Description + +Returns array of activities as paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| topic | query | | No | string | +| action | query | | No | string | +| uid | query | | No | string | +| email | query | | No | string | +| from | query | An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved. | No | integer | +| to | query | An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved. | No | integer | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of activities as paginated collection | [API_V2_Admin_Entities_ActivityWithUser](#api_v2_admin_entities_activitywithuser) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/metrics + +#### GET +##### Description + +Returns main statistic in the given time period + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| created_from | query | | No | string | +| created_to | query | | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Returns main statistic in the given time period | +| 401 | Invalid bearer token | + +### /api/v2/barong/admin/restrictions + +#### DELETE +##### Description + +Delete restriction + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | query | Restriction id | Yes | integer | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Restriction was deleted | +| 401 | Invalid bearer token | + +#### PUT +##### Description + +Update restriction + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| id | formData | Restriction id | Yes | integer | +| scope | formData | | No | string | +| category | formData | | No | string | +| value | formData | | No | string | +| state | formData | | No | string | +| code | formData | | No | integer | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Restriction was updated | +| 401 | Invalid bearer token | + +#### POST +##### Description + +Create restriction + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| scope | formData | | Yes | string | +| value | formData | | Yes | string | +| category | formData | | Yes | string | +| state | formData | | No | string | +| code | formData | | No | integer | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Restriction was created | +| 401 | Invalid bearer token | + +#### GET +##### Description + +Returns array of restrictions as a paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| scope | query | | No | string | +| category | query | | No | string | +| range | query | | No | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of restrictions as a paginated collection | [API_V2_Entities_Restriction](#api_v2_entities_restriction) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/restrictions/whitelink + +#### POST +##### Description + +Create whitelink + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| expire_time | formData | link will be active for (Time.now + expire_time in following range) | No | integer | +| range | formData | In combination with expire_time gives full controll over token expiration | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Created whitelink | +| 401 | Invalid bearer token | + +### /api/v2/barong/admin/profiles + +#### POST +##### Description + +Create a profile for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | | Yes | string | +| first_name | formData | | No | string | +| last_name | formData | | No | string | +| dob | formData | | No | date | +| address | formData | | No | string | +| postcode | formData | | No | string | +| city | formData | | No | string | +| country | formData | | No | string | +| metadata | formData | Any additional key: value pairs in json string format | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create a profile for user | [API_V2_Admin_Entities_Profile](#api_v2_admin_entities_profile) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 422 | Validation errors | | + +#### PUT +##### Description + +Verify user's profile + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | | Yes | string | +| state | formData | | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Verify user's profile | [API_V2_Admin_Entities_Profile](#api_v2_admin_entities_profile) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 422 | Validation errors | | + +#### GET +##### Description + +Return all profiles + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Return all profiles | [API_V2_Admin_Entities_Profile](#api_v2_admin_entities_profile) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/levels + +#### GET +##### Description + +Returns array of permissions as paginated collection + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns array of permissions as paginated collection | [API_V2_Entities_Level](#api_v2_entities_level) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/admin/abilities + +#### GET +##### Description + +Get all roles and admin_permissions of barong cancan. + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get all roles and admin_permissions of barong cancan. | + +### Models + +#### API_V2_Admin_Entities_UserWithKYC + +Adds new user's comment + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Admin_Entities_Profile](#api_v2_admin_entities_profile) | | No | +| labels | [API_V2_Entities_AdminLabelView](#api_v2_entities_adminlabelview) | | No | +| phones | [API_V2_Admin_Entities_Phone](#api_v2_admin_entities_phone) | | No | +| documents | [API_V2_Admin_Entities_Document](#api_v2_admin_entities_document) | | No | +| data_storages | [API_V2_Entities_DataStorage](#api_v2_entities_datastorage) | | No | +| comments | [API_V2_Entities_Comment](#api_v2_entities_comment) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Admin_Entities_Profile + +Return all profiles + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| first_name | string | First Name | No | +| last_name | string | Last name | No | +| dob | date | Birth date | No | +| address | string | Address | No | +| postcode | string | Address Postcode | No | +| city | string | City name | No | +| country | string | Country name | No | +| state | string | Profile state: drafted, submitted, verified, rejected | No | +| metadata | object | Profile additional fields | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_AdminLabelView + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| key | string | Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| scope | string | Label scope: 'public' or 'private' | No | +| description | string | Label desc: json string with any additional information | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Admin_Entities_Phone + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| country | string | Phone country | No | +| number | string | Phone number | No | +| validated_at | s (g) | Phone validation date | No | + +#### API_V2_Admin_Entities_Document + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| upload | string | File url | No | +| doc_type | string | Document type: passport, driver license, utility bill, identity card, institutional, address, residental | No | +| doc_number | string | document number: AB123123 type | No | +| doc_expire | string | Expire date of uploaded documents | No | +| metadata | string | Any additional stored data | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_DataStorage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| title | string | Any additional data title | No | +| data | string | Any additional data json key:value pairs | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Comment + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Comment id | No | +| author_uid | string | Comment author UID | No | +| title | string | Comment title | No | +| data | string | Comment plain text | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_User + +Returns array of users as paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | + +#### API_V2_Entities_APIKey + +List all api keys for selected account. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| kid | string | JWT public key | No | +| algorithm | string | Cryptographic hash function type | No | +| scope | string | Serialized array of scopes | No | +| state | string | active/non-active state of key | No | +| secret | string | Api key secret | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Permission + +Returns array of permissions as paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Permission id | No | +| action | string | Permission action: accept (allow access (drop access), audit (record activity) | No | +| role | string | Permission user role | No | +| verb | string | Permission verb: put, post, delete, get | No | +| path | string | API path | No | +| topic | string | Permission topic: general, session etc | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Admin_Entities_AdminActivity + +Returns array of activities as paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user_ip | string | User IP | No | +| user_agent | string | User Browser Agent | No | +| topic | string | Defined topic (session, adjustments) or general by default | No | +| action | string | API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method | No | +| result | string | Status of API response: succeed, failed, denied | No | +| data | string | Parameters which was sent to specific API endpoint | No | +| admin | [API_V2_Entities_User](#api_v2_entities_user) | | No | +| target | [API_V2_Entities_User](#api_v2_entities_user) | | No | +| created_at | string | | No | + +#### API_V2_Admin_Entities_ActivityWithUser + +Returns array of activities as paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| user_ip | string | User IP | No | +| user_agent | string | User Browser Agent | No | +| topic | string | Defined topic (session, adjustments) or general by default | No | +| action | string | API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method | No | +| result | string | Status of API response: succeed, failed, denied | No | +| data | string | Parameters which was sent to specific API endpoint | No | +| user | [API_V2_Entities_User](#api_v2_entities_user) | | No | +| created_at | string | | No | + +#### API_V2_Entities_Restriction + +Returns array of restrictions as a paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Restriction id | No | +| category | string | Restriction categories: blacklist, maintenance, whitelist, blocklogin | No | +| scope | string | Restriction scopes: continent, country, ip, ip_subnet, all | No | +| value | string | Restriction value: IP address, country abbreviation, all | No | +| code | integer | Restriction codes: {"continent"=>423, "country"=>423, "ip_subnet"=>403, "ip"=>401, "all"=>401} | No | +| state | string | Restriction states: disabled, enabled | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Level + +Returns array of permissions as paginated collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Level identifier, level number | No | +| key | string | Label key. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | + +#### API_V2_Admin_Entities_UserWithProfile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Admin_Entities_Profile](#api_v2_admin_entities_profile) | | No | +| created_at | string | | No | +| updated_at | string | | No | diff --git a/docs/api/barong_management_api_v2.md b/docs/api/barong_management_api_v2.md new file mode 100644 index 0000000..9194a54 --- /dev/null +++ b/docs/api/barong_management_api_v2.md @@ -0,0 +1,678 @@ +# Barong +Management API for barong OAuth server + +## Version: 2.7.0 + +### Security +**SecurityScope** + +|basic|*Basic*| +|---|---| +|Description|JWT should have signature keychains| +|Name|Authorization| + +### /api/v2/barong/management/labels/delete + +#### POST +##### Description + +Delete a label with 'private' scope + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| user_uid | formData | User uid | Yes | string | +| key | formData | Label key. | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Delete a label with 'private' scope | + +### /api/v2/barong/management/labels + +#### PUT +##### Description + +Update a label with 'private' scope + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| user_uid | formData | User uid | Yes | string | +| key | formData | Label key. | Yes | string | +| value | formData | Label value. | Yes | string | +| description | formData | Label desc. | No | string | +| replace | formData | When true label will be created if not exist | No | boolean | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Update a label with 'private' scope | [API_V2_Entities_Label](#api_v2_entities_label) | + +#### POST +##### Description + +Create a label with 'private' scope and assigns to users + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| user_uid | formData | User uid | Yes | string | +| key | formData | Label key. | Yes | string | +| value | formData | Label value. | Yes | string | +| description | formData | Label desc. | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create a label with 'private' scope and assigns to users | [API_V2_Entities_Label](#api_v2_entities_label) | + +### /api/v2/barong/management/labels/list + +#### POST +##### Description + +Get user collection filtered on label attributes + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| user_uid | formData | User uid | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get user collection filtered on label attributes | [API_V2_Entities_AdminLabelView](#api_v2_entities_adminlabelview) | + +### /api/v2/barong/management/labels/filter/users + +#### POST +##### Description + +Get all labels assigned to users + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | formData | Label key. | Yes | string | +| value | formData | Label value. | No | string | +| scope | formData | Label scope. | No | string | +| extended | formData | When true endpoint returns full information about users | No | boolean | +| range | formData | | No | string | +| page | formData | Page number (defaults to 1). | No | integer | +| limit | formData | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get all labels assigned to users | [API_V2_Entities_User](#api_v2_entities_user) | + +### /api/v2/barong/management/users/import + +#### POST +##### Description + +Imports an existing user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | User Email | Yes | string | +| password_digest | formData | User Password Hash | Yes | string | +| referral_uid | formData | Referral uid | No | string | +| phone | formData | Phone | No | string | +| first_name | formData | First Name | No | string | +| last_name | formData | Last Name | No | string | +| dob | formData | Birth date | No | date | +| address | formData | Address | No | string | +| postcode | formData | Postcode | No | string | +| city | formData | City | No | string | +| country | formData | Country | No | string | +| state | formData | State | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Imports an existing user | [API_V2_Management_Entities_UserWithProfile](#api_v2_management_entities_userwithprofile) | + +### /api/v2/barong/management/users/update + +#### POST +##### Description + +Updates role and data fields of existing user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User Uid | Yes | string | +| role | formData | User Role | No | string | +| data | formData | Any additional key:value pairs in json format | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Updates role and data fields of existing user | [API_V2_Management_Entities_UserWithProfile](#api_v2_management_entities_userwithprofile) | + +### /api/v2/barong/management/users + +#### POST +##### Description + +Creates new user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | User Email | Yes | string | +| password | formData | User Password | Yes | string | +| referral_uid | formData | Referral uid | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Creates new user | [API_V2_Management_Entities_UserWithProfile](#api_v2_management_entities_userwithprofile) | + +### /api/v2/barong/management/users/list + +#### POST +##### Description + +Returns array of users as collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| extended | formData | When true endpoint returns full information about users | No | boolean | +| range | formData | | No | string | +| from | formData | An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved. | No | integer | +| to | formData | An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved. | No | integer | +| page | formData | Page number (defaults to 1). | No | integer | +| limit | formData | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Returns array of users as collection | [API_V2_Entities_User](#api_v2_entities_user) | + +### /api/v2/barong/management/users/get + +#### POST +##### Description + +Get users and profile information + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User uid | No | string | +| email | formData | User email | No | string | +| phone_num | formData | User phone number | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get users and profile information | [API_V2_Management_Entities_UserWithKYC](#api_v2_management_entities_userwithkyc) | + +### /api/v2/barong/management/profiles + +#### POST +##### Description + +Imports a profile for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User Uid | Yes | string | +| first_name | formData | First Name | No | string | +| last_name | formData | Last Name | No | string | +| dob | formData | Birth date | No | date | +| address | formData | Address | No | string | +| postcode | formData | Postcode | No | string | +| city | formData | City | No | string | +| country | formData | Country | No | string | +| state | formData | State | No | string | +| metadata | formData | Metadata | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Imports a profile for user | [API_V2_Management_Entities_UserWithProfile](#api_v2_management_entities_userwithprofile) | + +### /api/v2/barong/management/phones/delete + +#### POST +##### Description + +Delete phone number for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User uid | Yes | string | +| number | formData | User phone number | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Delete phone number for user | [API_V2_Management_Entities_Phone](#api_v2_management_entities_phone) | + +### /api/v2/barong/management/phones + +#### POST +##### Description + +Create phone number for user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User uid | Yes | string | +| number | formData | User phone number | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create phone number for user | [API_V2_Management_Entities_Phone](#api_v2_management_entities_phone) | + +### /api/v2/barong/management/phones/get + +#### POST +##### Description + +Get user phone numbers + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User uid | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get user phone numbers | [API_V2_Management_Entities_Phone](#api_v2_management_entities_phone) | + +### /api/v2/barong/management/otp/sign + +#### POST +##### Description + +Sign request with barong signature + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| user_uid | formData | Account UID | Yes | string | +| otp_code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Sign request with barong signature | + +### /api/v2/barong/management/documents + +#### POST +##### Description + +Push documents to barong DB + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | User uid | Yes | string | +| doc_type | formData | Document type | Yes | string | +| doc_number | formData | Document number | Yes | string | +| filename | formData | Document name | Yes | string | +| file_ext | formData | Document file extension | Yes | string | +| upload | formData | Base64 encoded document | Yes | string | +| doc_expire | formData | Document expiration date | No | date | +| update_labels | formData | If set to false, user label will not be created/updated | No | boolean | +| metadata | formData | Any additional key: value pairs in json string format | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Push documents to barong DB | + +### /api/v2/barong/management/service_accounts/delete + +#### POST +##### Description + +Delete specific service_account + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | service_account uid | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Delete specific service_account | [API_V2_Entities_ServiceAccounts](#api_v2_entities_serviceaccounts) | + +### /api/v2/barong/management/service_accounts/create + +#### POST +##### Description + +Create service_account + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| owner_uid | formData | owner uid | Yes | string | +| service_account_role | formData | service_account role | Yes | string | +| service_account_uid | formData | service_account uid | No | string | +| service_account_email | formData | service_account email | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create service_account | [API_V2_Entities_ServiceAccounts](#api_v2_entities_serviceaccounts) | + +### /api/v2/barong/management/service_accounts/list + +#### POST +##### Description + +Get service_accounts as a paginated collection + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| page | formData | Page number (defaults to 1). | No | integer | +| limit | formData | Number of users per page (defaults to 100, maximum is 100). | No | integer | +| owner_uid | formData | owner uid | No | string | +| owner_email | formData | owner email | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get service_accounts as a paginated collection | [API_V2_Entities_ServiceAccounts](#api_v2_entities_serviceaccounts) | + +### /api/v2/barong/management/service_accounts/get + +#### POST +##### Description + +Get specific service_account information + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| uid | formData | service_account uid | No | string | +| email | formData | service_account email | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Get specific service_account information | [API_V2_Entities_ServiceAccounts](#api_v2_entities_serviceaccounts) | + +### /api/v2/barong/management/timestamp + +#### POST +##### Description + +Returns server time in seconds since Unix epoch. + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Returns server time in seconds since Unix epoch. | + +### Models + +#### API_V2_Entities_Label + +Create a label with 'private' scope and assigns to users + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| key | string | Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| scope | string | Label scope: 'public' or 'private' | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_AdminLabelView + +Get user collection filtered on label attributes + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| key | string | Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| scope | string | Label scope: 'public' or 'private' | No | +| description | string | Label desc: json string with any additional information | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_User + +Returns array of users as collection + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | + +#### API_V2_Management_Entities_UserWithProfile + +Imports a profile for user + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Management_Entities_Profile](#api_v2_management_entities_profile) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Management_Entities_Profile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| first_name | string | First Name | No | +| last_name | string | Last name | No | +| dob | date | Birth date | No | +| address | string | Address | No | +| postcode | string | Address Postcode | No | +| city | string | City name | No | +| country | string | Country name | No | +| state | string | Profile state: drafted, submitted, verified, rejected | No | +| metadata | object | Profile additional fields | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Management_Entities_UserWithKYC + +Get users and profile information + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Management_Entities_Profile](#api_v2_management_entities_profile) | | No | +| labels | [API_V2_Entities_AdminLabelView](#api_v2_entities_adminlabelview) | | No | +| phones | [API_V2_Management_Entities_Phone](#api_v2_management_entities_phone) | | No | +| documents | [API_V2_Management_Entities_Document](#api_v2_management_entities_document) | | No | +| data_storages | [API_V2_Entities_DataStorage](#api_v2_entities_datastorage) | | No | +| comments | [API_V2_Entities_Comment](#api_v2_entities_comment) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Management_Entities_Phone + +Get user phone numbers + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| country | string | Phone country | No | +| number | string | Phone Number | No | +| validated_at | s (g) | Phone validation date | No | + +#### API_V2_Management_Entities_Document + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| upload | string | File url | No | +| doc_type | string | Document type: passport, driver license, utility bill, identity card, institutional, address, residental | No | +| doc_number | string | Document number: AB123123 type | No | +| doc_expire | string | Expire date of uploaded documents | No | +| metadata | string | Any additional stored data | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_DataStorage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| title | string | Any additional data title | No | +| data | string | Any additional data json key:value pairs | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Comment + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Comment id | No | +| author_uid | string | Comment author UID | No | +| title | string | Comment title | No | +| data | string | Comment plain text | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_ServiceAccounts + +Get specific service_account information + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | Service Account Role | No | +| level | integer | User Level | No | +| state | string | Service Account State: active, disabled | No | +| user | [API_V2_Entities_User](#api_v2_entities_user) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_APIKey + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| kid | string | JWT public key | No | +| algorithm | string | Cryptographic hash function type | No | +| scope | string | Serialized array of scopes | No | +| state | string | active/non-active state of key | No | +| secret | string | Api key secret | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_UserWithFullInfo + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| csrf_token | string | Сsrf protection token | No | +| labels | [API_V2_Entities_Label](#api_v2_entities_label) | | No | +| phones | [API_V2_Entities_Phone](#api_v2_entities_phone) | | No | +| profiles | [API_V2_Entities_Profile](#api_v2_entities_profile) | | No | +| data_storages | [API_V2_Entities_DataStorage](#api_v2_entities_datastorage) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Phone + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| country | string | Phone country | No | +| number | string | Submasked phone number | No | +| validated_at | s (g) | Phone validation date | No | + +#### API_V2_Entities_Profile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| first_name | string | First Name | No | +| last_name | string | Submasked last name | No | +| dob | date | Submasked birth date | No | +| address | string | Address | No | +| postcode | string | Address Postcode | No | +| city | string | City name | No | +| country | string | Country name | No | +| state | string | Profile state: drafted, submitted, verified, rejected | No | +| metadata | object | Profile additional fields | No | +| created_at | string | | No | +| updated_at | string | | No | diff --git a/docs/api/barong_user_api_v2.md b/docs/api/barong_user_api_v2.md new file mode 100644 index 0000000..2ea2493 --- /dev/null +++ b/docs/api/barong_user_api_v2.md @@ -0,0 +1,1317 @@ +# Barong +RESTful API for barong OAuth server + +## Version: 2.7.0 + +### /api/v2/barong/identity/users/password/confirm_code + +#### POST +##### Description + +Sets new account password + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| reset_password_token | formData | Token from email | Yes | string | +| password | formData | User password | Yes | string | +| confirm_password | formData | User password | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Resets password | +| 400 | Required params are empty | +| 404 | Record is not found | +| 422 | Validation errors | + +### /api/v2/barong/identity/users/password/generate_code + +#### POST +##### Description + +Send password reset instructions + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | Account email | Yes | string | +| captcha_response | formData | Response from captcha widget | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Generated password reset code | +| 400 | Required params are missing | +| 404 | User doesn't exist | +| 422 | Validation errors | + +### /api/v2/barong/identity/users/email/confirm_code + +#### POST +##### Description + +Confirms an account + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| token | formData | Token from email | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Confirms an account | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | +| 400 | Required params are missing | | +| 422 | Validation errors | | + +### /api/v2/barong/identity/users/email/generate_code + +#### POST +##### Description + +Send confirmations instructions + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | Account email | Yes | string | +| captcha_response | formData | Response from captcha widget | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Generated verification code | +| 400 | Required params are missing | +| 422 | Validation errors | + +### /api/v2/barong/identity/users/register_geetest + +#### GET +##### Description + +Register Geetest captcha + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Register Geetest captcha | + +### /api/v2/barong/identity/users + +#### POST +##### Description + +Creates new user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | User Email | Yes | string | +| password | formData | User Password | Yes | string | +| refid | formData | Referral uid | No | string | +| captcha_response | formData | Response from captcha widget | No | string | +| data | formData | Any additional key: value pairs in json string format | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Creates new user | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | +| 400 | Required params are missing | | +| 422 | Validation errors | | + +### /api/v2/barong/identity/users/access + +#### POST +##### Description + +Creates new whitelist restriction + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| whitelink_token | formData | | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Whitelist restriction was created | +| 400 | Required params are missing | +| 422 | Validation errors | + +### /api/v2/barong/identity/sessions + +#### DELETE +##### Description + +Destroy current session + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Session was destroyed | +| 400 | Required params are empty | +| 404 | Record is not found | + +#### POST +##### Description + +Start a new session + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| email | formData | | Yes | string | +| password | formData | | Yes | string | +| captcha_response | formData | Response from captcha widget | No | string | +| otp_code | formData | Code from Google Authenticator | No | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Start a new session | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | +| 400 | Required params are empty | | +| 404 | Record is not found | | + +### /api/v2/barong/identity/configs + +#### GET +##### Description + +Get barong configurations + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get barong configurations | + +### /api/v2/barong/identity/version + +#### GET +##### Description + +Get barong version + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get barong version | + +### /api/v2/barong/identity/time + +#### GET +##### Description + +Get server current unix timestamp. + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get server current unix timestamp. | + +### /api/v2/barong/identity/ping + +#### GET +##### Description + +Test connectivity + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Test connectivity | + +### /api/v2/barong/identity/password/validate + +#### POST +##### Description + +Password strength testing + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| password | formData | User password | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Password strength testing | + +### /api/v2/barong/resource/swagger_doc/{name} + +#### GET +##### Description + +Swagger compatible API description for specific API + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| name | path | Resource name of mounted API | Yes | string | +| locale | query | Locale of API documentation | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Swagger compatible API description for specific API | + +### /api/v2/barong/resource/swagger_doc + +#### GET +##### Description + +Swagger compatible API description + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Swagger compatible API description | + +### /api/v2/barong/resource/service_accounts/api_keys/{kid} + +#### PUT +##### Description + +Updates an api key + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| service_account_uid | formData | | Yes | string | +| kid | path | Service account kid | Yes | string | +| scope | formData | Comma separated scopes | No | string | +| state | formData | State of API Key. "active" state means key is active and can be used for auth | No | string | +| totp_code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updates an api key | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 404 | Record is not found | | +| 422 | Validation errors | | + +#### DELETE +##### Description + +Delete an api key for specific service account + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| service_account_uid | query | | Yes | string | +| kid | path | Service account kid | Yes | string | +| totp_code | query | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Succefully deleted | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | + +### /api/v2/barong/resource/service_accounts/api_keys + +#### POST +##### Description + +Create api key for specific service account. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| service_account_uid | formData | | Yes | string | +| algorithm | formData | Service account algorithm | Yes | string | +| scope | formData | Comma separated scopes | No | string | +| totp_code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create api key for specific service account. | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Require 2FA and totp code | | +| 401 | Invalid bearer token | | + +#### GET +##### Description + +List all api keys for specific service account. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| ordering | query | If set, returned values will be sorted in specific order, defaults to 'asc'. | No | string | +| order_by | query | Name of the field, which result will be ordered by. | No | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | +| service_account_uid | query | | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List all api keys for specific service account. | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Require 2FA and totp code | | +| 401 | Invalid bearer token | | + +### /api/v2/barong/resource/service_accounts + +#### GET +##### Description + +List all service accounts for current user. + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | List all service accounts for current user. | +| 400 | Require 2FA and totp code | +| 401 | Invalid bearer token | + +##### Security + +| Security Schema | Scopes | +| --- | --- | +| BearerToken | | + +### /api/v2/barong/resource/data_storage + +#### POST +##### Description + +Create data storage + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| title | formData | Storage title | Yes | string | +| data | formData | Storage data | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Data Storage was created | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +### /api/v2/barong/resource/api_keys + +#### GET +##### Description + +List all api keys for current account. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| ordering | query | If set, returned values will be sorted in specific order, defaults to 'asc'. | No | string | +| order_by | query | Name of the field, which result will be ordered by. | No | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List all api keys for current account. | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Require 2FA and totp code | | +| 401 | Invalid bearer token | | + +#### POST +##### Description + +Create an api key + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| algorithm | formData | API key algorithm | Yes | string | +| scope | formData | Comma separated scopes | No | string | +| totp_code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create an api key | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 422 | Validation errors | | + +### /api/v2/barong/resource/api_keys/{kid} + +#### DELETE +##### Description + +Delete an api key + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| kid | path | API key kid | Yes | string | +| totp_code | query | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Succefully deleted | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | + +#### PATCH +##### Description + +Updates an api key + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| kid | path | API key kid | Yes | string | +| scope | formData | Comma separated scopes | No | string | +| state | formData | State of API Key. "active" state means key is active and can be used for auth | No | string | +| totp_code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updates an api key | [API_V2_Entities_APIKey](#api_v2_entities_apikey) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 404 | Record is not found | | +| 422 | Validation errors | | + +### /api/v2/barong/resource/otp/verify + +#### POST +##### Description + +Verify 2FA code + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | 2FA was verified | +| 400 | 2FA has not been enabled for this account or code is missing | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +### /api/v2/barong/resource/otp/disable + +#### POST +##### Description + +Disable 2FA + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | 2FA was disabled | +| 400 | 2FA has not been enabled for this account or code is missing | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +### /api/v2/barong/resource/otp/enable + +#### POST +##### Description + +Enable 2FA + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| code | formData | Code from Google Authenticator | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | 2FA was enabled | +| 400 | 2FA has been enabled for this account or code is missing | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +### /api/v2/barong/resource/otp/generate_qrcode + +#### POST +##### Description + +Generate qr code for 2FA + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | QR code was generated | +| 400 | 2FA has been enabled for this account | +| 401 | Invalid bearer token | + +### /api/v2/barong/resource/phones/verify + +#### POST +##### Description + +Verify a phone + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| phone_number | formData | Phone number with country code | Yes | string | +| verification_code | formData | Verification code from sms | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Verify a phone | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 404 | Record is not found | | + +### /api/v2/barong/resource/phones/send_code + +#### POST +##### Description + +Resend activation code + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| phone_number | formData | Phone number with country code | Yes | string | +| channel | formData | The verification method to use | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Activation code was resend | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | +| 422 | Validation errors | + +### /api/v2/barong/resource/phones + +#### POST +##### Description + +Add new phone + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| phone_number | formData | Phone number with country code | Yes | string | +| channel | formData | The verification method to use | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | New phone was added | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | +| 422 | Validation errors | + +#### GET +##### Description + +Returns list of user's phones + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns list of user's phones | [API_V2_Entities_Phone](#api_v2_entities_phone) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/resource/documents + +#### POST +##### Description + +Upload a new document for current user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| doc_type | formData | Document type | Yes | string | +| doc_number | formData | Document number | Yes | string | +| upload | formData | Array of Rack::Multipart::UploadedFile | Yes | string | +| doc_expire | formData | Document expiration date | No | date | +| doc_category | formData | Category of the submitted document - front/back/selfie etc. | No | string | +| identificator | formData | Identificator for documents to be supplied together | No | string | +| metadata | formData | Any additional key: value pairs in json string format | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Document is uploaded | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +#### GET +##### Description + +Return current user documents list + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Return current user documents list | [API_V2_Entities_Document](#api_v2_entities_document) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/resource/profiles + +#### PUT +##### Description + +Update a profile for current_user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| first_name | formData | First Name | No | string | +| last_name | formData | Last Name | No | string | +| dob | formData | Date of Birth | No | date | +| address | formData | Address | No | string | +| postcode | formData | Postcode | No | string | +| city | formData | City | No | string | +| country | formData | Country | No | string | +| metadata | formData | Any additional key: value pairs in json string format | No | string | +| confirm | formData | Profile confirmation | No | boolean | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Update a profile for current_user | [API_V2_Entities_Profile](#api_v2_entities_profile) | +| 401 | Invalid bearer token | | +| 422 | Validation errors | | + +#### POST +##### Description + +Create a profile for current_user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| first_name | formData | First Name | No | string | +| last_name | formData | Last Name | No | string | +| dob | formData | Date of Birth | No | date | +| address | formData | Address | No | string | +| postcode | formData | Postcode | No | string | +| city | formData | City | No | string | +| country | formData | Country | No | string | +| metadata | formData | Any additional key: value pairs in json string format | No | string | +| confirm | formData | Profile confirmation | No | boolean | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create a profile for current_user | [API_V2_Entities_Profile](#api_v2_entities_profile) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 409 | Profile already exists | | +| 422 | Validation errors | | + +### /api/v2/barong/resource/profiles/me + +#### GET +##### Description + +Return profiles of current resource owner + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Return profiles of current resource owner | [API_V2_Entities_Profile](#api_v2_entities_profile) | +| 401 | Invalid bearer token | | +| 404 | User has no profile | | + +### /api/v2/barong/resource/labels/{key} + +#### DELETE +##### Description + +Delete a label with 'public' scope. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | path | Label key. | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Succefully deleted | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 404 | Record is not found | + +#### PATCH +##### Description + +Update a label with 'public' scope. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | path | Label key. | Yes | string | +| value | formData | Label value. | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Update a label with 'public' scope. | [API_V2_Entities_Label](#api_v2_entities_label) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 404 | Record is not found | | +| 422 | Validation errors | | + +#### GET +##### Description + +Return a label by key. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | path | Label key. | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Return a label by key. | [API_V2_Entities_Label](#api_v2_entities_label) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 404 | Record is not found | | + +### /api/v2/barong/resource/labels + +#### POST +##### Description + +Create a label with 'public' scope. + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| key | formData | Label key. | Yes | string | +| value | formData | Label value. | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 201 | Create a label with 'public' scope. | [API_V2_Entities_Label](#api_v2_entities_label) | +| 400 | Required params are empty | | +| 401 | Invalid bearer token | | +| 422 | Validation errors | | + +#### GET +##### Description + +List all labels for current user. + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | List all labels for current user. | [API_V2_Entities_Label](#api_v2_entities_label) | +| 401 | Invalid bearer token | | + +### /api/v2/barong/resource/users/password + +#### PUT +##### Description + +Sets new account password + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| old_password | formData | Previous account password | Yes | string | +| new_password | formData | User password | Yes | string | +| confirm_password | formData | User password | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Changes password | +| 400 | Required params are empty | +| 404 | Record is not found | +| 422 | Validation errors | + +### /api/v2/barong/resource/users/activity/{topic} + +#### GET +##### Description + +Returns user activity + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| topic | path | Topic of user activity. Allowed: [all, password, session, otp] | Yes | string | +| time_from | query | An integer represents the seconds elapsed since Unix epoch.If set, only activities created after the time will be returned. | No | integer | +| time_to | query | An integer represents the seconds elapsed since Unix epoch.If set, only activities created before the time will be returned. | No | integer | +| result | query | Result of user activity. Allowed: [succeed, failed, denied] | No | string | +| page | query | Page number (defaults to 1). | No | integer | +| limit | query | Number of users per page (defaults to 100, maximum is 100). | No | integer | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns user activity | [API_V2_Entities_Activity](#api_v2_entities_activity) | + +### /api/v2/barong/resource/users/me + +#### DELETE +##### Description + +Blocks current user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| password | query | Account password | Yes | string | +| otp_code | query | Code from Google Authenticator | No | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 204 | Current user was blocked | + +#### PUT +##### Description + +Updates current user data field + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| data | formData | Any additional key: value pairs in json string format | Yes | string | + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Updates current user data field | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | + +#### GET +##### Description + +Returns current user + +##### Responses + +| Code | Description | Schema | +| ---- | ----------- | ------ | +| 200 | Returns current user | [API_V2_Entities_UserWithFullInfo](#api_v2_entities_userwithfullinfo) | + +### /api/v2/barong/resource/addresses + +#### POST +##### Description + +Upload a new address approval document for current user + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| country | formData | Document type | Yes | string | +| address | formData | Document number | Yes | string | +| upload | formData | Array of Rack::Multipart::UploadedFile | Yes | string | +| city | formData | Document expiration date | Yes | string | +| postcode | formData | Any additional key: value pairs in json string format | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | New address approval document was uploaded | +| 400 | Required params are empty | +| 401 | Invalid bearer token | +| 422 | Validation errors | + +### /api/v2/barong/public/configs + +#### GET +##### Description + +Get barong configurations + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get barong configurations | + +### /api/v2/barong/public/version + +#### GET +##### Description + +Get barong version + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get barong version | + +### /api/v2/barong/public/time + +#### GET +##### Description + +Get server current unix timestamp. + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Get server current unix timestamp. | + +### /api/v2/barong/public/ping + +#### GET +##### Description + +Test connectivity + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 200 | Test connectivity | + +### /api/v2/barong/public/password/validate + +#### POST +##### Description + +Password strength testing + +##### Parameters + +| Name | Located in | Description | Required | Schema | +| ---- | ---------- | ----------- | -------- | ---- | +| password | formData | User password | Yes | string | + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | Password strength testing | + +### /api/v2/barong/public/kyc + +#### POST +##### Description + +KYC callback + +##### Responses + +| Code | Description | +| ---- | ----------- | +| 201 | KYC callback | + +### Models + +#### API_V2_Entities_UserWithFullInfo + +Returns current user + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| csrf_token | string | Сsrf protection token | No | +| labels | [API_V2_Entities_Label](#api_v2_entities_label) | | No | +| phones | [API_V2_Entities_Phone](#api_v2_entities_phone) | | No | +| profiles | [API_V2_Entities_Profile](#api_v2_entities_profile) | | No | +| data_storages | [API_V2_Entities_DataStorage](#api_v2_entities_datastorage) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Label + +List all labels for current user. + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| key | string | Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| scope | string | Label scope: 'public' or 'private' | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Phone + +Returns list of user's phones + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| country | string | Phone country | No | +| number | string | Submasked phone number | No | +| validated_at | s (g) | Phone validation date | No | + +#### API_V2_Entities_Profile + +Return profiles of current resource owner + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| first_name | string | First Name | No | +| last_name | string | Submasked last name | No | +| dob | date | Submasked birth date | No | +| address | string | Address | No | +| postcode | string | Address Postcode | No | +| city | string | City name | No | +| country | string | Country name | No | +| state | string | Profile state: drafted, submitted, verified, rejected | No | +| metadata | object | Profile additional fields | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_DataStorage + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| title | string | Any additional data title | No | +| data | string | Any additional data json key:value pairs | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_APIKey + +Create an api key + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| kid | string | JWT public key | No | +| algorithm | string | Cryptographic hash function type | No | +| scope | string | Serialized array of scopes | No | +| state | string | active/non-active state of key | No | +| secret | string | Api key secret | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Document + +Return current user documents list + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| upload | string | File url | No | +| doc_type | string | Document type: passport, driver license, utility bill, identity card, institutional, address, residental | No | +| doc_number | string | Submasked document number: AB123123 type | No | +| doc_expire | string | Expire date of uploaded documents | No | +| metadata | string | Any additional stored data | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Activity + +Returns user activity + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Activity ID | No | +| user_ip | string | User IP | No | +| user_agent | string | User Browser Agent | No | +| topic | string | Defined topic (session, adjustments) or general by default | No | +| action | string | API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method | No | +| result | string | Status of API response: succeed, failed, denied | No | +| data | string | Parameters which was sent to specific API endpoint | No | +| created_at | string | | No | + +#### API_V2_Entities_Level + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Level identifier, level number | No | +| key | string | Label key. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | + +#### API_V2_Entities_User + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | + +#### API_V2_Entities_UserWithProfile + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Entities_Profile](#api_v2_entities_profile) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_UserWithKYC + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | User role | No | +| level | integer | User level | No | +| otp | boolean | is 2FA enabled for account | No | +| state | string | User state: active, pending, inactive | No | +| referral_uid | string | UID of referrer | No | +| data | string | Additional phone and profile info | No | +| profiles | [API_V2_Entities_Profile](#api_v2_entities_profile) | | No | +| labels | [API_V2_Entities_AdminLabelView](#api_v2_entities_adminlabelview) | | No | +| phones | [API_V2_Entities_Phone](#api_v2_entities_phone) | | No | +| documents | [API_V2_Entities_Document](#api_v2_entities_document) | | No | +| data_storages | [API_V2_Entities_DataStorage](#api_v2_entities_datastorage) | | No | +| comments | [API_V2_Entities_Comment](#api_v2_entities_comment) | | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_AdminLabelView + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| key | string | Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters. | No | +| value | string | Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters. | No | +| scope | string | Label scope: 'public' or 'private' | No | +| description | string | Label desc: json string with any additional information | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_Comment + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| id | integer | Comment id | No | +| author_uid | string | Comment author UID | No | +| title | string | Comment title | No | +| data | string | Comment plain text | No | +| created_at | string | | No | +| updated_at | string | | No | + +#### API_V2_Entities_ServiceAccounts + +| Name | Type | Description | Required | +| ---- | ---- | ----------- | -------- | +| email | string | User Email | No | +| uid | string | User UID | No | +| role | string | Service Account Role | No | +| level | integer | User Level | No | +| state | string | Service Account State: active, disabled | No | +| user | [API_V2_Entities_User](#api_v2_entities_user) | | No | +| created_at | string | | No | +| updated_at | string | | No | diff --git a/docs/api/swagger/admin_api.json b/docs/api/swagger/admin_api.json new file mode 100644 index 0000000..07da31d --- /dev/null +++ b/docs/api/swagger/admin_api.json @@ -0,0 +1,2731 @@ +{ + "info": { + "title": "Barong", + "description": "RESTful AdminAPI for barong OAuth server", + "version": "2.7.0" + }, + "swagger": "2.0", + "produces": [ + "application/json" + ], + "securityDefinitions": { + "BearerToken": { + "description": "Bearer Token authentication", + "type": "basic", + "name": "Authorization", + "in": "header" + } + }, + "host": "www.alvand.local", + "basePath": "/api/v2/barong/admin", + "tags": [ + { + "name": "api", + "description": "Operations about apis" + } + ], + "paths": { + "/api/v2/barong/admin/users/data_storage": { + "delete": { + "description": "Deletes user's data storage record", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "title", + "description": "data storage uniq title", + "type": "string", + "required": true + } + ], + "responses": { + "204": { + "description": "Deletes user's data storage record", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_UserWithKYC" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "deleteApiV2BarongAdminUsersDataStorage" + } + }, + "/api/v2/barong/admin/users/{uid}": { + "get": { + "description": "Returns user info", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "path", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "Returns user info", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_UserWithKYC" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsersUid" + } + }, + "/api/v2/barong/admin/users/documents": { + "put": { + "description": "change documents by admin", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "doc_type", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "doc_state", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "change documents by admin", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "doesnt exist" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminUsersDocuments" + }, + "get": { + "description": "Returns user documents", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "user_uid", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "doc_type", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "doc_category", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "doc_state", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Returns user documents", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Document" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "doesnt exist" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsersDocuments" + } + }, + "/api/v2/barong/admin/users/documents/pending": { + "get": { + "description": "Returns array of users with pending or replaced documents as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "extended", + "description": "When true endpoint returns full information about users", + "type": "boolean", + "required": false + }, + { + "in": "query", + "name": "uid", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "email", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "role", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "first_name", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "last_name", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "country", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "level", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "state", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "range", + "type": "string", + "default": "created", + "required": false + }, + { + "in": "query", + "name": "from", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "to", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of users with pending or replaced documents as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsersDocumentsPending" + } + }, + "/api/v2/barong/admin/users": { + "put": { + "description": "Update user attributes", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "email", + "description": "User Email", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "state", + "description": "user state", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "otp", + "description": "user 2fa status", + "type": "boolean", + "required": false + } + ], + "responses": { + "200": { + "description": "User attributes were created" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminUsers" + }, + "get": { + "description": "Returns array of users as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "extended", + "description": "When true endpoint returns full information about users", + "type": "boolean", + "required": false + }, + { + "in": "query", + "name": "uid", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "email", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "role", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "country", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "level", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "state", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "range", + "type": "string", + "default": "created", + "required": false + }, + { + "in": "query", + "name": "ordering", + "description": "If set, returned values will be sorted in specific order, defaults to 'asc'.", + "type": "string", + "default": "asc", + "required": false + }, + { + "in": "query", + "name": "order_by", + "description": "Name of the field, which result will be ordered by.", + "type": "string", + "default": "id", + "required": false + }, + { + "in": "query", + "name": "from", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "to", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of users as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsers" + } + }, + "/api/v2/barong/admin/users/role": { + "post": { + "description": "Update user role", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "role", + "description": "user role", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "User role was created" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminUsersRole" + } + }, + "/api/v2/barong/admin/users/update": { + "post": { + "description": "Update user attributes", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "state", + "description": "user state", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "otp", + "description": "user 2fa status", + "type": "boolean", + "required": false + } + ], + "responses": { + "200": { + "description": "User attributes were updated" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminUsersUpdate" + } + }, + "/api/v2/barong/admin/users/labels": { + "delete": { + "description": "Deletes label for user", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "key", + "description": "label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "scope", + "description": "label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + } + ], + "responses": { + "204": { + "description": "Label was deleted" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "deleteApiV2BarongAdminUsersLabels" + }, + "put": { + "description": "Update user label scope", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "scope", + "description": "label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "description", + "description": "label description. [A-Za-z0-9_-] should be used. max - 255 characters.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "value", + "description": "Label value.", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "Label was updated" + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "Record is not found" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminUsersLabels" + }, + "post": { + "description": "Add label for user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "value", + "description": "label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "description", + "description": "label description. [A-Za-z0-9_-] should be used. max - 255 characters.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "scope", + "description": "Label scope: 'public' or 'private'. Default is public", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Label was created" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminUsersLabels" + }, + "get": { + "description": "Returns array of users as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "key", + "description": "Label key", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "value", + "description": "Label value", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of users as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsersLabels" + } + }, + "/api/v2/barong/admin/users/labels/update": { + "post": { + "description": "Update user label value", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "scope", + "description": "label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "value", + "description": "Label value.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "description", + "description": "label description. [A-Za-z0-9_-] should be used. max - 255 characters.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "replace", + "description": "When true label will be created if not exist", + "type": "boolean", + "default": true, + "required": false + } + ], + "responses": { + "200": { + "description": "Label was updated" + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "Record is not found" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminUsersLabelsUpdate" + } + }, + "/api/v2/barong/admin/users/labels/list": { + "get": { + "description": "Returns existing labels keys and values", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Returns existing labels keys and values" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminUsersLabelsList" + } + }, + "/api/v2/barong/admin/users/comments": { + "delete": { + "description": "Delete user's comment", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "id", + "description": "comment uniq id", + "type": "integer", + "format": "int32", + "required": true + } + ], + "responses": { + "204": { + "description": "Delete user's comment", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_UserWithKYC" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "deleteApiV2BarongAdminUsersComments" + }, + "put": { + "description": "Edit user's comment", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "id", + "description": "comment uniq id", + "type": "integer", + "format": "int32", + "required": true + }, + { + "in": "formData", + "name": "title", + "description": "comment title", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "data", + "description": "comment data", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Edit user's comment", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_UserWithKYC" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminUsersComments" + }, + "post": { + "description": "Adds new user's comment", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "title", + "description": "comment uniq title", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "data", + "description": "comment data", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Adds new user's comment", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_UserWithKYC" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminUsersComments" + } + }, + "/api/v2/barong/admin/api_keys": { + "get": { + "description": "List all api keys for selected account.", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "uid", + "description": "user uniq id", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "ordering", + "description": "If set, returned values will be sorted in specific order, defaults to 'asc'.", + "type": "string", + "default": "asc", + "required": false + }, + { + "in": "query", + "name": "order_by", + "description": "Name of the field, which result will be ordered by.", + "type": "string", + "default": "id", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "List all api keys for selected account.", + "schema": { + "$ref": "#/definitions/API_V2_Entities_APIKey" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminApiKeys" + } + }, + "/api/v2/barong/admin/permissions": { + "put": { + "description": "Update Permission", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "id", + "description": "Permission id", + "type": "integer", + "format": "int32", + "required": true + }, + { + "in": "formData", + "name": "role", + "description": "permission field - role", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "verb", + "description": "permission field - request verb", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "path", + "description": "permission field - request path", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "action", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "topic", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Permission was updated" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminPermissions" + }, + "delete": { + "description": "Deletes permission", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "id", + "description": "permission id", + "type": "integer", + "format": "int32", + "required": true + } + ], + "responses": { + "204": { + "description": "Permission was deleted" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "deleteApiV2BarongAdminPermissions" + }, + "post": { + "description": "Create permission", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "role", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "verb", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "path", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "action", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "topic", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Permission was created" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminPermissions" + }, + "get": { + "description": "Returns array of permissions as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of permissions as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Permission" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminPermissions" + } + }, + "/api/v2/barong/admin/activities/admin": { + "get": { + "description": "Returns array of activities as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "topic", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "action", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "uid", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "email", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "from", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "to", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + }, + { + "in": "query", + "name": "target_uid", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "range", + "type": "string", + "default": "created", + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of activities as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_AdminActivity" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminActivitiesAdmin" + } + }, + "/api/v2/barong/admin/activities": { + "get": { + "description": "Returns array of activities as paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "topic", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "action", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "uid", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "email", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "from", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "to", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of activities as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_ActivityWithUser" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminActivities" + } + }, + "/api/v2/barong/admin/metrics": { + "get": { + "description": "Returns main statistic in the given time period", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "created_from", + "type": "string", + "required": false + }, + { + "in": "query", + "name": "created_to", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Returns main statistic in the given time period" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminMetrics" + } + }, + "/api/v2/barong/admin/restrictions": { + "delete": { + "description": "Delete restriction", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "id", + "description": "Restriction id", + "type": "integer", + "format": "int32", + "required": true + } + ], + "responses": { + "204": { + "description": "Restriction was deleted" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "deleteApiV2BarongAdminRestrictions" + }, + "put": { + "description": "Update restriction", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "id", + "description": "Restriction id", + "type": "integer", + "format": "int32", + "required": true + }, + { + "in": "formData", + "name": "scope", + "type": "string", + "enum": [ + "continent", + "country", + "ip", + "ip_subnet", + "all" + ], + "required": false + }, + { + "in": "formData", + "name": "category", + "type": "string", + "enum": [ + "whitelist", + "maintenance", + "blacklist", + "blocklogin" + ], + "required": false + }, + { + "in": "formData", + "name": "value", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "state", + "type": "string", + "enum": [ + "enabled", + "disabled" + ], + "required": false + }, + { + "in": "formData", + "name": "code", + "type": "integer", + "format": "int32", + "required": false + } + ], + "responses": { + "200": { + "description": "Restriction was updated" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminRestrictions" + }, + "post": { + "description": "Create restriction", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "scope", + "type": "string", + "enum": [ + "continent", + "country", + "ip", + "ip_subnet", + "all" + ], + "required": true + }, + { + "in": "formData", + "name": "value", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "category", + "type": "string", + "enum": [ + "whitelist", + "maintenance", + "blacklist", + "blocklogin" + ], + "required": true + }, + { + "in": "formData", + "name": "state", + "type": "string", + "default": "enabled", + "enum": [ + "enabled", + "disabled" + ], + "required": false + }, + { + "in": "formData", + "name": "code", + "type": "integer", + "format": "int32", + "required": false + } + ], + "responses": { + "200": { + "description": "Restriction was created" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminRestrictions" + }, + "get": { + "description": "Returns array of restrictions as a paginated collection", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "scope", + "type": "string", + "enum": [ + "continent", + "country", + "ip", + "ip_subnet", + "all" + ], + "required": false + }, + { + "in": "query", + "name": "category", + "type": "string", + "enum": [ + "whitelist", + "maintenance", + "blacklist", + "blocklogin" + ], + "required": false + }, + { + "in": "query", + "name": "range", + "type": "string", + "default": "created", + "required": false + }, + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Returns array of restrictions as a paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Restriction" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminRestrictions" + } + }, + "/api/v2/barong/admin/restrictions/whitelink": { + "post": { + "description": "Create whitelink", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "expire_time", + "description": "link will be active for (Time.now + expire_time in following range)", + "type": "integer", + "format": "int32", + "default": 1, + "minimum": 1, + "maximum": 30, + "required": false + }, + { + "in": "formData", + "name": "range", + "description": "In combination with expire_time gives full controll over token expiration", + "type": "string", + "default": "day", + "required": false + } + ], + "responses": { + "200": { + "description": "Created whitelink" + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminRestrictionsWhitelink" + } + }, + "/api/v2/barong/admin/profiles/label": { + "put": { + "description": "verifying labels by admin", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "label_key", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "label_value", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "verifying labels by admin", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "doesnt exist" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminProfilesLabel" + } + }, + "/api/v2/barong/admin/profiles": { + "post": { + "description": "Create a profile for user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "first_name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "last_name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "dob", + "type": "string", + "format": "date", + "required": false + }, + { + "in": "formData", + "name": "address", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "postcode", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "city", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "country", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "metadata", + "description": "Any additional key: value pairs in json string format", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Create a profile for user", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongAdminProfiles" + }, + "put": { + "description": "Verify user's profile", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "state", + "type": "string", + "required": true + } + ], + "responses": { + "200": { + "description": "Verify user's profile", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminProfiles" + }, + "get": { + "description": "Return all profiles", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "query", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "200": { + "description": "Return all profiles", + "schema": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminProfiles" + } + }, + "/api/v2/barong/admin/profiles/treasury/list": { + "get": { + "description": "Return list of treasuries", + "produces": [ + "application/json" + ], + "parameters": [ + { + "in": "query", + "name": "user_uid", + "type": "string", + "required": true + }, + { + "in": "query", + "name": "kind", + "type": "string", + "required": false + } + ], + "responses": { + "200": { + "description": "Return list of treasuries", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Treasury" + } + }, + "401": { + "description": "Invalid bearer token" + }, + "404": { + "description": "User has no treasuries" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminProfilesTreasuryList" + } + }, + "/api/v2/barong/admin/levels": { + "put": { + "description": "Change vip level", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "level", + "description": "change level from vip", + "type": "string", + "default": "3", + "required": true + } + ], + "responses": { + "200": { + "description": "Change vip level", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + }, + "400": { + "description": "Required params are empty" + }, + "401": { + "description": "Invalid bearer token" + }, + "422": { + "description": "Validation errors" + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongAdminLevels" + }, + "get": { + "description": "Returns array of permissions as paginated collection", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Returns array of permissions as paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Level" + } + }, + "401": { + "description": "Invalid bearer token" + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminLevels" + } + }, + "/api/v2/barong/admin/abilities": { + "get": { + "description": "Get all roles and admin_permissions of barong cancan.", + "produces": [ + "application/json" + ], + "responses": { + "200": { + "description": "Get all roles and admin_permissions of barong cancan." + } + }, + "tags": [ + "api" + ], + "operationId": "getApiV2BarongAdminAbilities" + } + } + }, + "definitions": { + "API_V2_Admin_Entities_UserWithKYC": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + }, + "profiles": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + }, + "labels": { + "$ref": "#/definitions/API_V2_Entities_AdminLabelView" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "documents": { + "$ref": "#/definitions/API_V2_Admin_Entities_Document" + } + }, + "description": "Adds new user's comment" + }, + "API_V2_Admin_Entities_Profile": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "description": "first name" + }, + "last_name": { + "type": "string", + "description": "Last name" + }, + "national_code": { + "type": "string", + "description": "National Code" + }, + "dob": { + "type": "string", + "format": "date", + "description": "Birth date" + }, + "address": { + "type": "string", + "description": "Address" + }, + "postcode": { + "type": "string", + "description": "Address Postcode" + }, + "city": { + "type": "string", + "description": "City name" + }, + "country": { + "type": "string", + "description": "Country name" + }, + "state": { + "type": "string", + "description": "Profile state: drafted, submitted, verified, rejected" + }, + "metadata": { + "type": "object", + "description": "Profile additional fields" + }, + "upload": { + "type": "string", + "description": "Address Postcode" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Return all profiles" + }, + "API_V2_Entities_AdminLabelView": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters." + }, + "value": { + "type": "string", + "description": "Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters." + }, + "scope": { + "type": "string", + "description": "Label scope: 'public' or 'private'" + }, + "description": { + "type": "string", + "description": "Label desc: json string with any additional information" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Admin_Entities_Document": { + "type": "object", + "properties": { + "upload": { + "type": "string", + "description": "File Upload" + }, + "url": { + "type": "string", + "description": "File url" + }, + "doc_type": { + "type": "string", + "description": "Document type: passport, driver license, utility bill, identity card, institutional, address, residental" + }, + "doc_number": { + "type": "string", + "description": "document number: AB123123 type" + }, + "doc_expire": { + "type": "string", + "description": "Expire date of uploaded documents" + }, + "metadata": { + "type": "string", + "description": "Any additional stored data" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_Document": { + "type": "object", + "properties": { + "upload": { + "type": "string", + "description": "File Upload" + }, + "url": { + "type": "string", + "description": "File url" + }, + "doc_type": { + "type": "string", + "description": "Document type: passport, driver license, utility bill, identity card, institutional, address, residental" + }, + "doc_number": { + "type": "string", + "description": "Submasked document number: AB123123 type" + }, + "doc_expire": { + "type": "string", + "description": "Expire date of uploaded documents" + }, + "metadata": { + "type": "string", + "description": "Any additional stored data" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Returns user documents" + }, + "API_V2_Entities_User": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + } + }, + "description": "Change vip level" + }, + "API_V2_Entities_APIKey": { + "type": "object", + "properties": { + "kid": { + "type": "string", + "description": "JWT public key" + }, + "algorithm": { + "type": "string", + "description": "Cryptographic hash function type" + }, + "scope": { + "type": "string", + "description": "Serialized array of scopes" + }, + "state": { + "type": "string", + "description": "active/non-active state of key" + }, + "secret": { + "type": "string", + "description": "Api key secret" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "List all api keys for selected account." + }, + "API_V2_Entities_Permission": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Permission id" + }, + "action": { + "type": "string", + "description": "Permission action: accept (allow access (drop access), audit (record activity)" + }, + "role": { + "type": "string", + "description": "Permission user role" + }, + "verb": { + "type": "string", + "description": "Permission verb: put, post, delete, get" + }, + "path": { + "type": "string", + "description": "API path" + }, + "topic": { + "type": "string", + "description": "Permission topic: general, session etc" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Returns array of permissions as paginated collection" + }, + "API_V2_Admin_Entities_AdminActivity": { + "type": "object", + "properties": { + "user_ip": { + "type": "string", + "description": "User IP" + }, + "user_agent": { + "type": "string", + "description": "User Browser Agent" + }, + "topic": { + "type": "string", + "description": "Defined topic (session, adjustments) or general by default" + }, + "action": { + "type": "string", + "description": "API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method" + }, + "result": { + "type": "string", + "description": "Status of API response: succeed, failed, denied" + }, + "data": { + "type": "string", + "description": "Parameters which was sent to specific API endpoint" + }, + "admin": { + "$ref": "#/definitions/API_V2_Entities_User" + }, + "target": { + "$ref": "#/definitions/API_V2_Entities_User" + }, + "created_at": { + "type": "string" + } + }, + "description": "Returns array of activities as paginated collection" + }, + "API_V2_Admin_Entities_ActivityWithUser": { + "type": "object", + "properties": { + "user_ip": { + "type": "string", + "description": "User IP" + }, + "user_agent": { + "type": "string", + "description": "User Browser Agent" + }, + "topic": { + "type": "string", + "description": "Defined topic (session, adjustments) or general by default" + }, + "action": { + "type": "string", + "description": "API action: POST => 'create', PUT => 'update', GET => 'read', DELETE => 'delete', PATCH => 'update' or system if there is no match of HTTP method" + }, + "result": { + "type": "string", + "description": "Status of API response: succeed, failed, denied" + }, + "data": { + "type": "string", + "description": "Parameters which was sent to specific API endpoint" + }, + "user": { + "$ref": "#/definitions/API_V2_Entities_User" + }, + "created_at": { + "type": "string" + } + }, + "description": "Returns array of activities as paginated collection" + }, + "API_V2_Entities_Restriction": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Restriction id" + }, + "category": { + "type": "string", + "description": "Restriction categories: blacklist, maintenance, whitelist, blocklogin" + }, + "scope": { + "type": "string", + "description": "Restriction scopes: continent, country, ip, ip_subnet, all" + }, + "value": { + "type": "string", + "description": "Restriction value: IP address, country abbreviation, all" + }, + "code": { + "type": "integer", + "format": "int32", + "description": "Restriction codes: {\"continent\"=>423, \"country\"=>423, \"ip_subnet\"=>403, \"ip\"=>401, \"all\"=>401}" + }, + "state": { + "type": "string", + "description": "Restriction states: disabled, enabled" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Returns array of restrictions as a paginated collection" + }, + "API_V2_Entities_Treasury": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "id of record" + }, + "title": { + "type": "string", + "description": "title" + }, + "state": { + "type": "string", + "description": "state" + }, + "kind": { + "type": "string", + "description": "kind" + }, + "data": { + "type": "string", + "description": "Submasked data" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Return list of treasuries" + }, + "API_V2_Entities_Level": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Level identifier, level number" + }, + "key": { + "type": "string", + "description": "Label key. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters." + }, + "value": { + "type": "string", + "description": "Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters." + } + }, + "description": "Returns array of permissions as paginated collection" + }, + "API_V2_Admin_Entities_Phone": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "Phone country" + }, + "number": { + "type": "string", + "description": "Phone number" + }, + "validated_at": { + "type": "s", + "format": "g", + "description": "Phone validation date" + } + } + }, + "API_V2_Admin_Entities_UserWithProfile": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + }, + "profiles": { + "$ref": "#/definitions/API_V2_Admin_Entities_Profile" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + } + } +} diff --git a/docs/api/swagger/management_api.json b/docs/api/swagger/management_api.json new file mode 100644 index 0000000..702176b --- /dev/null +++ b/docs/api/swagger/management_api.json @@ -0,0 +1,1685 @@ +{ + "info": { + "title": "Barong", + "description": "Management API for barong OAuth server", + "version": "2.7.0" + }, + "swagger": "2.0", + "produces": [ + "application/json" + ], + "securityDefinitions": { + "SecurityScope": { + "description": "JWT should have signature keychains", + "type": "basic", + "name": "Authorization" + } + }, + "host": "localhost:3000", + "basePath": "/api/v2/barong/management", + "tags": [ + { + "name": "api", + "description": "Operations about apis" + } + ], + "paths": { + "/api/v2/barong/management/labels/delete": { + "post": { + "description": "Delete a label with 'private' scope", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Delete a label with 'private' scope" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementLabelsDelete" + } + }, + "/api/v2/barong/management/labels": { + "put": { + "description": "Update a label with 'private' scope", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "value", + "description": "Label value.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "description", + "description": "Label desc.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "replace", + "description": "When true label will be created if not exist", + "type": "boolean", + "default": true, + "required": false + } + ], + "responses": { + "200": { + "description": "Update a label with 'private' scope", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Label" + } + } + }, + "tags": [ + "api" + ], + "operationId": "putApiV2BarongManagementLabels" + }, + "post": { + "description": "Create a label with 'private' scope and assigns to users", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "value", + "description": "Label value.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "description", + "description": "Label desc.", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Create a label with 'private' scope and assigns to users", + "schema": { + "$ref": "#/definitions/API_V2_Entities_Label" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementLabels" + } + }, + "/api/v2/barong/management/labels/list": { + "post": { + "description": "Get user collection filtered on label attributes", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "description": "User uid", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Get user collection filtered on label attributes", + "schema": { + "$ref": "#/definitions/API_V2_Entities_AdminLabelView" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementLabelsList" + } + }, + "/api/v2/barong/management/labels/filter/users": { + "post": { + "description": "Get all labels assigned to users", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "key", + "description": "Label key.", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "value", + "description": "Label value.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "scope", + "description": "Label scope.", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "extended", + "description": "When true endpoint returns full information about users", + "type": "boolean", + "required": false + }, + { + "in": "formData", + "name": "range", + "type": "string", + "default": "created", + "required": false + }, + { + "in": "formData", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "formData", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "201": { + "description": "Get all labels assigned to users", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementLabelsFilterUsers" + } + }, + "/api/v2/barong/management/users/import": { + "post": { + "description": "Imports an existing user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "email", + "description": "User Email", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "password_digest", + "description": "User Password Hash", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "referral_uid", + "description": "Referral uid", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "phone", + "description": "Phone", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "first_name", + "description": "First Name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "last_name", + "description": "Last Name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "dob", + "description": "Birth date", + "type": "string", + "format": "date", + "required": false + }, + { + "in": "formData", + "name": "address", + "description": "Address", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "postcode", + "description": "Postcode", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "city", + "description": "City", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "country", + "description": "Country", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "state", + "description": "State", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Imports an existing user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_UserWithProfile" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementUsersImport" + } + }, + "/api/v2/barong/management/users/update": { + "post": { + "description": "Updates role and data fields of existing user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User Uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "role", + "description": "User Role", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "data", + "description": "Any additional key:value pairs in json format", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Updates role and data fields of existing user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_UserWithProfile" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementUsersUpdate" + } + }, + "/api/v2/barong/management/users": { + "post": { + "description": "Creates new user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "email", + "description": "User Email", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "password", + "description": "User Password", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "referral_uid", + "description": "Referral uid", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Creates new user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_UserWithProfile" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementUsers" + } + }, + "/api/v2/barong/management/users/list": { + "post": { + "description": "Returns array of users as collection", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "extended", + "description": "When true endpoint returns full information about users", + "type": "boolean", + "required": false + }, + { + "in": "formData", + "name": "range", + "type": "string", + "default": "created", + "required": false + }, + { + "in": "formData", + "name": "from", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records FROM the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "formData", + "name": "to", + "description": "An integer represents the seconds elapsed since Unix epoch.If set, only records BEFORE the time will be retrieved.", + "type": "integer", + "format": "int32", + "required": false + }, + { + "in": "formData", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "formData", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + } + ], + "responses": { + "201": { + "description": "Returns array of users as collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_User" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementUsersList" + } + }, + "/api/v2/barong/management/users/get": { + "post": { + "description": "Get users and profile information", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User uid", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "email", + "description": "User email", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "phone_num", + "description": "User phone number", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Get users and profile information", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_UserWithKYC" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementUsersGet" + } + }, + "/api/v2/barong/management/profiles": { + "post": { + "description": "Imports a profile for user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User Uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "first_name", + "description": "First Name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "last_name", + "description": "Last Name", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "dob", + "description": "Birth date", + "type": "string", + "format": "date", + "required": false + }, + { + "in": "formData", + "name": "address", + "description": "Address", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "postcode", + "description": "Postcode", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "city", + "description": "City", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "country", + "description": "Country", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "state", + "description": "State", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "metadata", + "description": "Metadata", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Imports a profile for user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_UserWithProfile" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementProfiles" + } + }, + "/api/v2/barong/management/phones/delete": { + "post": { + "description": "Delete phone number for user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "number", + "description": "User phone number", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Delete phone number for user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_Phone" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementPhonesDelete" + } + }, + "/api/v2/barong/management/phones": { + "post": { + "description": "Create phone number for user", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "number", + "description": "User phone number", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Create phone number for user", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_Phone" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementPhones" + } + }, + "/api/v2/barong/management/phones/get": { + "post": { + "description": "Get user phone numbers", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User uid", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Get user phone numbers", + "schema": { + "$ref": "#/definitions/API_V2_Management_Entities_Phone" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementPhonesGet" + } + }, + "/api/v2/barong/management/otp/sign": { + "post": { + "description": "Sign request with barong signature", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "user_uid", + "description": "Account UID", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "otp_code", + "description": "Code from Google Authenticator", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Sign request with barong signature" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementOtpSign" + } + }, + "/api/v2/barong/management/documents": { + "post": { + "description": "Push documents to barong DB", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "User uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "doc_type", + "description": "Document type", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "doc_number", + "description": "Document number", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "filename", + "description": "Document name", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "file_ext", + "description": "Document file extension", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "upload", + "description": "Base64 encoded document", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "doc_expire", + "description": "Document expiration date", + "type": "string", + "format": "date", + "required": false + }, + { + "in": "formData", + "name": "update_labels", + "description": "If set to false, user label will not be created/updated", + "type": "boolean", + "default": true, + "required": false + }, + { + "in": "formData", + "name": "metadata", + "description": "Any additional key: value pairs in json string format", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Push documents to barong DB" + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementDocuments" + } + }, + "/api/v2/barong/management/service_accounts/delete": { + "post": { + "description": "Delete specific service_account", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "service_account uid", + "type": "string", + "required": true + } + ], + "responses": { + "201": { + "description": "Delete specific service_account", + "schema": { + "$ref": "#/definitions/API_V2_Entities_ServiceAccounts" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementServiceAccountsDelete" + } + }, + "/api/v2/barong/management/service_accounts/create": { + "post": { + "description": "Create service_account", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "owner_uid", + "description": "owner uid", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "service_account_role", + "description": "service_account role", + "type": "string", + "required": true + }, + { + "in": "formData", + "name": "service_account_uid", + "description": "service_account uid", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "service_account_email", + "description": "service_account email", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Create service_account", + "schema": { + "$ref": "#/definitions/API_V2_Entities_ServiceAccounts" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementServiceAccountsCreate" + } + }, + "/api/v2/barong/management/service_accounts/list": { + "post": { + "description": "Get service_accounts as a paginated collection", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "page", + "description": "Page number (defaults to 1).", + "type": "integer", + "format": "int32", + "default": 1, + "required": false + }, + { + "in": "formData", + "name": "limit", + "description": "Number of users per page (defaults to 100, maximum is 100).", + "type": "integer", + "format": "int32", + "default": 100, + "minimum": 1, + "maximum": 100, + "required": false + }, + { + "in": "formData", + "name": "owner_uid", + "description": "owner uid", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "owner_email", + "description": "owner email", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Get service_accounts as a paginated collection", + "schema": { + "$ref": "#/definitions/API_V2_Entities_ServiceAccounts" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementServiceAccountsList" + } + }, + "/api/v2/barong/management/service_accounts/get": { + "post": { + "description": "Get specific service_account information", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "parameters": [ + { + "in": "formData", + "name": "uid", + "description": "service_account uid", + "type": "string", + "required": false + }, + { + "in": "formData", + "name": "email", + "description": "service_account email", + "type": "string", + "required": false + } + ], + "responses": { + "201": { + "description": "Get specific service_account information", + "schema": { + "$ref": "#/definitions/API_V2_Entities_ServiceAccounts" + } + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementServiceAccountsGet" + } + }, + "/api/v2/barong/management/timestamp": { + "post": { + "description": "Returns server time in seconds since Unix epoch.", + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "responses": { + "201": { + "description": "Returns server time in seconds since Unix epoch." + } + }, + "tags": [ + "api" + ], + "operationId": "postApiV2BarongManagementTimestamp" + } + } + }, + "definitions": { + "API_V2_Entities_Label": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters." + }, + "value": { + "type": "string", + "description": "Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters." + }, + "scope": { + "type": "string", + "description": "Label scope: 'public' or 'private'" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Create a label with 'private' scope and assigns to users" + }, + "API_V2_Entities_AdminLabelView": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters." + }, + "value": { + "type": "string", + "description": "Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters." + }, + "scope": { + "type": "string", + "description": "Label scope: 'public' or 'private'" + }, + "description": { + "type": "string", + "description": "Label desc: json string with any additional information" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Get user collection filtered on label attributes" + }, + "API_V2_Entities_User": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + } + }, + "description": "Returns array of users as collection" + }, + "API_V2_Management_Entities_UserWithProfile": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + }, + "profiles": { + "$ref": "#/definitions/API_V2_Management_Entities_Profile" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Imports a profile for user" + }, + "API_V2_Management_Entities_Profile": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "description": "First Name" + }, + "last_name": { + "type": "string", + "description": "Last name" + }, + "dob": { + "type": "string", + "format": "date", + "description": "Birth date" + }, + "address": { + "type": "string", + "description": "Address" + }, + "postcode": { + "type": "string", + "description": "Address Postcode" + }, + "city": { + "type": "string", + "description": "City name" + }, + "country": { + "type": "string", + "description": "Country name" + }, + "state": { + "type": "string", + "description": "Profile state: drafted, submitted, verified, rejected" + }, + "metadata": { + "type": "object", + "description": "Profile additional fields" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Management_Entities_UserWithKYC": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + }, + "profiles": { + "$ref": "#/definitions/API_V2_Management_Entities_Profile" + }, + "labels": { + "$ref": "#/definitions/API_V2_Entities_AdminLabelView" + }, + "phones": { + "$ref": "#/definitions/API_V2_Management_Entities_Phone" + }, + "documents": { + "$ref": "#/definitions/API_V2_Management_Entities_Document" + }, + "data_storages": { + "$ref": "#/definitions/API_V2_Entities_DataStorage" + }, + "comments": { + "$ref": "#/definitions/API_V2_Entities_Comment" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Get users and profile information" + }, + "API_V2_Management_Entities_Phone": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "Phone country" + }, + "number": { + "type": "string", + "description": "Phone Number" + }, + "validated_at": { + "type": "s", + "format": "g", + "description": "Phone validation date" + } + }, + "description": "Get user phone numbers" + }, + "API_V2_Management_Entities_Document": { + "type": "object", + "properties": { + "upload": { + "type": "string", + "description": "File url" + }, + "doc_type": { + "type": "string", + "description": "Document type: passport, driver license, utility bill, identity card, institutional, address, residental" + }, + "doc_number": { + "type": "string", + "description": "Document number: AB123123 type" + }, + "doc_expire": { + "type": "string", + "description": "Expire date of uploaded documents" + }, + "metadata": { + "type": "string", + "description": "Any additional stored data" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_DataStorage": { + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Any additional data title" + }, + "data": { + "type": "string", + "description": "Any additional data json key:value pairs" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_Comment": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int32", + "description": "Comment id" + }, + "author_uid": { + "type": "string", + "description": "Comment author UID" + }, + "title": { + "type": "string", + "description": "Comment title" + }, + "data": { + "type": "string", + "description": "Comment plain text" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_ServiceAccounts": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "Service Account Role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User Level" + }, + "state": { + "type": "string", + "description": "Service Account State: active, disabled" + }, + "user": { + "$ref": "#/definitions/API_V2_Entities_User" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "description": "Get specific service_account information" + }, + "API_V2_Entities_APIKey": { + "type": "object", + "properties": { + "kid": { + "type": "string", + "description": "JWT public key" + }, + "algorithm": { + "type": "string", + "description": "Cryptographic hash function type" + }, + "scope": { + "type": "string", + "description": "Serialized array of scopes" + }, + "state": { + "type": "string", + "description": "active/non-active state of key" + }, + "secret": { + "type": "string", + "description": "Api key secret" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_UserWithFullInfo": { + "type": "object", + "properties": { + "email": { + "type": "string", + "description": "User Email" + }, + "uid": { + "type": "string", + "description": "User UID" + }, + "role": { + "type": "string", + "description": "User role" + }, + "level": { + "type": "integer", + "format": "int32", + "description": "User level" + }, + "otp": { + "type": "boolean", + "description": "is 2FA enabled for account" + }, + "state": { + "type": "string", + "description": "User state: active, pending, inactive" + }, + "referral_uid": { + "type": "string", + "description": "UID of referrer" + }, + "data": { + "type": "string", + "description": "Additional phone and profile info" + }, + "csrf_token": { + "type": "string", + "description": "Сsrf protection token" + }, + "labels": { + "$ref": "#/definitions/API_V2_Entities_Label" + }, + "phones": { + "$ref": "#/definitions/API_V2_Entities_Phone" + }, + "profiles": { + "$ref": "#/definitions/API_V2_Entities_Profile" + }, + "data_storages": { + "$ref": "#/definitions/API_V2_Entities_DataStorage" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + }, + "API_V2_Entities_Phone": { + "type": "object", + "properties": { + "country": { + "type": "string", + "description": "Phone country" + }, + "number": { + "type": "string", + "description": "Submasked phone number" + }, + "validated_at": { + "type": "s", + "format": "g", + "description": "Phone validation date" + } + } + }, + "API_V2_Entities_Profile": { + "type": "object", + "properties": { + "first_name": { + "type": "string", + "description": "First Name" + }, + "last_name": { + "type": "string", + "description": "Submasked last name" + }, + "dob": { + "type": "string", + "format": "date", + "description": "Submasked birth date" + }, + "address": { + "type": "string", + "description": "Address" + }, + "postcode": { + "type": "string", + "description": "Address Postcode" + }, + "city": { + "type": "string", + "description": "City name" + }, + "country": { + "type": "string", + "description": "Country name" + }, + "state": { + "type": "string", + "description": "Profile state: drafted, submitted, verified, rejected" + }, + "metadata": { + "type": "object", + "description": "Profile additional fields" + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + } + } + } +} diff --git a/docs/api/swagger/user_api.json b/docs/api/swagger/user_api.json new file mode 100644 index 0000000..7410860 --- /dev/null +++ b/docs/api/swagger/user_api.json @@ -0,0 +1 @@ +{"info":{"title":"Barong","description":"RESTful API for barong OAuth server","version":"2.7.0"},"swagger":"2.0","produces":["application/json"],"host":"www.alvand.local","basePath":"/api/v2/barong","tags":[{"name":"api","description":"Operations about apis"}],"paths":{"/api/v2/barong/identity/users/password/reset":{"post":{"description":"Sets new account password(for forgot password)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","description":"user email","type":"string","required":true},{"in":"formData","name":"code","description":"code from email again","type":"string","required":true},{"in":"formData","name":"password","description":"User new password","type":"string","required":true},{"in":"formData","name":"confirm_password","description":"User new password","type":"string","required":true}],"responses":{"201":{"description":"Resets password"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersPasswordReset"}},"/api/v2/barong/identity/users/password/confirm_code":{"post":{"description":"Just check confirm code for reset password(check authorization for forget password)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","description":"Account email","type":"string","required":true},{"in":"formData","name":"code","description":"Confirm code","type":"string","required":true},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false}],"responses":{"201":{"description":"reset code is ok"},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"},"404":{"description":"User doesn't exist"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersPasswordConfirmCode"}},"/api/v2/barong/identity/users/password/generate_code":{"post":{"description":"Send password reset instructions(forget password)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","description":"Account email","type":"string","required":true},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false}],"responses":{"201":{"description":"Generated password reset code"},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"},"404":{"description":"User doesn't exist"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersPasswordGenerateCode"}},"/api/v2/barong/identity/users/email/confirm_email":{"post":{"description":"Confirms an account by Authorization code (verify user email and achieve first label)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from email","type":"string","required":true},{"in":"formData","name":"email","description":"user email","type":"string","required":true}],"responses":{"201":{"description":"Confirms an account by Authorization code (verify user email and achieve first label)","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}},"400":{"description":"Required code are missing"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersEmailConfirmEmail"}},"/api/v2/barong/identity/users/email/generate_email_code":{"post":{"description":"Send confirmations instructions (code in email)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","description":"Account email","type":"string","required":true},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false}],"responses":{"201":{"description":"Generated verification code"},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersEmailGenerateEmailCode"}},"/api/v2/barong/identity/users/register_geetest":{"get":{"description":"Register Geetest captcha","produces":["application/json"],"responses":{"200":{"description":"Register Geetest captcha"}},"tags":["api"],"operationId":"getApiV2BarongIdentityUsersRegisterGeetest"}},"/api/v2/barong/identity/users":{"post":{"description":"Creates new user (sign up)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","description":"User Email","type":"string","required":true},{"in":"formData","name":"password","description":"User Password","type":"string","required":true},{"in":"formData","name":"refid","description":"Referral uid","type":"string","required":false},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false},{"in":"formData","name":"data","description":"Any additional key: value pairs in json string format","type":"string","required":false}],"responses":{"201":{"description":"Creates new user (sign up)","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsers"}},"/api/v2/barong/identity/users/access":{"post":{"description":"Creates new whitelist restriction","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"whitelink_token","type":"string","required":true}],"responses":{"200":{"description":"Whitelist restriction was created"},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentityUsersAccess"}},"/api/v2/barong/identity/sessions/resend":{"post":{"description":"Resend confirmations code(authorization code)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"data","description":"Account email or Telephone number","type":"string","required":true},{"in":"formData","name":"action","description":"for what need auth code","type":"string","required":true},{"in":"formData","name":"channel","description":"channel that send in","type":"string","default":"email","required":false},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false}],"responses":{"201":{"description":"Generated verification code"},"400":{"description":"Required params are missing"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongIdentitySessionsResend"}},"/api/v2/barong/identity/sessions/auth0":{"post":{"description":"Auth0 authentication by id_token","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"id_token","description":"ID Token","type":"string","required":true}],"responses":{"200":{"description":"User authenticated"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"postApiV2BarongIdentitySessionsAuth0"}},"/api/v2/barong/identity/sessions":{"delete":{"description":"Destroy current session for LogOut","produces":["application/json"],"responses":{"204":{"description":"Session was destroyed"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"deleteApiV2BarongIdentitySessions"},"post":{"description":"Start a new session for every LogIn","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"email","type":"string","required":true},{"in":"formData","name":"password","type":"string","required":true},{"in":"formData","name":"captcha_response","description":"Response from captcha widget","type":"string","required":false},{"in":"formData","name":"otp_code","description":"Code from Google Authenticator","type":"string","required":false}],"responses":{"201":{"description":"Start a new session for every LogIn"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"postApiV2BarongIdentitySessions"}},"/api/v2/barong/identity/configs":{"get":{"description":"Get barong configurations","produces":["application/json"],"responses":{"200":{"description":"Get barong configurations"}},"tags":["api"],"operationId":"getApiV2BarongIdentityConfigs"}},"/api/v2/barong/identity/version":{"get":{"description":"Get barong version","produces":["application/json"],"responses":{"200":{"description":"Get barong version"}},"tags":["api"],"operationId":"getApiV2BarongIdentityVersion"}},"/api/v2/barong/identity/time":{"get":{"description":"Get server current unix timestamp.","produces":["application/json"],"responses":{"200":{"description":"Get server current unix timestamp."}},"tags":["api"],"operationId":"getApiV2BarongIdentityTime"}},"/api/v2/barong/identity/ping":{"get":{"description":"Test connectivity","produces":["application/json"],"responses":{"200":{"description":"Test connectivity"}},"tags":["api"],"operationId":"getApiV2BarongIdentityPing"}},"/api/v2/barong/identity/password/validate":{"post":{"description":"Password strength testing","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"password","description":"User password","type":"string","required":true}],"responses":{"201":{"description":"Password strength testing"}},"tags":["api"],"operationId":"postApiV2BarongIdentityPasswordValidate"}},"/api/v2/barong/resource/swagger_doc/{name}":{"get":{"description":"Swagger compatible API description for specific API","produces":["application/json"],"parameters":[{"in":"path","name":"name","description":"Resource name of mounted API","type":"string","required":true},{"in":"query","name":"locale","description":"Locale of API documentation","type":"string","required":false}],"responses":{"200":{"description":"Swagger compatible API description for specific API"}},"tags":["api"],"operationId":"getApiV2BarongResourceSwaggerDocName"}},"/api/v2/barong/resource/swagger_doc":{"get":{"description":"Swagger compatible API description","produces":["application/json"],"responses":{"200":{"description":"Swagger compatible API description"}},"tags":["api"],"operationId":"getApiV2BarongResourceSwaggerDoc"}},"/api/v2/barong/resource/service_accounts/api_keys/{kid}":{"put":{"description":"Updates an api key","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"service_account_uid","type":"string","required":true},{"in":"path","name":"kid","description":"Service account kid","type":"string","required":true},{"in":"formData","name":"scope","description":"Comma separated scopes","type":"string","required":false},{"in":"formData","name":"state","description":"State of API Key. \"active\" state means key is active and can be used for auth","type":"string","required":false},{"in":"formData","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"Updates an api key","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"putApiV2BarongResourceServiceAccountsApiKeysKid"},"delete":{"description":"Delete an api key for specific service account","produces":["application/json"],"parameters":[{"in":"query","name":"service_account_uid","type":"string","required":true},{"in":"path","name":"kid","description":"Service account kid","type":"string","required":true},{"in":"query","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"204":{"description":"Succefully deleted"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"deleteApiV2BarongResourceServiceAccountsApiKeysKid"}},"/api/v2/barong/resource/service_accounts/api_keys":{"post":{"description":"Create api key for specific service account.","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"service_account_uid","type":"string","required":true},{"in":"formData","name":"algorithm","description":"Service account algorithm","type":"string","required":true},{"in":"formData","name":"scope","description":"Comma separated scopes","type":"string","required":false},{"in":"formData","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"201":{"description":"Create api key for specific service account.","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Require 2FA and totp code"},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"postApiV2BarongResourceServiceAccountsApiKeys"},"get":{"description":"List all api keys for specific service account.","produces":["application/json"],"parameters":[{"in":"query","name":"ordering","description":"If set, returned values will be sorted in specific order, defaults to 'asc'.","type":"string","default":"asc","required":false},{"in":"query","name":"order_by","description":"Name of the field, which result will be ordered by.","type":"string","default":"id","required":false},{"in":"query","name":"page","description":"Page number (defaults to 1).","type":"integer","format":"int32","default":1,"required":false},{"in":"query","name":"limit","description":"Number of users per page (defaults to 100, maximum is 100).","type":"integer","format":"int32","default":100,"minimum":1,"maximum":100,"required":false},{"in":"query","name":"service_account_uid","type":"string","required":true}],"responses":{"200":{"description":"List all api keys for specific service account.","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Require 2FA and totp code"},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceServiceAccountsApiKeys"}},"/api/v2/barong/resource/service_accounts":{"get":{"description":"List all service accounts for current user.","produces":["application/json"],"security":[{"BearerToken":[]}],"responses":{"200":{"description":"List all service accounts for current user."},"400":{"description":"Require 2FA and totp code"},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceServiceAccounts"}},"/api/v2/barong/resource/data_storage":{"post":{"description":"Create data storage","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"title","description":"Storage title","type":"string","required":true},{"in":"formData","name":"data","description":"Storage data","type":"string","required":true}],"responses":{"201":{"description":"Data Storage was created"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceDataStorage"}},"/api/v2/barong/resource/api_keys":{"get":{"description":"List all api keys for current account.","produces":["application/json"],"parameters":[{"in":"query","name":"ordering","description":"If set, returned values will be sorted in specific order, defaults to 'asc'.","type":"string","default":"asc","required":false},{"in":"query","name":"order_by","description":"Name of the field, which result will be ordered by.","type":"string","default":"id","required":false},{"in":"query","name":"page","description":"Page number (defaults to 1).","type":"integer","format":"int32","default":1,"required":false},{"in":"query","name":"limit","description":"Number of users per page (defaults to 100, maximum is 100).","type":"integer","format":"int32","default":100,"minimum":1,"maximum":100,"required":false}],"responses":{"200":{"description":"List all api keys for current account.","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Require 2FA and totp code"},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceApiKeys"},"post":{"description":"Create an api key","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"algorithm","description":"API key algorithm","type":"string","required":true},{"in":"formData","name":"scope","description":"Comma separated scopes","type":"string","required":false},{"in":"formData","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"201":{"description":"Create an api key","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceApiKeys"}},"/api/v2/barong/resource/api_keys/{kid}":{"delete":{"description":"Delete an api key","produces":["application/json"],"parameters":[{"in":"path","name":"kid","description":"API key kid","type":"string","required":true},{"in":"query","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"204":{"description":"Succefully deleted"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"deleteApiV2BarongResourceApiKeysKid"},"patch":{"description":"Updates an api key","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"path","name":"kid","description":"API key kid","type":"string","required":true},{"in":"formData","name":"scope","description":"Comma separated scopes","type":"string","required":false},{"in":"formData","name":"state","description":"State of API Key. \"active\" state means key is active and can be used for auth","type":"string","required":false},{"in":"formData","name":"totp_code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"Updates an api key","schema":{"$ref":"#/definitions/API_V2_Entities_APIKey"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"patchApiV2BarongResourceApiKeysKid"}},"/api/v2/barong/resource/otp/verify":{"post":{"description":"Verify 2FA code(for checking 2fa)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"2FA was verified"},"400":{"description":"2FA has not been enabled for this account or code is missing"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpVerify"}},"/api/v2/barong/resource/otp/disable_email":{"post":{"description":"enter authorization code that send by email (second step for disabling 2FA)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"2FA was disabled"},"400":{"description":"2FA has been enabled for this account or code is missing"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpDisableEmail"}},"/api/v2/barong/resource/otp/disable":{"post":{"description":"Disable 2FA request and send authorization code(first step disabling)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"2FA was disabled"},"400":{"description":"2FA has not been enabled for this account or code is missing"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpDisable"}},"/api/v2/barong/resource/otp/enable_2fa":{"post":{"description":"enter authorization key and enable 2fa(third step for enabling)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from email","type":"string","required":true}],"responses":{"200":{"description":"2FA was enabled"},"400":{"description":"2FA has been enabled for this account or code is missing"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpEnable2fa"}},"/api/v2/barong/resource/otp/enable":{"post":{"description":"enter google authenticator code and send authorization code by email(second step for enabling)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Code from Google Authenticator","type":"string","required":true}],"responses":{"200":{"description":"2FA was enabled"},"400":{"description":"2FA has been enabled for this account or code is missing"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpEnable"}},"/api/v2/barong/resource/otp/generate_qrcode":{"post":{"description":"Generate qr code for 2FA (first step for enabling)","produces":["application/json"],"consumes":["application/json"],"responses":{"200":{"description":"QR code was generated"},"400":{"description":"2FA has been enabled for this account"},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"postApiV2BarongResourceOtpGenerateQrcode"}},"/api/v2/barong/resource/phones/verify":{"post":{"description":"Verify a phone (enter code and verify number)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"Phone number with country code(in iran with +98)","type":"string","required":true},{"in":"formData","name":"verification_code","description":"Verification code from call","type":"string","required":true}],"responses":{"201":{"description":"Verify a phone (enter code and verify number)","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"postApiV2BarongResourcePhonesVerify"}},"/api/v2/barong/resource/phones/send_code":{"post":{"description":"Resend activation code in call","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"Phone number with country code","type":"string","required":true},{"in":"formData","name":"channel","description":"The verification method to use","type":"string","default":"call","enum":["call","sms"],"required":false}],"responses":{"200":{"description":"Activation code was resend"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourcePhonesSendCode"}},"/api/v2/barong/resource/phones":{"post":{"description":"Add new phone","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"Phone number with country code(in iran +98)","type":"string","required":true},{"in":"formData","name":"channel","description":"The verification method to use","type":"string","default":"call","enum":["call","sms"],"required":false}],"responses":{"200":{"description":"New phone was added"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourcePhones"}},"/api/v2/barong/resource/mobiles/verify":{"post":{"description":"Verify a mobile(enter code and verify number)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"phone number with country code(in iran with +98)","type":"string","required":true},{"in":"formData","name":"verification_code","description":"Verification code from sms(this param is for legacy structure must be sms)","type":"string","required":true}],"responses":{"201":{"description":"Verify a mobile(enter code and verify number)","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"postApiV2BarongResourceMobilesVerify"}},"/api/v2/barong/resource/mobiles/send_code":{"post":{"description":"Resend activation code for mobile sms","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"Phone number with country code( in iran +98 )","type":"string","required":true},{"in":"formData","name":"channel","description":"The verification method to use(this param is for legacy structure must be sms)","type":"string","default":"sms","enum":["call","sms"],"required":false}],"responses":{"200":{"description":"Activation code was resend"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceMobilesSendCode"}},"/api/v2/barong/resource/mobiles":{"post":{"description":"Add new mobile","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"phone_number","description":"Phone number with country code(in iran +98)","type":"string","required":true},{"in":"formData","name":"channel","description":"The verification method to use(this param is for legacy structure must be sms)","type":"string","default":"sms","enum":["call","sms"],"required":false}],"responses":{"200":{"description":"New phone was added"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceMobiles"},"get":{"description":"Returns list of user's phones","produces":["application/json"],"responses":{"200":{"description":"Returns list of user's phones","schema":{"$ref":"#/definitions/API_V2_Entities_Phone"}},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceMobiles"}},"/api/v2/barong/resource/documents":{"post":{"description":"Upload a new document for current user","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"doc_type","description":"Document type","type":"string","required":true},{"in":"formData","name":"doc_number","description":"Document number","type":"string","required":true},{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true},{"in":"formData","name":"doc_expire","description":"Document expiration date","type":"string","format":"date","required":false},{"in":"formData","name":"doc_category","description":"Category of the submitted document - front/back/selfie etc.","type":"string","required":false},{"in":"formData","name":"identificator","description":"Identificator for documents to be supplied together","type":"string","required":false},{"in":"formData","name":"metadata","description":"Any additional key: value pairs in json string format","type":"string","required":false}],"responses":{"201":{"description":"Document is uploaded"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceDocuments"},"get":{"description":"Return current user documents list","produces":["application/json"],"responses":{"200":{"description":"Return current user documents list","schema":{"$ref":"#/definitions/API_V2_Entities_Document"}},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceDocuments"}},"/api/v2/barong/resource/profiles/address":{"post":{"description":"Adding Address","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"city","description":"city name","type":"string","required":true},{"in":"formData","name":"province","description":"province name","type":"string","required":true},{"in":"formData","name":"address","description":"address","type":"string","required":true},{"in":"formData","name":"postcode","description":"post code","type":"string","required":true},{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true}],"responses":{"201":{"description":"Adding Address","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"needed data doesnt exist"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceProfilesAddress"}},"/api/v2/barong/resource/profiles/selfie":{"post":{"description":"Create Level two - step three - selfie","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true},{"in":"formData","name":"confirm","description":"Profile confirmation","type":"boolean","default":true,"required":false}],"responses":{"201":{"description":"Create Level two - step three - selfie","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"409":{"description":"Selfie already exists"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceProfilesSelfie"}},"/api/v2/barong/resource/profiles/treasury/list":{"get":{"description":"Return list of treasuries","produces":["application/json"],"responses":{"200":{"description":"Return list of treasuries","schema":{"$ref":"#/definitions/API_V2_Entities_Treasury"}},"401":{"description":"Invalid bearer token"},"404":{"description":"User has no treasuries"}},"tags":["api"],"operationId":"getApiV2BarongResourceProfilesTreasuryList"}},"/api/v2/barong/resource/profiles/treasury/{id}":{"delete":{"description":"Delete a treasury","produces":["application/json"],"parameters":[{"in":"path","name":"id","type":"integer","format":"int32","required":true}],"responses":{"204":{"description":"Successfully deleted"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"deleteApiV2BarongResourceProfilesTreasuryId"}},"/api/v2/barong/resource/profiles/treasury":{"post":{"description":"Create Level one - step three - info bank ","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"data","description":"treasury number","type":"string","required":true},{"in":"formData","name":"kind","description":"iban or card","type":"string","required":true},{"in":"formData","name":"title","description":"treasury name","type":"string","required":false},{"in":"formData","name":"confirm","description":"Treasury confirmation","type":"boolean","default":true,"required":false}],"responses":{"201":{"description":"Create Level one - step three - info bank ","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"409":{"description":"Bank info already exists"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceProfilesTreasury"}},"/api/v2/barong/resource/profiles":{"put":{"description":"Update a profile for current_user","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"first_name","description":"First Name","type":"string","required":true},{"in":"formData","name":"last_name","description":"Last Name","type":"string","required":true},{"in":"formData","name":"dob","description":"Date of Birth","type":"string","format":"date","required":true},{"in":"formData","name":"national_code","description":"national code","type":"string","required":true},{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true},{"in":"formData","name":"confirm","description":"Profile confirmation","type":"boolean","default":true,"required":false}],"responses":{"200":{"description":"Update a profile for current_user","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"putApiV2BarongResourceProfiles"},"post":{"description":"Create Level one - step one - personal info in profiles","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"first_name","description":"First Name","type":"string","required":true},{"in":"formData","name":"last_name","description":"Last Name","type":"string","required":true},{"in":"formData","name":"dob","description":"Date of Birth","type":"string","format":"date","required":true},{"in":"formData","name":"national_code","description":"national code","type":"string","required":true},{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true},{"in":"formData","name":"confirm","description":"Profile confirmation","type":"boolean","default":true,"required":false}],"responses":{"201":{"description":"Create Level one - step one - personal info in profiles","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"409":{"description":"Profile already exists"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceProfiles"}},"/api/v2/barong/resource/profiles/me":{"get":{"description":"Return profiles of current resource owner","produces":["application/json"],"responses":{"200":{"description":"Return profiles of current resource owner","schema":{"$ref":"#/definitions/API_V2_Entities_Profile"}},"401":{"description":"Invalid bearer token"},"404":{"description":"User has no profile"}},"tags":["api"],"operationId":"getApiV2BarongResourceProfilesMe"}},"/api/v2/barong/resource/labels/{key}":{"delete":{"description":"Delete a label with 'public' scope.","produces":["application/json"],"parameters":[{"in":"path","name":"key","description":"Label key.","type":"string","required":true}],"responses":{"204":{"description":"Succefully deleted"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"deleteApiV2BarongResourceLabelsKey"},"patch":{"description":"Update a label with 'public' scope.","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"path","name":"key","description":"Label key.","type":"string","required":true},{"in":"formData","name":"value","description":"Label value.","type":"string","required":true}],"responses":{"200":{"description":"Update a label with 'public' scope.","schema":{"$ref":"#/definitions/API_V2_Entities_Label"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"patchApiV2BarongResourceLabelsKey"},"get":{"description":"Return a label by key.","produces":["application/json"],"parameters":[{"in":"path","name":"key","description":"Label key.","type":"string","required":true}],"responses":{"200":{"description":"Return a label by key.","schema":{"$ref":"#/definitions/API_V2_Entities_Label"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"404":{"description":"Record is not found"}},"tags":["api"],"operationId":"getApiV2BarongResourceLabelsKey"}},"/api/v2/barong/resource/labels":{"post":{"description":"Create a label with 'public' scope.","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"key","description":"Label key.","type":"string","required":true},{"in":"formData","name":"value","description":"Label value.","type":"string","required":true}],"responses":{"201":{"description":"Create a label with 'public' scope.","schema":{"$ref":"#/definitions/API_V2_Entities_Label"}},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceLabels"},"get":{"description":"List all labels for current user.","produces":["application/json"],"parameters":[{"in":"query","name":"ordering","description":"If set, returned labels sorted in specific order, default to \"asc\".","type":"string","default":"asc","enum":["asc","desc"],"required":false}],"responses":{"200":{"description":"List all labels for current user.","schema":{"$ref":"#/definitions/API_V2_Entities_Label"}},"401":{"description":"Invalid bearer token"}},"tags":["api"],"operationId":"getApiV2BarongResourceLabels"}},"/api/v2/barong/resource/users/confirm_password":{"post":{"description":"Confirm account password(enter authorization code)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"code","description":"Totp Code from email","type":"string","required":true}],"responses":{"201":{"description":"Changes password"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceUsersConfirmPassword"}},"/api/v2/barong/resource/users/password":{"put":{"description":"Sets new account password(change password)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"old_password","description":"Previous account password","type":"string","required":true},{"in":"formData","name":"new_password","description":"User password","type":"string","required":true},{"in":"formData","name":"confirm_password","description":"User password","type":"string","required":true}],"responses":{"201":{"description":"Code sent for confirm password"},"400":{"description":"Required params are empty"},"404":{"description":"Record is not found"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"putApiV2BarongResourceUsersPassword"}},"/api/v2/barong/resource/users/activity/{topic}":{"get":{"description":"Returns user activity","produces":["application/json"],"parameters":[{"in":"path","name":"topic","description":"Topic of user activity. Allowed: [all, password, session, otp]","type":"string","required":true},{"in":"query","name":"time_from","description":"An integer represents the seconds elapsed since Unix epoch.If set, only activities created after the time will be returned.","type":"integer","format":"int32","required":false},{"in":"query","name":"time_to","description":"An integer represents the seconds elapsed since Unix epoch.If set, only activities created before the time will be returned.","type":"integer","format":"int32","required":false},{"in":"query","name":"result","description":"Result of user activity. Allowed: [succeed, failed, denied]","type":"string","required":false},{"in":"query","name":"page","description":"Page number (defaults to 1).","type":"integer","format":"int32","default":1,"required":false},{"in":"query","name":"limit","description":"Number of users per page (defaults to 100, maximum is 100).","type":"integer","format":"int32","default":100,"minimum":1,"maximum":100,"required":false}],"responses":{"200":{"description":"Returns user activity","schema":{"$ref":"#/definitions/API_V2_Entities_Activity"}}},"tags":["api"],"operationId":"getApiV2BarongResourceUsersActivityTopic"}},"/api/v2/barong/resource/users/me":{"delete":{"description":"Blocks current user","produces":["application/json"],"parameters":[{"in":"query","name":"password","description":"Account password","type":"string","required":true},{"in":"query","name":"otp_code","description":"Code from Google Authenticator","type":"string","required":false}],"responses":{"204":{"description":"Current user was blocked"}},"tags":["api"],"operationId":"deleteApiV2BarongResourceUsersMe"},"put":{"description":"Updates current user data field","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"data","description":"Any additional key: value pairs in json string format","type":"string","required":true}],"responses":{"200":{"description":"Updates current user data field","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}}},"tags":["api"],"operationId":"putApiV2BarongResourceUsersMe"},"get":{"description":"Returns current user","produces":["application/json"],"responses":{"200":{"description":"Returns current user","schema":{"$ref":"#/definitions/API_V2_Entities_UserWithFullInfo"}}},"tags":["api"],"operationId":"getApiV2BarongResourceUsersMe"}},"/api/v2/barong/resource/addresses":{"post":{"description":"Upload a new address approval document for current user(this is what ui using now)","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"country","description":"country name","type":"string","required":true},{"in":"formData","name":"address","description":"address","type":"string","required":true},{"in":"formData","name":"upload","description":"Array of Rack::Multipart::UploadedFile","type":"string","required":true},{"in":"formData","name":"city","description":"city name","type":"string","required":true},{"in":"formData","name":"postcode","description":"Any additional key: value pairs in json string format","type":"string","required":true}],"responses":{"201":{"description":"New address approval document was uploaded"},"400":{"description":"Required params are empty"},"401":{"description":"Invalid bearer token"},"422":{"description":"Validation errors"}},"tags":["api"],"operationId":"postApiV2BarongResourceAddresses"}},"/api/v2/barong/public/configs/auth0":{"get":{"description":"Get auth0 configuration","produces":["application/json"],"responses":{"200":{"description":"Get auth0 configuration"}},"tags":["api"],"operationId":"getApiV2BarongPublicConfigsAuth0"}},"/api/v2/barong/public/configs":{"get":{"description":"Get barong configurations","produces":["application/json"],"responses":{"200":{"description":"Get barong configurations"}},"tags":["api"],"operationId":"getApiV2BarongPublicConfigs"}},"/api/v2/barong/public/version":{"get":{"description":"Get barong version","produces":["application/json"],"responses":{"200":{"description":"Get barong version"}},"tags":["api"],"operationId":"getApiV2BarongPublicVersion"}},"/api/v2/barong/public/time":{"get":{"description":"Get server current unix timestamp.","produces":["application/json"],"responses":{"200":{"description":"Get server current unix timestamp."}},"tags":["api"],"operationId":"getApiV2BarongPublicTime"}},"/api/v2/barong/public/ping":{"get":{"description":"Test connectivity","produces":["application/json"],"responses":{"200":{"description":"Test connectivity"}},"tags":["api"],"operationId":"getApiV2BarongPublicPing"}},"/api/v2/barong/public/password/validate":{"post":{"description":"Password strength testing","produces":["application/json"],"consumes":["application/json"],"parameters":[{"in":"formData","name":"password","description":"User password","type":"string","required":true}],"responses":{"201":{"description":"Password strength testing"}},"tags":["api"],"operationId":"postApiV2BarongPublicPasswordValidate"}},"/api/v2/barong/public/kyc":{"post":{"description":"KYC callback","produces":["application/json"],"consumes":["application/json"],"responses":{"201":{"description":"KYC callback"}},"tags":["api"],"operationId":"postApiV2BarongPublicKyc"}}},"definitions":{"API_V2_Entities_UserWithFullInfo":{"type":"object","properties":{"email":{"type":"string","description":"User Email"},"uid":{"type":"string","description":"User UID"},"role":{"type":"string","description":"User role"},"level":{"type":"integer","format":"int32","description":"User level"},"otp":{"type":"boolean","description":"is 2FA enabled for account"},"state":{"type":"string","description":"User state: active, pending, inactive"},"referral_uid":{"type":"string","description":"UID of referrer"},"data":{"type":"string","description":"Additional phone and profile info"},"csrf_token":{"type":"string","description":"Сsrf protection token"},"labels":{"$ref":"#/definitions/API_V2_Entities_Label"},"phones":{"$ref":"#/definitions/API_V2_Entities_Phone"},"profiles":{"$ref":"#/definitions/API_V2_Entities_Profile"},"data_storages":{"$ref":"#/definitions/API_V2_Entities_DataStorage"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"Returns current user"},"API_V2_Entities_Label":{"type":"object","properties":{"key":{"type":"string","description":"Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters."},"value":{"type":"string","description":"Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters."},"scope":{"type":"string","description":"Label scope: 'public' or 'private'"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"List all labels for current user."},"API_V2_Entities_Phone":{"type":"object","properties":{"country":{"type":"string","description":"Phone country"},"number":{"type":"string","description":"Submasked phone number"},"validated_at":{"type":"s","format":"g","description":"Phone validation date"}},"description":"Returns list of user's phones"},"API_V2_Entities_Profile":{"type":"object","properties":{"first_name":{"type":"string","description":"First Name"},"last_name":{"type":"string","description":"Submasked last name"},"national_code":{"type":"string","description":"Submasked national code"},"dob":{"type":"string","format":"date","description":"Submasked birth date"},"address":{"type":"string","description":"Address"},"postcode":{"type":"string","description":"Address Postcode"},"city":{"type":"string","description":"City name"},"country":{"type":"string","description":"Country name"},"state":{"type":"string","description":"Profile state: drafted, submitted, verified, rejected"},"metadata":{"type":"object","description":"Profile additional fields"},"upload":{"type":"string","description":"Address Postcode"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"Return profiles of current resource owner"},"API_V2_Entities_DataStorage":{"type":"object","properties":{"title":{"type":"string","description":"Any additional data title"},"data":{"type":"string","description":"Any additional data json key:value pairs"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"API_V2_Entities_APIKey":{"type":"object","properties":{"kid":{"type":"string","description":"JWT public key"},"algorithm":{"type":"string","description":"Cryptographic hash function type"},"scope":{"type":"string","description":"Serialized array of scopes"},"state":{"type":"string","description":"active/non-active state of key"},"secret":{"type":"string","description":"Api key secret"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"Create an api key"},"API_V2_Entities_Document":{"type":"object","properties":{"upload":{"type":"string","description":"File Upload"},"url":{"type":"string","description":"File url"},"doc_type":{"type":"string","description":"Document type: passport, driver license, utility bill, identity card, institutional, address, residental"},"doc_number":{"type":"string","description":"Submasked document number: AB123123 type"},"doc_expire":{"type":"string","description":"Expire date of uploaded documents"},"metadata":{"type":"string","description":"Any additional stored data"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"Return current user documents list"},"API_V2_Entities_Treasury":{"type":"object","properties":{"id":{"type":"integer","format":"int32","description":"id of record"},"title":{"type":"string","description":"title"},"state":{"type":"string","description":"state"},"kind":{"type":"string","description":"kind"},"data":{"type":"string","description":"Submasked data"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"description":"Return list of treasuries"},"API_V2_Entities_Activity":{"type":"object","properties":{"id":{"type":"integer","format":"int32","description":"Activity ID"},"user_ip":{"type":"string","description":"User IP"},"user_agent":{"type":"string","description":"User Browser Agent"},"topic":{"type":"string","description":"Defined topic (session, adjustments) or general by default"},"action":{"type":"string","description":"API action: POST =\u003e 'create', PUT =\u003e 'update', GET =\u003e 'read', DELETE =\u003e 'delete', PATCH =\u003e 'update' or system if there is no match of HTTP method"},"result":{"type":"string","description":"Status of API response: succeed, failed, denied"},"data":{"type":"string","description":"Parameters which was sent to specific API endpoint"},"created_at":{"type":"string"}},"description":"Returns user activity"},"API_V2_Entities_Level":{"type":"object","properties":{"id":{"type":"integer","format":"int32","description":"Level identifier, level number"},"key":{"type":"string","description":"Label key. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters."},"value":{"type":"string","description":"Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters."}}},"API_V2_Entities_User":{"type":"object","properties":{"email":{"type":"string","description":"User Email"},"uid":{"type":"string","description":"User UID"},"role":{"type":"string","description":"User role"},"level":{"type":"integer","format":"int32","description":"User level"},"otp":{"type":"boolean","description":"is 2FA enabled for account"},"state":{"type":"string","description":"User state: active, pending, inactive"},"referral_uid":{"type":"string","description":"UID of referrer"},"data":{"type":"string","description":"Additional phone and profile info"}}},"API_V2_Entities_UserWithProfile":{"type":"object","properties":{"email":{"type":"string","description":"User Email"},"uid":{"type":"string","description":"User UID"},"role":{"type":"string","description":"User role"},"level":{"type":"integer","format":"int32","description":"User level"},"otp":{"type":"boolean","description":"is 2FA enabled for account"},"state":{"type":"string","description":"User state: active, pending, inactive"},"referral_uid":{"type":"string","description":"UID of referrer"},"data":{"type":"string","description":"Additional phone and profile info"},"profiles":{"$ref":"#/definitions/API_V2_Entities_Profile"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"API_V2_Entities_UserWithKYC":{"type":"object","properties":{"email":{"type":"string","description":"User Email"},"uid":{"type":"string","description":"User UID"},"role":{"type":"string","description":"User role"},"level":{"type":"integer","format":"int32","description":"User level"},"otp":{"type":"boolean","description":"is 2FA enabled for account"},"state":{"type":"string","description":"User state: active, pending, inactive"},"referral_uid":{"type":"string","description":"UID of referrer"},"data":{"type":"string","description":"Additional phone and profile info"},"profiles":{"$ref":"#/definitions/API_V2_Entities_Profile"},"labels":{"$ref":"#/definitions/API_V2_Entities_AdminLabelView"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"API_V2_Entities_AdminLabelView":{"type":"object","properties":{"key":{"type":"string","description":"Label key. [a-z0-9_-]+ should be used. Min - 3, max - 255 characters."},"value":{"type":"string","description":"Label value. [A-Za-z0-9_-] should be used. Min - 3, max - 255 characters."},"scope":{"type":"string","description":"Label scope: 'public' or 'private'"},"description":{"type":"string","description":"Label desc: json string with any additional information"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"API_V2_Entities_ServiceAccounts":{"type":"object","properties":{"email":{"type":"string","description":"User Email"},"uid":{"type":"string","description":"User UID"},"role":{"type":"string","description":"Service Account Role"},"level":{"type":"integer","format":"int32","description":"User Level"},"state":{"type":"string","description":"Service Account State: active, disabled"},"user":{"$ref":"#/definitions/API_V2_Entities_User"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}},"API_V2_Entities_Comment":{"type":"object","properties":{"id":{"type":"integer","format":"int32","description":"Comment id"},"author_uid":{"type":"string","description":"Comment author UID"},"title":{"type":"string","description":"Comment title"},"data":{"type":"string","description":"Comment plain text"},"created_at":{"type":"string"},"updated_at":{"type":"string"}}}}} diff --git a/docs/configuration.md b/docs/configuration.md new file mode 100644 index 0000000..4026768 --- /dev/null +++ b/docs/configuration.md @@ -0,0 +1,194 @@ +# Barong environments overview +##### This document provides description, defaults and possible values for all environment variables that take a part in app configuration + +### General configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_app_name` | Barong | any string value | Define app name for `2FA issuer` and `friendly_name` for twilio v2 verification | +| `barong_domain` | openware.com | any string value | Value of the env will be sent as `domain` param in `EVENT API` in identity module, which helps mailer or 3rd party email send services to avoid additional configurations | +| `barong_uid_prefix` | ID | any string value that matches regex: `/^[A-z]{2,6}$/` | This env configurate first 2-6 chars of UID | +| `barong_session_name` | _barong_session | any string value | session cookie name | +| `barong_session_expire_time` | 1800 | any number ( value is in seconds) | session lifetime (auto-renews on every private call | +| `barong_required_docs_expire` | true | `false` `true` | force Barong to validate or not validate `expires_in` parameter at document creation. with `false` still can be sent and recorded but with no time validation | +| `barong_doc_num_limit` | 10 | any amount number | number of maximum documents that can be attached to uniq user | +| `barong_geoip_lang` | en | `en`, `de`, `es`, `fr`, `ja`, `ru` | internal GeoIP lang `Barong::GeoIP.lang`, which configures the language of detected country/continent name | +| `barong_csrf_protection` | true | `true`, `false` | when turned on (`true`) exposes csrf_token on session create and requires X-CSRF-Token on every private POST PUT PATCH DELETE TRACE on AuthZ level | +| `barong_apikey_nonce_lifetime` | 5000 | integer representation of milliseconds | nonce in api key headers should not be older than this env value | +| `barong_gateway` | 'cloudflare' | `cloudflare`, `akamai` | when turned on (`true`) user IP on session and AuthZ level will firstly be checked in TRUE_CLIENT_IP header | +| `barong_jwt_expire_time` | '3600' | integer representation of seconds | general purpose tokens (reset password, confirm email) expiration time | +| `crc32_salt` | - | any string value | salt for crc32 algorithm which used to searching in encrypted fields | +| `api_data_masking_enabled` | true | `true`, `false` | when turned on (`true`) user API will be with ecnrypted user data | +|`first_registration_superadmin`| true | `true`, `false` | when turned on (`true`) first registered user on a platform will be superadmin without any email confirmation | +|`mgn_api_keys_user`| false | `true`, `false` | when turned on (`true`) management API to create/update api keys will be provided for user entity| +|`mgn_api_keys_sa`| false | `true`, `false` | when turned on (`true`) management API to create/update api keys will be provided for service account entity | + +### Password configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_password_regexp` | ^(?=.*[[:lower:]])(?=.*[[:upper:]])(?=.*[[:digit:]])(?=.*[[:graph:]]).{8,80}$ | any valid regex without / / | regex will validate password while user sign up / reset pass / password change | +| `barong_password_min_entropy` | 14 | any positive int | minimal entropy required by password | +| `barong_password_use_dictionary` | true | bool | activates or deactivates most common password dictionary check | + +### Storage configuration +More details in [storage configuration doc](#storage-configuration) + +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_storage_provider` | local | `local` `google` `aws` `alicloud` | provider for documents store. this env may have an affected on other from this module | +| `barong_storage_bucket_name` | local | any string value | bucket name, required for all providers | +| `barong_storage_access_key` | - | any string value | access key for bucket, required for all providers | +| `barong_storage_secret_key` | - | any string value | secret key for bucket, required for all providers | +| `barong_storage_endpoint` | - | any string valid url value | custom storage endpoint, can be used for AWS, AliCloud providers | +| `barong_storage_signature_version` | 4 | `2` `3` `4` | custom signature version, can be used for AWS provider | +| `barong_storage_region` | - | any string value | bucket storage region | +| `barong_storage_pathstyle` | false | `false` `true` | storage pathstyle, myght be used for AWS or AliCloud providers | +| `barong_upload_size_min_range` | 1 | any integer value | minimum size of possible upload (in megabytes) | +| `barong_upload_size_max_range` | 10 | any integer value | maximum size of possible upload (in megabytes) | +| `barong_upload_auth_url_expiration` | 1 | any integer value | configures in minutes the lifetime of auth signature to see upload | +| `barong_upload_extension_whitelist` | jpg, jpeg, png, pdf | string with comma-separated extensions formats | whitelist of upload extensions | + +### API CORS configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_api_cors_origins` | * | any string valid url value or wildcard `*` | CORS configuration - url or wildcard | +| `barong_api_cors_max_age` | 3600 | any number ( value is in seconds) | indicates how long the results of a preflight request can be cached, in seconds | +| `barong_api_cors_allow_credentials` | false | `false` `true` | allows cookies to be sent in cross-domain responses | + +### CAPTCHA configuration +More details in [captcha policy doc](https://www.openware.com/sdk/docs/barong/general/captcha.html) + +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_captcha` | none | `none` `recaptcha` `geetest` | configures captcha policy | +| `barong_geetest_id` | - | any string value | geetest id for captcha from geetest.com | +| `barong_geetest_key` | - | any string value | geetest id for captcha from geetest.com | +| `barong_recaptcha_site_key` | - | any string value | site key for RECAPTCHA | +| `barong_recaptcha_secret_key` | - | any string value | secret key for RECAPTCHA | + +### Twilio configuration +More details in [twilio configuration](#twilio-configuration) + +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_phone_verification` | mock | `twilio_verify` , `twilio_sms` , `mock` | sms send policy, switcher between twilio services and stub (mock) | +| `barong_twilio_phone_number` | +15005550000 | any twilio valid number or twilio string name | Twilio sms sender number/name | +| `barong_twilio_account_sid` | - | any string value | twilio account sid, required by configuration | +| `barong_twilio_auth_token` | - | any string value | twilio auth token, required by configuration | +| `barong_twilio_service_sid` | - | any string value | twilio service sid, required by configuration of `twilio_verify` policy | +| `barong_sms_content_template` | Your verification code for Barong: `{{code}}` | any string value containing `{{code}}` | template, used in both configurations as content for SMS | + +### RabbitMQ configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_event_api_rabbitmq_host` | localhost | any string value | rabbitmq server host | +| `barong_event_api_rabbitmq_port` | 5672 | any valid port string | rabbitmq server port | +| `barong_event_api_rabbitmq_username` | guest | any string value | rabbitmq server access username | +| `barong_event_api_rabbitmq_password` | guest | any string value | rabbitmq server access password | + +### Redis configuration +| `barong_redis_cluster` | `false` | `false` `true` | define redis mode usage (https://redis.io/topics/cluster-tutorial) | +| `barong_redis_url` | `redis://localhost:6379/1` | any valid url | url of redis server with port | +| `barong_redis_password` | ~ | any string value | redis server access password | + +### Vault configuration +| `barong_vault_address` | `http://localhost:8200` | any valid url | vault server url with port | +| `barong_vault_token` | | any string value | vault access token | +| `barong_vault_app_name` | barong | any string value | the name of the application, all encryption keys in Vault will be prefixed with this application name | + +### Sentry configuration +| `barong_sentry_dsn_backend` | ~ | valid host url | Sentry SDK client key | + +### Auth0 configuration + +| Env name | Default value | Possible values | Description | +| ---------- | :------: |:------: |---------------------------------- | +|`auth0_domain`| - | any string value | auth0 Domain name (without https://) | +|`auth0_client_id`| - | any string value | the client_id of your auth0 application | + +### SMTP configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_sender_email` | noreply@barong.io | any valid email | this will be displayed as sender email for client in all outbox | +| `barong_sender_name` | Barong | any string value | this will be displayed as sender name for client in all outbox | +| `barong_smtp_password` | - | any string value | password for auth 3d party send emails service smtp | +| `barong_smtp_port` | 1025 | any integer value | port for auth 3d party send emails service smtp | +| `barong_smtp_host` | localhost | valid host url | host for auth 3d party send emails service smtp | +| `barong_smtp_user` | - | any string value | username for auth 3d party send emails service smtp | +| `barong_default_language` | en | alpha-2 country | default language for email letters | + +### Config files configuration +| Env name | Default value | Possible values | Description | +| ---------- | ------ |-------------------------|---------------------------------- | +| `barong_config` | config/barong.yml | any valid path to existing file | path to barong config with `activation_requirements`, `state_triggers`, `document_types` and `user_storage_titles` | +| `barong_maxminddb_path` | geolite/GeoLite2-Country.mmdb | any valid path to existing file | path to geolite country DB file | +| `barong_seeds_file` | config/seeds.yml | any valid path to existing file | path to configuration file with pre-defined API rules, users and levels | +| `barong_authz_rules_file` | config/authz_rules.yml | any valid path to existing file | path to configuration file with blacklisted and whitelisted API pathes | + +# Barong configurations overview +## Twilio configuration +For twilio configuration we need to set such required envs +- `BARONG_TWILIO_ACCOUNT_SID`, which acts as a twilio username +- `BARONG_TWILIO_SERVICE_SID`, which acts as a twilio password +- `BARONG_TWILIO_PHONE_NUMBER`, virtual phone numbers which will give you instant access to local, national, mobile, and toll-free phone numbers + +We have ability to set twilio with 3 different ways +1. ```BARONG_PHONE_VERIFICATION == "twilio_sms"``` + If you choose phone verification as twilio sms we will use send_sms [API call](https://www.twilio.com/docs/sms/send-messages) + Also you can add your own template for sms using `BARONG_SMS_CONTENT_TEMPLATE` +2. ```BARONG_PHONE_VERIFICATION == "twilio_verify"``` + In this case we will use twilio Verify [API call](https://www.twilio.com/docs/verify/api) + There are a lot of benefits of using Verify API like you can validate users via voice + One verification service can be used to send multiple verification tokens, it is not necessary to create a new service each time, so you can set ```BARONG_TWILIO_SERVICE_SID``` at once +3. ```BARONG_PHONE_VERIFICATION == "mock"``` + With this type of verification all the numbers will be accepted and validated as a right code for any given number + +--- + +## Blacklist/Whitelist configuration + +`Pass` routes will never be checked by AuthZ endpoint and will be available without session requirement. On `Block` routes user always will get 401, it doesn't depend on a session / role / ip / etc + +You need to put whitelisted (public) routes for pass object and blacklisted routes for block in authz_rules.yml + +```yml +rules: + pass: + - api/v2/barong/identity + - api/v2/peatio/public + - api/v2/ranger/public + - api/v2/applogic/public + block: + - api/v2/barong/management + - api/v2/peatio/managemen +``` + +--- + +## State configuration + +We can customize barong configuration as we want + +1. For user activation we just need to have verified email label in example below. You can put more labels to create your own rules for user activation +2. For example, if you want to ban your user you just need to put ban and fraud labels on tower admin panel. For sure you can customize this case too and put change or add label names in barong.yml +3. For document verification we use, as standard - following document types. But you can configure available document types by changing or extending existing list. This way we keep an opportunity to support any custom KYC services, logic, etc + +```yml +activation_requirements: + email: 'verified' +state_triggers: + banned: + - ban + - fraud + deleted: + - delete + locked: + - suspicious + - lock +document_types: + - Passport + - Identity card + - Driver license + - Utility Bill + - Residental + - Institutional +``` diff --git a/docs/development/mailer.md b/docs/development/mailer.md new file mode 100644 index 0000000..559a911 --- /dev/null +++ b/docs/development/mailer.md @@ -0,0 +1,39 @@ +# Run the barong mailer locally + +## Prerequisites +You need to following deamons running + +1. RabbitMQ +2. Redis + +## Start Rails console + +```bash +rails console +``` + +## Start the mailer + +1. Generate the barong public key in base64 format + +``` +cat config/rsa-key.pub | base64 -w0 (base64 -b0 for MacOS users) +``` + +2. Configure it in config/mailer.yml +Replace *changeme* with the previous generated string. + +```yaml + keychain: + barong: + algorithm: RS256 + value: "changeme" +``` + +## Generate an event + +From the rails console you can generate an event by creating a label on a user: + +```ruby +Label.create!(user_id:1, key: 'phone', value: 'verified') +``` diff --git a/docs/general/2fa.md b/docs/general/2fa.md new file mode 100644 index 0000000..0259b0e --- /dev/null +++ b/docs/general/2fa.md @@ -0,0 +1,70 @@ +# Setting up 2FA + +This document describes Barong [TOTP](https://tools.ietf.org/html/rfc6238) setup +using [Vault](https://www.vaultproject.io/intro/getting-started/install.html). + +## Prerequisites + +[Vault](https://www.vaultproject.io/intro/getting-started/install.html) +with [TOTP secrets engine](https://www.vaultproject.io/docs/secrets/totp/index.html#setup) enabled. + +## Configuration + +To use Vault with Barong you will need to set the following environment variables: + +```shell +export VAULT_ADDR=http://your-vault-url.com +export VAULT_TOKEN=12345-vault-t0k3n-54321 +``` + +To allow using Google Authenticator `VAULT_ADDR` should be _public_ ip. + +Note, that TOTP uses time-based algorithm. +So, if you want to test 2FA with phone, make sure, that your Vault's server time and your phone's time are synchronized, or it will not work. +[ntpdate](http://doc.ntp.org/4.1.1/ntpdate.htm) can help you to update your time with ntp servers: + +```shell +sudo ntpdate 0.ua.pool.ntp.org +``` + + +## Developer How-tos + +### Getting a code without Google Authenticator: + +* From _shell_: + + ```shell + $ vault login + $ vault read totp/code/IDMYAWESOMEID + ``` + +* From _rails console_: + + ```ruby + > me = Account.find_by_email('me@example.com') + > Vault.logical.read("totp/code/#{me.uid}") + ``` + +### Getting a new key (e.g. if you lost your Google Authenticator): + +* From _shell_: + + ```shell + $ vault login + $ vault write totp/keys/IDMYAWESOMEID \ + generate=true \ + issuer=Barong \ + account_name=me@example.com + ``` + +* From _rails console_: + + ```ruby + > me = Account.find_by_email('me@example.com') + > Vault::TOTP.send(:create, me.uid) + ``` + +Each response includes equivalent base64-encoded barcode and OTP url. +You can find the key's secret in this OTP url query params. + diff --git a/docs/general/activities.md b/docs/general/activities.md new file mode 100644 index 0000000..773bdff --- /dev/null +++ b/docs/general/activities.md @@ -0,0 +1,35 @@ +### Activities + +To track admin activities you need to define it on seed.yml on `permissions` key +- `role` should be in a range of existing: `admin`, `superadmin`, `support`, `techical`, `accountant` +- `verb` should be `post`, `get`, `put`, `delete` +- `path` - endpoint which should be checked, should be started with `api/v2/#{component}` as prefix +- `action` should be `audit` +``` +For example + +permissions: +- { role: 'admin', verb: 'post', path: api/v2/admin, action: audit } +``` + +Here you can see a list of possible fields for activity: +| Field | Type | Description | +|:-----------|:--------:|:-----------:| +| user_id | bigint | ID of user who creates activity | +| target_uid | string | User UID for whom activity was created (admin remove OTP for user, target_uid will be uid of user for which admin removed OTP| +| category | string | `admin` (admin activities), `user` (user activities)| +| user_ip | string | IP address | +| user_agent | string | User Agent such as `Mozilla/5.0`| +| topic | string | Defined topic (`session`, `adjustments`) or `general` by default| +| action | string | API action: `POST => 'create'`, `PUT => 'update'`, `GET => 'read'`, `DELETE => 'delete'`, `PATCH => 'update'` or `system` if there is no match of HTTP method| +| result | string | Status of API response: `succeed`, `failed`, `denied`| +| data | text | Parameters which was sent to specific API endpoint| +| created_at | datetime | Time of activity creation| + +##### Useful commands +If you want to delete old activities you can run next command + +Be sure that your parameters has valid date string, such as `YYYY-mm-dd` ! +``` +bundle exec rake activities:delete[from,to] +``` diff --git a/docs/general/api-keys.md b/docs/general/api-keys.md new file mode 100644 index 0000000..aad1d70 --- /dev/null +++ b/docs/general/api-keys.md @@ -0,0 +1,142 @@ +# Barong API keys creation and usage + +This document explains how to create an API key on barong using the UI or command line tool. +This API key can be used to access microservices in the cluster protected by barong authentication. +You can find below an example how to use the API key. + +## How to create API key ? + +### Using UI (recommended option) + +1. Find API keys section (often located on profile page). + +![API-keys-section](../images/api-keys-1.jpeg) + +2. Create your API key and securely save Access Key and Secret Key + +![API-key-creation](../images/api-keys-2.jpeg) + +### Using API (use this option in case your frontend doesn't support API keys feature) + +1. Install [httpie](https://httpie.org/) + +2. Login into your account using httpie + +```bash +http --session barong_session https://your.domain/api/v2/barong/identity/sessions \ + email=your@email.com password=changeme otp_code=000000 +``` +Example of response: +```json +{ + "created_at": "2020-06-01T07:01:20Z", + "csrf_token": "f5b36515a428328e199a", + "data": "{\"language\":\"en\"}", + "data_storages": [], + "email": "your@example.com", + "labels": [ + { + "created_at": "2020-06-01T07:01:45Z", + "key": "email", + "scope": "private", + "updated_at": "2020-06-01T07:01:45Z", + "value": "verified" + } + ], + "level": 5, + "otp": true, + "phones": [ + { + "country": "FR", + "number": "33*****0471", + "validated_at": "2020-06-01T07:03:18.000Z" + } + ], + "profiles": [], + "referral_uid": null, + "role": "member", + "state": "active", + "uid": "IDAF1AED1A42", + "updated_at": "2020-10-22T18:01:09Z" +} +``` + +3. Validate your session + +```bash +http --session barong_session https://your.domain.com/api/v2/peatio/account/balances +``` + +4. Create your API key + +``` +http --session barong_session https://your.domain.com/api/v2/barong/resource/api_keys \ + algorithm=HS256 totp_code=681757 x-csrf-token:f5b36515a428328e199a +``` + +Expected response: + +```json +{ + "algorithm": "HS256", + "created_at": "2019-12-23T12:22:15Z", + "kid": "61d025b8573501c2", // Access Key + "scope": [], + "secret": { + "auth": null, + "data": { + "value": "2d0b4979c7fe6986daa8e21d1dc0644f" // Secret Key + }, + "lease_duration": 2764800, + "lease_id": "", + "metadata": null, + "renewable": false, + "warnings": null, + "wrap_info": null + }, + "state": "active", + "updated_at": "2019-12-23T12:22:15Z" +} +``` + +5. Securely save Access Key and Secret Key + +## How to use API key ? + +To authenticate using API key you need to pass next 3 headers: + +| Header | Description | +| ---------------- | ------------------------------------------------------------ | +| X-Auth-Apikey | Access Key for API key (see 'How to create API key section ?') | +| X-Auth-Nonce | Timestamp in milliseconds (can be passed as a string) | +| X-Auth-Signature | HMAC-SHA256, calculated using concatenation of X-Auth-Nonce and Access Key | + +1. Generate X-Auth-Nonce - unique string (e.g current unix timestamp) + +```bash +date +%s%3N +1584524005143 +``` + +Nonce will be validated on server side to be not older than 5 seconds from the generation moment + +2. Calculate X-Auth-Signature header. + +X-Auth-Signature is HMAC-SHA256, calculated using concatenation of X-Auth-Nonce and Access Key. + +```ruby +nonce = (Time.now.to_f * 1000).to_i.to_s # timestamp in milliseconds, ex: 1584524005143 +access_key = '61d025b8573501c2' # Access Key from 'How to create API key section ?' +secret_key = '2d0b4979c7fe6986daa8e21d1dc0644f' # Secret Key from 'How to create API key section ?' +OpenSSL::HMAC.hexdigest("SHA256", secret_key, nonce + access_key) +# => "bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13" +``` + +3. Pass your headers in httpie (note `--session` is not needed anymore) + +``` +http https://your.domain.com/api/v2/peatio/account/balances \ + "X-Auth-Apikey: 61d025b8573501c2" \ + "X-Auth-Nonce: 1584524005143" \ + "X-Auth-Signature: bd42b945e095880e28d046846dbecf655fdf09d95a396a24fe6fe1df42f15d13" +``` diff --git a/docs/general/auth0.md b/docs/general/auth0.md new file mode 100644 index 0000000..e597dce --- /dev/null +++ b/docs/general/auth0.md @@ -0,0 +1,48 @@ +# Auth0 integration + +## How to create an application + +When you signed up for Auth0, a new application was created for you, or you could have created a new one (the most appropriate application for our structure is Single Page Application). +![Application](../images/auth0_dashboard.png) + +You will need some details about that application to communicate with Auth0. You can get these details from the Application Settings section in the Auth0 dashboard. + +![Settings](../images/auth0_settings.png) + +You should put `auth0_domain` from the Domain field and `auth0_client_id` from the Client ID field + +For a single page application better to use authorization code flow with proof key for code exchange. + +## Authorization Code Flow with Proof Key for Code Exchange (PKCE) +When public clients request Access Tokens, some additional security concerns are posed that are not mitigated by the Authorization Code Flow alone. This is because single-page apps cannot securely store a Client Secret because their entire source is available to the browser. + + +### How it works +![PKCE](../images/auth0_pkce.png) +Because the PKCE-enhanced Authorization Code Flow builds upon the standard Authorization Code Flow, the steps are very similar. + +1. The user clicks Login within the application. + +2. Auth0's SDK creates a cryptographically-random code_verifier and from this generates a code_challenge. + +3. Auth0's SDK redirects the user to the Auth0 Authorization Server (/authorize endpoint) along with the code_challenge. + +4. Your Auth0 Authorization Server redirects the user to the login and authorization prompt. + +5. The user authenticates using one of the configured login options and may see a consent page listing the permissions Auth0 will give to the application. + +6. Your Auth0 Authorization Server stores the code_challenge and redirects the user back to the application with an authorization code, which is good for one use. + +7. Auth0's SDK sends this code and the code_verifier (created in step 2) to the Auth0 Authorization Server (/oauth/token endpoint). + +8. Your Auth0 Authorization Server verifies the code_challenge and code_verifier. + +9. Your Auth0 Authorization Server responds with an ID Token and Access Token (and optionally, a Refresh Token). + +10. Your application can use the Access Token to call an API to access information about the user. + +11. The API responds with requested data. + +You can try to call your [API using the authorization Code Flow with PKCE](https://auth0.com/docs/flows/call-your-api-using-the-authorization-code-flow-with-pkce). + +Also you can find a link with [Authentication API description](https://auth0.com/docs/api/authentication#introduction) here. diff --git a/docs/general/captcha.md b/docs/general/captcha.md new file mode 100644 index 0000000..1453893 --- /dev/null +++ b/docs/general/captcha.md @@ -0,0 +1,32 @@ +# Barong Captcha Policy +#### Overview +A CAPTCHA (an acronym for "Completely Automated Public Turing test to tell Computers and Humans Apart") is a type of challenge–response test used in computing to determine whether or not the user is human) [Link to wiki](https://en.wikipedia.org/wiki/CAPTCHA) + +Currently Barong versions 2.3+ supports 3 options in captcha policy on `sign up` and `sign in` API endpoints. + +Configuration manages through environment variable - `BARONG_CAPTCHA`. Available values - `geetest`, `recaptcha`, `none`. +With a wrong value barong will fail on start with error: `#{KEY} invalid, enabled values: NONE GEETEST RECAPTCHA`. + +## Disabled (default) +`none` - if ENV `BARONG_CAPTCHA` has this value - no captcha response will be required on sign in and sign up, so no bot traffic prevention. +This option is not recommended to use in `production` environment. +`None` policy was designed in testing and demo purposes, to start barong without any additional keys. + +## Re CAPTCHA v2 +reCAPTCHA is a free service that protects your site from spam and abuse. It uses advanced risk analysis techniques to tell humans and bots apart. [Get started from google team](https://developers.google.com/recaptcha/intro) + +`recaptcha` - this value in `BARONG_CAPTCHA` env enables re_captcha protection, designed and maintained by Google company. [Small developers tips from google team](https://developers.google.com/recaptcha/docs/display) + +To properly configurate re_captcha you will need to set value for ENVs `recaptcha_site_key` and `recaptcha_secret_key`. Both of them you can generate [in google admin panel](https://www.google.com/recaptcha/admin/create) + +After enabling and configuring captcha, `sign up` and `sign in` endpoint will require new parameter - `captcha_response`(`string`) and validate captcha response on server side, to protect from bots traffic. + +## Geetest Captcha (Puzzle captcha) + +GeeTest captcha is an user-friendly captcha with high security. GeeTest captcha enables digital businesses to secure control of their websites against bots. [geetest captcha site](https://www.geetest.com) + +`geetest` - this value in BARONG_CAPTCHA env enables geetest captcha protection, designed and maintained by geetest.com + +To properly configurate `geetest` you will need to set value for ENVs `geetest_id` and `geetest_key`. How to generate them, you can find in official [get started guide](https://docs.geetest.com/captcha/overview/guide) + +After enabling and configuring geetest captcha, `sign up` and `sign in` endpoint will require new parameter - `captcha_response`(`hash` - with three keys `geetest_challenge`, `geetest_seccode`, `geetest_validate`) and validate captcha response on server side, to protect from bots traffic. diff --git a/docs/general/encryption.md b/docs/general/encryption.md new file mode 100644 index 0000000..4a51566 --- /dev/null +++ b/docs/general/encryption.md @@ -0,0 +1,46 @@ +## Encryption + +Data sensitivity will be defined as follow:\ +**Low**: IP address\ +**Medium**: Email address, Location data\ +**High**: Full name, Street address, phone number, date of birth\ +**Very High**: Passport number, Driver’s license number + +Low and Medium will not be masked on the UI. + +| Field | Mask | Comment | +|---|---|---| +| Street address | No mask | No need | +| First Name | No mask | No need | +| Last Name | B****** | Display first letter | +| Phone Number | +380 **** 4556 | Display country code and last 4 digits | +| Date of birth | 1980-01-** | Hide day | +| Document Number | FG****64 | First 2 number and last 2 digits | + +### Approach +Rails offers a handy ActiveSupport::MessageEncryptor class, that hides away all the complexity of data encryption, and was wrapped in a simple to use service object or reusable module. + +Service object class doing the actual heavy lifting, but only exposing two straightforward public class methods encrypt and decrypt. + +System have weekly salt rotation, so encrypted keys in DB will be prepended with salt, which will be mix of year and week number starting from 0. + +Make sure to store `SECRET_KEY_BASE` somewhere safe otherwise, you would not be able to decrypt your secure data, also you need to have this ENV variable at the start of your application as it will not create models (Profile, Phone, Documents) which have encrypted fields. + +#### Searching by encrypted values + +To have ability to search by encrypted fields, system implements additional field named `attribute_index` which use [crc32 algorithm](http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html) for storing attribute value. + +Make sure you have `BARONG_CRC32_SALT` to make algrithm more powerful. + +#### Rotation + +To update all encrypted fields to latest key values (salt will be "#{current_year}#{current_week}"), you can use following rake tasks: + +`rake rotate:phones` +`rake rotate:profiles` +`rake rotate:documents` + +### Fields masking on user API + +Sensitive data fields like `last name`, `dob`, `phone number`, `document number` are masked in user API by default. +You can disable this masking by changing the environment variable `BARONG_API_DATA_MASKING_ENABLED` to `false`. diff --git a/docs/general/errors.md b/docs/general/errors.md new file mode 100644 index 0000000..5b46bbb --- /dev/null +++ b/docs/general/errors.md @@ -0,0 +1,179 @@ +# Barong errors list + +## Resource module errors + +``` +resource.labels.private - Can't update Label. +resource.user.no_activity No activity recorded or wrong topic +resource.user.empty_otp_code Cant delete account. 2FA is on, but otp_code is empty +resource.user.invalid_otp Cant delete account. 2FA is on, but otp_code is invalid +resource.user.missing_otp_code Cant delete account. 2FA is on, but otp_code is missing +resource.user.invalid_password Cant delete account. Password is wrong +resource.profile.not_exist User has no profile +resource.profile.exist Profile already exists +resource.api_key.2fa_disabled Only accounts with enabled 2FA alowed +resource.api_key.missing_otp Theaccount has enabled 2FA but OTP code is missing +resource.api_key.invalid_otp OTP code is invalid +resource.phone.twillio Something wrong with Twilio Client +resource.phone.invalid_num Phone number is invalid +resource.phone.number_exist Phone number already exists +resource.phone.verification_invalid Phone is not found or verification code is invalid +resource.documents.limit_reached Maximum number of documents already reached +resource.documents.limit_will_be_reached Documents amount will reach limit by this upload +resource.otp.already_enabled 2FA has been already enabled for this account +resource.otp.invalid OTP code is invalid +resource.password.doesnt_match New passwords don\'t match +resource.password.prev_pass_not_correct Previous password is not correct +resource.password.no_change_provided New password cant be the same, as old one +``` + +## Identity module errors + +``` +identity.user.invalid_referral_format Invalid referral uid format +identity.user.referral_doesnt_exist Referral doesn't exist +identity.user.active_or_doesnt_exist User doesn't exist or has already been activated' +identity.password.user_doesnt_exist User doesn't exist +identity.user.passwords_doesnt_match Passwords don't match +identity.user.utilized_token JWT has already been used +identity.session.invalid_params Invalid Email or Password +identity.session.invalid Invalid Session +identity.captcha.required captcha_response is required +identity.captcha.mandatory_fields Mandatory fields must be filled in +identity.session.deleted Your account is deleted +identity.session.not_active Your account is not active +identity.session.banned Your account is banned +identity.session.invalid_params Invalid Email or Password +identity.session.missing_otp The account has enabled 2FA but OTP code is missing +identity.session.invalid_otp OTP code is invalid +``` + +## Admin module errors + +``` +admin.user.update_himself Admin can't update himself +admin.user.enable_2fa Manual 2FA enabling not allowed +admin.user.state_no_change Can't change state, as its already {active} +admin.user.doesnt_exist User with such UID doesnt exist +admin.label.doesnt_exist Label with such key doesnt exist or not assigned to chosen user +admin.access.denied Access Denied: User is not Admin +admin.user.non_user_field Search field is not a user attribute +admin.user.no_matches Search result is empty array +admin.user.label_no_matches Search result is empty array +``` + +## General errors + +``` +record.not_found Record is not found +jwt.decode_and_verify Failed to decode and verify JWT +authz.invalid_session Failed to decode cookies +authz.user_not_active User is not active +authz.invalid_signature API Key header 'signature' is invalid +authz.apikey_not_active API Key state is 'inactive' +authz.disabled_2fa API Key owner has disabled 2FA +authz.invalid_api_key_headers Blank or missing API Key headers +authz.permission_denied Path is blacklisted +authz.unexistent_apikey X-Auth-Apikey header is invalid +``` + +## Validation errors +### Admin module + +``` +admin.user.non_integer_page +admin.user.non_positive_page +admin.user.non_integer_limit +admin.user.invalid_limit +admin.user.missing_uid +admin.user.empty_uid +admin.user.empty_state +admin.user.empty_otp +admin.user.empty_role +admin.user.one_of_state_otp +admin.user.one_of_state_otp_email +admin.user.missing_key +admin.user.empty_key +admin.user.missing_scope +admin.user.empty_scope +admin.user.missing_value +admin.user.empty_value +``` + +### Identity module + +``` +identity.user.missing_email +identity.user.empty_email +identity.user.missing_password +identity.user.empty_password +identity.user.missing_token +identity.user.empty_token +identity.user.missing_reset_password_token +identity.user.empty_reset_password_token +identity.user.missing_confirm_password +identity.user.empty_confirm_password + +identity.session.missing_email +identity.session.missing_password +identity.session.invalid_captcha_format +``` + +### Resource module + +``` +resource.otp.missing_code +resource.otp.empty_code + +resource.labels.missing_key +resource.labels.empty_key +resource.labels.missing_value +resource.labels.empty_value + +resource.documents.expire_not_a_date +resource.documents.invalid_format +resource.documents.already_expired +resource.documents.missing_doc_expire +resource.documents.empty_doc_expire +resource.documents.missing_doc_type +resource.documents.empty_doc_type +resource.documents.missing_doc_number +resource.documents.empty_doc_number +resource.documents.missing_upload + + +resource.user.missing_topic +resource.user.empty_topic +resource.user.missing_old_password +resource.user.empty_old_password +resource.user.missing_new_password +resource.user.empty_new_password +resource.user.missing_confirm_password +resource.user.empty_confirm_password + + +resource.profile.missing_first_name +resource.profile.missing_last_name +resource.profile.missing_dob +resource.profile.missing_address +resource.profile.missing_postcode +resource.profile.missing_city +resource.profile.missing_country + + +resource.api_key.missing_algorithm +resource.api_key.empty_algorithm +resource.api_key.empty_kid +resource.api_key.empty_scope +resource.api_key.missing_totp +resource.api_key.empty_totp +resource.api_key.missing_kid +resource.api_key.empty_state + + + +resource.phone.missing_phone_number +resource.phone.empty_phone_number +resource.phone.missing_verification_code +resource.phone.empty_verification_code +``` diff --git a/docs/general/event_api.md b/docs/general/event_api.md new file mode 100644 index 0000000..bcf8d77 --- /dev/null +++ b/docs/general/event_api.md @@ -0,0 +1,593 @@ +# RabbitMQ Barong Event API + +## Overview of RabbitMQ details + +Barong submits all events into three exchanges depending on event category (read next). + +The exchange name consists of three parts: + + 1) application name (typically `barong`) + + 2) fixed keyword `events`. + + 3) category of event, like `system` (generic system event), `model` (the attributes of some record were updated) + +The routing key looks like `user.password.reset.token`, `user.created`. +The event name matches the routing key but with event category appended at the beginning, like `system.user.password.reset.token`, `market.user.created`. + +## Overview of RabbitMQ message + +Each produced message in `Event API` is JWT (complete format). + +This is very similar to `Management API`. + +The example below demonstrates both generation and verification of JWT: + +```ruby +require "jwt-multisig" +require "securerandom" + +jwt_payload = { + iss: 'barong', + jti: SecureRandom.uuid, + iat: Time.now.to_i, + exp: Time.now.to_i + 60, + event: event_payload +} + + private_key = OpenSSL::PKey.read(Base64.urlsafe_decode64(private_key) + algorithm = 'RS256' + jwt = JWT::Multisig.generate_jwt jwt_payload, \ + { barong: private_key }, + { barong: algorithm } + + Kernel.puts "GENERATED JWT", jwt.to_json, "\n" + + verification_result = JWT::Multisig.verify_jwt jwt.deep_stringify_keys, \ + { barong: public_key }, { verify_iss: true, iss: "barong", verify_jti: true } + + decoded_jwt_payload = verification_result[:payload] + + Kernel.puts "MATCH AFTER VERIFICATION: #{jwt_payload == decoded_jwt_payload}." +``` + +The RabbitMQ message is stored in JWT field called `event`. + +## Overview of Event API message + +The typical event looks like (JSON): + +```ruby +event: { + record: { + foo: "ID30DD0DD986", + bar: "example@barong.io", + baz: "member", + qux: 0 + }, + name: "model.user.created" +} +``` + +The field `event[:name]` contains event name (same as routing key). +The fields `foo`, `bar`, `baz`, `qux` (example) are fields which carry useful information. + +# Barong Event API messages + +## Format of `model.user.created` event + +```ruby +event: { + record: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + }, + name: "model.user.created" +} +``` + +| Field | Description | +| ---------- | ----------------------------------- | +| `record` | Created user up-to-date attributes. | + +## Format of `model.user.updated` event + +```ruby +event: { + record: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 1, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + }, + changes: { + level: 0 + } + name: "model.user.updated" +} +``` + +| Field | Description | +| ---------- | ----------------------------------- | +| `record` | Created user up-to-date attributes. | +| `changes` | The changed user attributes and their values. | + + +## Format of `model.user.created` event + +```ruby +event: { + record: { + address:"Illinois", + city:"New Garfieldbury", + country:"COD", + dob:"1984-05-22", + first_name:"Irina", + last_name:"Heathcote", + postcode:"10029", + created_at:"2019-10-02T08:14:20Z", + updated_at:"2019-10-02T08:14:20Z", + user: { + email: "clarisa_larkin@sawayn.info", + level: 0, + otp: false, + referral_uid: nil, + role: "member", + state: "pending", + uid: "IDEA819FB3F1", + updated_at: "2019-10-02T08:14:20Z" + created_at: "2019-10-02T08:14:20Z" + } + }, + name: "model.profile.created" +} +``` + +| Field | Description | +| ---------- | -------------------------------------- | +| `record` | Created profile up-to-date attributes. | + +## Format of `model.user.updated` event + +```ruby +event: { + record: { + address:"Illinois", + city:"New Garfieldbury", + country:"COD", + created_at:"2019-10-02T08:14:20Z", + dob:"1984-05-22", + first_name:"Irina", + last_name:"Heathcote", + postcode:"10029", + updated_at:"2019-10-02T08:14:20Z", + user: { + email: "clarisa_larkin@sawayn.info", + level: 0, + otp: false, + referral_uid: nil, + role: "member", + state: "pending", + uid: "IDEA819FB3F1", + updated_at: "2019-10-02T08:14:20Z" + created_at: "2019-10-02T08:14:20Z" + } + }, + changes: { + first_name: "Vernell" + }, + name: "model.profile.updated" +} +``` + +| Field | Description | +| ---------- | ----------------------------------- | +| `record` | Profile up-to-date attributes. | +| `changes` | The changed profile attributes and their values. | + +## Format of `model.label.created` event + +```ruby +event: { + record: { + id: 1, + key: "email", + value: "verified", + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 2, + otp: false, + state: "active", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + } + }, + name: "model.label.created" +} +``` + +| Field | Description | +| ---------- | ----------------------------------- | +| `record` | Created label up-to-date attributes. | + +## Format of `model.label.updated` event + +```ruby +event: { + record: { + id: 1, + key: "new_key", + value: "verified", + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 2, + otp: false, + state: "active", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + } + }, + changes: { + key: "old_key" + } + name: "model.label.updated" +} +``` + +| Field | Description | +| ---------- | ----------------------------------- | +| `record` | Created label up-to-date attributes. | +| `changes` | The changed label attributes and their values. | + +## Format of `model.document.created` event + +```ruby +event: { + record: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [], + updated_at:"2019-01-28T08:35:29Z", + created_at:"2019-01-28T08:35:29ZZ", + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 2, + otp: false, + state: "active", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + } + } + name: "model.document.created" +} +``` + +| Field | Description | +| ------------ | --------------------------------- | +| `user` | The up-to-date user attributes. | +| `doc_type` | Document type. | +| `doc_expire` | Experation time for document. | +| `doc_number` | Document number. | +| `upload` | Array of updaded objects | +| `updated_at` | Time of document object creation | +| `created_at` | Time of last document update | + +## Format of `system.user.email.confirmation.token` event + +```ruby +event: { + record: { + user: { + uid: "ID739065AFD3", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T09:03:50Z", + updated_at: "2019-01-28T09:03:50Z" + }, + language: "EN", + domain: "www.barong.io", + token: "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDg2NjYyMzAsImV4cCI6MTU0ODY3MjIzMCwic3ViIjoiY29uZmlybWF0aW9uIiwiaXNzIjoiYmFyb25nIiwiYXVkIjpbInBlYXRpbyIsImJhcm9uZyJdLCJqdGkiOiI5OWJkNzFkMjU2NTdlMmI1YzI1MCIsImVtYWlsIjoiYWRtaW4xMjNAYmFyb25nLmlvIiwidWlkIjoiSUQ3MzkwNjVBRkQzIn0.OI5tL9kV6cA1JBAy7G5iqd3WplxcB-waHYKFjm83koMEpx2Hlw9fksq5lip5cIHTjR8i3ambFL40OaCwDNc1jAiDsHwuv2nLswgi88_M1G8KVFylboQdtgmH_cZiz-Y-51Fq2oqEID5QyJnsSMSJbfspb6A0JGT_V-SPK4WFZw43F_RKhlZBCrxojljMwd20rGqFPYirMgUpsfiW0_-mESXzQ7UK1eA8mYO7Id4y6JR2Yoo-JTloEnBL1M189tOz6LqmmQB0M_QjTiHG3y9I97Med3StgVziYo9qog9kJXyPuXbboddg__5WEhMcWbaToohoiT5UvpVJHKfgxEVaDg" + }, + name: "system.user.email.confirmation.token" +} +``` + +| Field | Description | +| ---------- | ------------------------------------------------ | +| `user` | The up-to-date user attributes. | +| `language` | The language. | +| `domain` | The domain name of barong. | +| `token` | Valid confirm-acc jwt token (mandatory param for user confirmation endpoint) `/identity/users/email/confirm_code`. | + +## Format of `system.user.email.confirmed` event + +```ruby +event: { + record: { + user: { + uid: "IDB1629BFE9E", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "active", + created_at: "2019-01-28T10:17:27Z", + updated_at: "2019-01-28T10:17:45Z" + }, + language: "EN", + domain: "www.barong.io" + }, + name: "system.user.email.confirmed" +} +``` + +| Field | Description | +| ---------- | -------------------------------- | +| `user` | The up-to-date user attributes. | +| `language` | The language. | +| `domain` | The domain name of barong. | + +## Format of `system.user.password.reset.token` event + +```ruby +event: { + record: { + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + }, + language: "EN", + domain: "www.barong.io", + token: "eyJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE1NDg2NjQ1OTUsImV4cCI6MTU0ODY3MDU5NSwic3ViIjoicmVzZXQiLCJpc3MiOiJiYXJvbmciLCJhdWQiOlsicGVhdGlvIiwiYmFyb25nIl0sImp0aSI6IjRhY2IzM2IzYmE2NDc0ZjY1YTI5IiwiZW1haWwiOiJhZG1pbjEyQGJhcm9uZy5pbyIsInVpZCI6IklEMzBERDBERDk4NiJ9.Rie4LCbkV0jVBbhMoceYx8a9uDA-ea9D1v790zlIqP_EY8Iue_OOKXYWiC1Y-55MPicFbknBILjZlPewvAF8ZrhqIt04ROsgBdDGEUGY_SnLWhXzqSx9-v_o_w2MVjLOUxvRBm6sD0RvL-_5LmOcLqhYtf7ZPUnPDwsvhDedqDfbXPEvI7OK2SZ-1uPAOg1IMOX1k7xaDt5I1Wp-Knr2DmEgwNYbIjaXraComYcMdtVSuYVJAufgA0kTADMeT3cV3jzGy9dNfs8heMCtf5tr72IbL0_N0VeUQj9uaPDUr4ntsYk7gOPmA3RSVrSismtYdBXA9oLA0b0YfOctiY9dqg" + }, + name: "system.user.password.reset.token" +} +``` + +| Field | Description | +| ---------- | ------------------------------------------------ | +| `user` | The up-to-date user attributes. | +| `language` | The language. | +| `domain` | The domain name of barong. | +| `token` | Valid reset-pass jwt token (mandatory param for password reset endpoint) `/identity/users/password/confirm_code`. | + +## Format of `system.user.account.deleted` event + +```ruby +event: { + record: { + user: { + uid: "IDB1629BFE9E", + email: "example@barong.io", + role: "member", + level: 1, + otp: false, + state: "deleted", + created_at: "2019-01-28T10:17:27Z", + updated_at: "2019-01-28T10:17:45Z", + } + }, + name: "system.user.account.deleted" +} +``` + +| Field | Description | +| ---------- | ------------------------------------------------ | +| `user` | The up-to-date user attributes. | + +## Format of `system.user.password.reset` event + +```ruby +event: { + record: { + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T09:42:36Z" + } + }, + name: "system.user.password.reset" +} +``` + +| Field | Description | +| ---------- | ------------------------------------------------ | +| `user` | The up-to-date user attributes. | + +## Format of `system.user.password.change` event + +```ruby +event: { + record: { + user: { + uid: "IDC554ED1D0F", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "active", + created_at: "2019-01-09T15:54:56Z", + updated_at: "2019-01-28T09:59:03Z" + } + }, + name: "system.user.password.change" +} +``` + +| Field | Description | +| --------- | ------------------------------------------------- | +| `user` | The up-to-date user attributes. | + +## Format of `system.document.verified` event + +```ruby +event: { + record: { + user: { + uid: "IDC554ED1D0F", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "active", + created_at: "2019-01-09T15:54:56Z", + updated_at: "2019-01-28T09:59:03Z" + }, + id: 1, + key: "something", + value: "verified" + }, + name: "system.document.verified" +} +``` + +| Field | Description | +| --------- | ------------------------------------------------- | +| `user` | The up-to-date user attributes. | + +## Format of `system.document.rejected` event + +```ruby +event: { + record: { + user: { + uid: "IDC554ED1D0F", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "active", + created_at: "2019-01-09T15:54:56Z", + updated_at: "2019-01-28T09:59:03Z" + }, + id: 1, + key: "something", + value: "rejected" + }, + name: "system.document.rejected" +} +``` + +| Field | Description | +| --------- | ------------------------------------------------- | +| `user` | The up-to-date user attributes. | + +## Format of `system.session.create` event +```ruby + event: { + record: { + user: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + }, + user_ip: "127.0.0.1", + user_agent: "Chrome" + }, + name: "system.session.create" + } +``` + +## Producing events using Ruby + +```ruby +require "bunny" + +def generate_jwt(jwt_payload) + Kernel.abort "Please, see «Overview of RabbitMQ message» for implementation guide." +end + +Bunny.run host: "localhost", port: 5672, username: "guest", password: "guest" do |session| + channel = session.channel + exchange = channel.direct("barong.events.model") + jwt_payload = { + iss: "barong", + jti: SecureRandom.uuid, + iat: Time.now.to_i, + exp: Time.now.to_i + 60, + event: { + record: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 0, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" + }, + name: "model.user.created" + } + } + exchange.publish(generate_jwt(jwt_payload), routing_key: "user.created") +end +``` + +IMPORTANT: Don't forget to implement the logic for JWT exception handling! + +## Producing events using `rabbitmqadmin` + +`rabbitmqadmin publish routing_key=user.created payload=JWT exchange=barong.events.model` + +Don't forget to pass environment variable `JWT`. + +## Consuming events using Ruby + +```ruby +require "bunny" + +def verify_jwt(jwt_payload) + Kernel.abort "Please, see «Overview of RabbitMQ message» for implementation guide." +end + +Bunny.run host: "localhost", port: 5672, username: "guest", password: "guest" do |session| + channel = session.channel + exchange = channel.direct("barong.events.model") + queue = channel.queue("", auto_delete: true, durable: true, exclusive: true) + .bind(exchange, routing_key: "user.created") + queue.subscribe manual_ack: true, block: true do |delivery_info, metadata, payload| + Kernel.puts verify_jwt(JSON.parse(payload)).fetch(:event) + channel.ack(delivery_info.delivery_tag) + rescue => e + channel.nack(delivery_info.delivery_tag, false, true) + end +end +``` + +IMPORTANT: Don't forget to implement the logic for JWT exception handling! diff --git a/docs/general/password_hashing.md b/docs/general/password_hashing.md new file mode 100644 index 0000000..1caf0b3 --- /dev/null +++ b/docs/general/password_hashing.md @@ -0,0 +1,71 @@ +## Barong password hashing ## + +### Overview ### + +Barong since 2.0 version use OpenBSD bcrypt() password hashing algorithm, that allow us easily store a secure hash of users' passwords. + +As a base Barong takes [bcrypt-ruby gem](https://github.com/codahale/bcrypt-ruby) - Ruby binding for the OpenBSD bcrypt() +With [rails 5 has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) it gives us full power of algorithm + +### How it works ### + +Hash algorithms take a chunk of data (e.g., user's password) and create a "digital fingerprint," or hash, of it. +Because this process is not reversible, there's no way to go from the hash back to the password. + +In other words: + + hash(p) #=> + +We store the hash and check it against a hash made of a potentially valid password: + + =? hash(just_entered_password) + +### Rainbow Tables + +But even this has weaknesses -- attackers can just run lists of possible passwords through the same algorithm, store the +results in a big database, and then look up the passwords by their hash: + + PrecomputedPassword.find_by_hash().password #=> "secret1" + +Our solution to this is to add a small chunk of random data -- called a salt -- to the password before it's hashed: + + hash(salt + p) #=> + +The salt is then stored along with the hash in the database, and used to check potentially valid passwords: + + =? hash(salt + just_entered_password) + +bcrypt-ruby automatically handles the storage and generation of these salts for you. + +Adding a salt means that an attacker has to have a gigantic database for each unique salt -- for a salt made of 4 +letters, that's 456,976 different databases. Pretty much no one has that much storage space, so attackers try a +different, slower method -- throw a list of potential passwords at each individual password: + + hash(salt + "aadvark") =? + hash(salt + "abacus") =? + etc. + +This is much slower than the big database approach, but most hash algorithms are pretty quick -- and therein lies the +problem. Hash algorithms aren't usually designed to be slow, they're designed to turn gigabytes of data into secure +fingerprints as quickly as possible. `bcrypt()`, though, is designed to be computationally expensive: + + Ten thousand iterations: + user system total real + md5 0.070000 0.000000 0.070000 ( 0.070415) + bcrypt 22.230000 0.080000 22.310000 ( 22.493822) + +If an attacker was using Ruby to check each password, they could check ~140,000 passwords a second with MD5 but only +~450 passwords a second with `bcrypt()`. + +## More Information + +`bcrypt()` is currently used as the default password storage hash in OpenBSD, widely regarded as the most secure operating +system available. + +For a more technical explanation of the algorithm and its design criteria, please read Niels Provos and David Mazières' +Usenix99 paper: +https://www.usenix.org/events/usenix99/provos.html + +If you'd like more down-to-earth advice regarding cryptography, I suggest reading Practical Cryptography by Niels +Ferguson and Bruce Schneier: +https://www.schneier.com/book-practical.html \ No newline at end of file diff --git a/docs/general/profiles.md b/docs/general/profiles.md new file mode 100644 index 0000000..bfe7d08 --- /dev/null +++ b/docs/general/profiles.md @@ -0,0 +1,45 @@ +# Barong + +## Profiles story and administration + +This document explain original profiles submit-n-verify process and possible customizations. + +## Version + +Story described in the document actual for latest 2.5 stable version and higher. + +## User side of the story + +`Comment`: Previously (in 2.3 and lower) user was able to submit only 1 profile, and all later modifications affect it. Starting from 2.4 we changed `user has_one profile` relation to `user has many profiles`. This was done first of all to be able to track history of modifications and to be able to control changes from admin panel. Meanwhile, it also brought additional manual verification step in the legacy KYC process. + +`Story`: +User can submit profile with following fields (all are optional by default): (via `POST /resource/profiles`) + +``` + t.string "first_name" + t.string "last_name" + t.date "dob" + t.string "address" + t.string "postcode" + t.string "city" + t.string "country" + t.text "metadata" +``` + +Profile creates with `drafted` state in database. At this point user can edit the information (via `PUT /resource/profiles`), and administrators will not review it yet. +Once all the information is edited and validated by user correctly, he can submit profile for verification (via `PUT /resource/profiles`) by passing `confirmation: true` in the params. Profile state changes to "submitted" in database and from now on this profile is pending for admin verification. + +Meanwhile, there is a possibility to skip this "edit" step and create a profile directly with `submitted` state. For this user need to pass `confirmation: true` parameter directly in `POST /resource/profiles`. + +After admin will verify the profile and mark it as `"verified"` or `"rejected"` user will be able to create a new profile with `drafted/submitted` state, if he need it. Flow mostly controls by a `server-side rule`: user can have `ANY` amount of profiles, but `ONLY ONE` of `drafted/submitted` at a time. + +## Admin side of the story + +Once user sumbit a profile (with state submitted) admin can verify or reject it, by changing a profile state and creating a correct label. Usually its `key: profile, value: verified/rejected'. + +Also administrator has an access to the full profiles history, so he can check and compare new changes with old profiles, if the exist. As well he has an information about previous decisions about profile verification per each request. + +Once profile is rejected or verified, admin can create new profile for user (via `POST admin/profiles`). +In this case, profile will have a 'submitted' state and 'author' field with admin UID in DB. + +`!!!Attention` By default, if admin creates a profile for user, the same admin account cant approve or reject this profile, he need to wait for second admin approval. However, this can be changed by env `BARONG_PROFILE_DOUBLE_VERIFICATION` which can receive 2 value: `true` for enabling and `false` for disabling the feature diff --git a/docs/images/amqp-mailer-schema.jpg b/docs/images/amqp-mailer-schema.jpg new file mode 100644 index 0000000..22f25b7 Binary files /dev/null and b/docs/images/amqp-mailer-schema.jpg differ diff --git a/docs/images/api-keys-1.jpeg b/docs/images/api-keys-1.jpeg new file mode 100644 index 0000000..1d0577e Binary files /dev/null and b/docs/images/api-keys-1.jpeg differ diff --git a/docs/images/api-keys-2.jpeg b/docs/images/api-keys-2.jpeg new file mode 100644 index 0000000..d0b9ddd Binary files /dev/null and b/docs/images/api-keys-2.jpeg differ diff --git a/docs/images/auth0_dashboard.png b/docs/images/auth0_dashboard.png new file mode 100644 index 0000000..a25ca62 Binary files /dev/null and b/docs/images/auth0_dashboard.png differ diff --git a/docs/images/auth0_pkce.png b/docs/images/auth0_pkce.png new file mode 100644 index 0000000..701918f Binary files /dev/null and b/docs/images/auth0_pkce.png differ diff --git a/docs/images/auth0_settings.png b/docs/images/auth0_settings.png new file mode 100644 index 0000000..5c3dbe2 Binary files /dev/null and b/docs/images/auth0_settings.png differ diff --git a/docs/images/mailer-retry.jpg b/docs/images/mailer-retry.jpg new file mode 100644 index 0000000..9ea9989 Binary files /dev/null and b/docs/images/mailer-retry.jpg differ diff --git a/docs/kycaid.md b/docs/kycaid.md new file mode 100644 index 0000000..3b40738 --- /dev/null +++ b/docs/kycaid.md @@ -0,0 +1,47 @@ +# Barong KYCAID configuration +In Barong 2.5 and higher you can use the external KyC provider (KYCAID)[https://www.kycaid.com/] to fully automated your KYC process. + +### Basic configuration +Barong now support 2 ways of managing KYC - `local` and `kycaid`. You can switch between them using `BARONG_KYC_PROVIDER` env. By default this ENV has `kycaid`, as a value. If you set `local`as a value, then legacy flow will be turned on. `Profile` and `Document` steps will require manual admin approve from tower. + +### Credentials configuration +Once every credential is setted up correctly `KYCAID` will work in Barong out of the box. Lets see what we need to set up to make it work properly: + +1) We need to set up a bucket for storing docs (docs submitted by user will be stored both in barong and on KYCAID side) +Basic creds needed for that should be set in envs: `barong_storage_provider`, `barong_storage_bucket_name`, `barong_storage_access_key`, `barong_storage_secret_key`. +More about available options read here: [Barong storage configuration](https://www.openware.com/sdk/docs/barong/configuration.html#storage-configuration) +2) We need to set up authorization creds for KYCAID (get one on official site https://www.kycaid.com/). ENVs for that available under namings: +`barong_kycaid_authorization_token` - for auth secret token, +`barong_kycaid_sandbox_mode` - for switching between test mode and prod mode (`true` by default) +3) Be sure to check if `barong_domain` ENV has a correct value including `https:` at the beginning. This ENV configures the `callback` url, so its improtant. +4) Be sure to check `BARONG_REQUIRED_DOCS_EXPIRE` ENV value to be `false` if you want to include `address` verification in your KYC process. You can set it to `true` if only document check needed. +5) Check if in the `authz_rules.yml` file you have permitted path `- api/v2/barong/public` in the `pass` module. This allows platform to receive `callback`, as it is in the public module +6) Check if in the `barong.yml` file you have correct list of `document_types`. Be sure, that you have at least this values inside: + - Passport + - Identity card + - Driver license + - Address + +### Sidekiq +Dont forget to check if your deployment / local installation has running sidekiq. As soon as all the jobs are running a-sync, we use sidekiq to interact with `KYCAID` in order to make user verification fast and clear from user point of view. + +Sidekiq runs with a command `bundle exec sidekiq` with the same image and ENVs as barong. + +### Verification flow explanation +First 2 common steps of KYC remains unchanged. We still use internal email verification and phone verification using Twilio. +#### Profile step +After user has submitted profile - we send a request to KYCAID, registering this user in KYCAID database and creating a n `applicant` record for him. At this step no additional checks provided by KYCAID team. + +#### Document step +User can submit docs (2 or 3), depending on the type he choose. It can be `passport`, it can be `driver license`, etc. +User need to provide photo of `first page` of document, `second page` if needed and `SELFIE`. +After user has submitted all the docs, we send them to the `bucket`, configured by platform (to save them internally and show on `tower`) and also send files to `KYCAID`, downloading them to `KYCAID database`. + +After that Barong triggers `verification request` with 2 types - `DOCUMENT` check and `FACIAL` check. From this moment verifications and checks are performing on `KYCAID` side (check their site and contact support to find out more about `algorithms` here https://www.kycaid.com/). KYCAID team sends back a `callback` with decision. +In our system, both `FACIAL` and `DOCUMENT` resolutes to one label, with key = `document`, so if one of those verifications fails - end user will get `reject` on his verification `attempt`. +If verification decision is `approve` user will automatically get updated label to `verified` and corresponding level. + +#### Address step +Address step implemented pretty the same, as previous, document one. User sumbits document, that proves his residence. +After user has submitted all the docs, we send them to the `bucket`, configured by platform (to save them internally and show on `tower`) and also send files to `KYCAID`, downloading them to `KYCAID database`. +After that Barong triggers `verification request` with only 1 type - `ADDRESS`. After all verification process on the `KYCAID` side `BARONG` receives callback with decision. If its positive - user get his level and `address`:`verified` label. diff --git a/docs/mailer.md b/docs/mailer.md new file mode 100644 index 0000000..ca6e0da --- /dev/null +++ b/docs/mailer.md @@ -0,0 +1,239 @@ +# Barong mailer + +Mailer in the barong app is a deamon listening events from RabbitMQ, it renders emails using templates in the language set in user profile (if possible) and send emails. + +This guide explains the basics of using mailer to manage events from rubykube Event API. + +It is usually used by following components: + +- [Barong](https://www.openware.com/sdk/docs.html#barong) +- [Peatio](https://www.openware.com/sdk/docs.html#peatio) + +Read more about [Peatio Event API](https://www.openware.com/sdk/docs/peatio/api/event-api.html). +Read more about [Barong Event API](https://www.openware.com/sdk/docs/barong/event-api.html). + +## Concepts + +An _Event_ is a message produced to message broker in [RFC7515](https://tools.ietf.org/html/rfc7515). + +Events have the following structure: + +```JSON +{ + "payload": "string", + "signatures": [ + { + "header": { + "kid":"string" + }, + "protected":"string", + "signature":"string" + } + ] +} +``` + +## Deep dive + +By specification defined in _RFC7515_ we can build a JSON Web Token. + +After parsing of prebuilt JWT, you will receive this payload with this structure. + +```JSON +{ + "iss": "string", + "jti": "string", + "iat": 1567777420, + "exp": 1567777480, + "event": { + "record": { + "user": { + "uid": "string", + "email": "string", + }, + "language": "string", + }, + "name": "string" + } +} +``` + +**Note:** All events should be properly signed using RS256 private key. + +Mailer validates the signature of each upcoming event. + +Further, we will see that mailer user can work with this payload directly. + +```JSON +{ + "record": { + "user": { + "uid": "UID12345678", + "email": "johndoe@example.com" + }, + "language": "EN" + }, + "changes": {}, + "name": "string" +} +``` + +## Run + +| Variable | Description | Required | Default | +| ------------------------------------ | ------------------------------ | -------- | ------------------- | +| `BARONG_EVENT_API_RABBITMQ_HOST` | Host of RabbitMQ daemon | _no_ | `localhost` | +| `BARONG_EVENT_API_RABBITMQ_PORT` | Port of RabbitMQ daemon | _no_ | `5672` | +| `BARONG_EVENT_API_RABBITMQ_USERNAME` | RabbitMQ username | _no_ | `guest` | +| `BARONG_EVENT_API_RABBITMQ_PASSWORD` | RabbitMQ password | _no_ | `guest` | +| `BARONG_SMTP_PASSWORD` | Password used for auth to SMTP | _yes_ | | +| `BARONG_SMTP_PORT` | Post of SMTP server | _no_ | `25` | +| `BARONG_SMTP_HOST` | Host of SMTP server | _no_ | `smtp.sendgrid.net` | +| `BARONG_SMTP_USER` | User used for auth to SMTP | _no_ | `apikey` | +| `BARONG_SENDER_EMAIL` | Email address of mail sender | _yes_ | | +| `BARONG_SENDER_NAME` | Name of mail sender | _no_ | `Barong` | + +```sh +./bin/mailer --config=config/mailer.yml run +``` + +Mailer creates one queue and bind pre-defined exchanges to it. + +## Configuration + +Mailer is a flexible tool, you can cusomize alomost everything. Biggest part of customizations defined in `mailer.yml`, but you can also modify templates. + +Each Event API provider uses own AMQP exchange and algorithm to sign payload. + +```yaml +exchanges: + barong: + name: barong.events.system + signer: peatio +``` + +Using keychain algorithms and defined public keys for each provider mailer will validate the data. + +```yaml +keychain: + barong: + algorithm: RS256 + value: "public_key" +``` + +In `events` you may define any event type from Event API providers and prepare email template for it. + +```yaml +events: + - name: Email Confirmation + key: user.email.confirmation.token + exchange: barong + templates: + EN: + subject: Registration Confirmation + template_path: email_confirmation.en.html.erb + RU: + subject: Подтверждение Регистрации + template_path: email_confirmation.ru.html.erb +``` + +The simpliest mailer configuration will look like this one: + +```yaml +keychain: + barong: + algorithm: RS256 + value: "changeme" + peatio: + algorithm: RS256 + value: "changeme" + +exchanges: + barong: + name: barong.events.system + signer: barong + +events: + - name: Email Confirmation + key: user.email.confirmation.token + exchange: barong_system + templates: + en: + subject: Registration Confirmation + template_path: email_confirmation.en.html.erb + ru: + subject: Подтверждение Регистрации + template_path: email_confirmation.ru.html.erb +``` + +## Templates + +Mailer exposes few variables for usage inside the templates. + +| Variable | Description | +| ---------- | -------------------------------------------- | +| `@user` | User related to this email | +| `@record` | Created user up-to-date attributes. | +| `@changes` | The changed user attributes and their values | + +### User + +User with related profile. + +| Variable | | +| ----------- | ----------------------------- | +| uid | Unique user id | +| email | User email | +| role | User role | +| level | User KYC level | +| state | State of user's account | +| referral_id | UID of referrer | +| profile | Information about KYC profile | + +### Record + +Record always containes `UID` and other attributes, that's why we can also expose user, by searching with unique user id. + +Example: + +```ruby +record: { + uid: "ID30DD0DD986", + email: "example@barong.io", + role: "member", + level: 1, + otp: false, + state: "pending", + created_at: "2019-01-28T08:35:29Z", + updated_at: "2019-01-28T08:35:29Z" +} +``` + +### Changes + +When entity changed, this will contain attributes before an udpate. + +Example: + +```ruby +changes: { + level: 0 +} +``` + + +### Queues and exchanges schema +![Mailer-Schema](images/amqp-mailer-schema.jpg) +### How retries works + +RabbitMQ Cluster is part of our infrastructure and the default queuing solution. RabbitMQ has Dead Letter Exchanges (DLX), which allows us to simulate message scheduling. + +#### Steps to test the solution: +1. Publish message to TargetQueue +2. Consumer gets the message and tries to process it +3. Process fails, consumer rejects the message +4. Rabbit routes the message to RetryExchange +5. Message moves to RetryQueue, sits for 2 minutes +6. When message expires, it is resent to TargetExchange and routed to TargetQueue + +![Mailer-Retry](images/mailer-retry.jpg) \ No newline at end of file diff --git a/docs/migrations/2_6_migration.md b/docs/migrations/2_6_migration.md new file mode 100644 index 0000000..17b24d5 --- /dev/null +++ b/docs/migrations/2_6_migration.md @@ -0,0 +1,64 @@ +# 2.6 Migration procedure + +## Introduction +2.6 version brings some important security improvments by allowing a better isolation of secrets in Vault. +API keys secrets where moved from Vault kv secret engine to transit engine. API keys secrets are encrypted/decrypted by Vault and stored encrypted in the main database, reducing the size of vault storage. + +## Setting a unique an meaningful application name +The application name is used as prefix of secrets stored in vault, it allows you to configure proper isolation using vault policies, see [Vault](https://www.openware.com/sdk/docs/barong/vault.html) documentation for more details about ACL configuration. + +The configuration entry for the application name is `barong_vault_app_name` (or the environment variable BARONG_VAULT_APP_NAME), see [Barong Configuration](https://www.openware.com/sdk/docs/barong/configuration.html) for more details. + +## Vault token for the migration +To export and import the TOTP you need to use the vault root token or a token with the following policies. +Replace *opendax* with your vault application name. + +``` +# Read api keys +path "secret/barong/api_key/*" { + capabilities = ["read"] +} + +# Manage the transit secrets engine +path "transit/keys/*" { + capabilities = [ "create", "read", "list" ] +} + +# Encrypt engines secrets +path "transit/encrypt/opendax_apikeys_*" { + capabilities = [ "create", "read", "update" ] +} + +# Decrypt engines secrets +path "transit/decrypt/opendax_apikeys_*" { + capabilities = [ "create", "read", "update" ] +} + +# Export otp +path "totp/export/*" { + capabilities = ["read"] +} + +# Create otp code +path "totp/keys/*" { + capabilities = ["create", "read", "delete"] +} +``` + +## Migrate the API keys +This will fetch the API keys from Vault kv secrets store engine, encrypt them with transit, store the encrypted version in the database and finally delete the legacy version stored in vault. + +``` +rake migrate:26-api-keys +``` + +## Migrate the TOTP secrets +TOTP secrets can't be exported by the official Vault build. +To do so you must use the openware patched version available in the docker container *quay.io/openware/vault:1.5.3-openware* + + +Then run the following command + +``` +rake migrate:26-totp +``` diff --git a/docs/migrations/postgresql.md b/docs/migrations/postgresql.md new file mode 100644 index 0000000..4273f3a --- /dev/null +++ b/docs/migrations/postgresql.md @@ -0,0 +1,9 @@ +# Use PostgreSQL with Barong + +Barong supports PostgreSQL 13.0 and higher. +To use PostgreSQL you need to set the following environment variables: + +```bash +export DATABASE_ADAPTER="postgresql" +export DATABASE_PORT="5432" +``` diff --git a/docs/releases/2.1.0.md b/docs/releases/2.1.0.md new file mode 100644 index 0000000..1668a9b --- /dev/null +++ b/docs/releases/2.1.0.md @@ -0,0 +1,85 @@ +## Barong 2.1.0 (April 22, 2019) ## + +### Overview ### + +Release includes significant new features, numerous functional fixes and stabilizing patches. +Barong 2.2.0 comes without views, controllers, huge dependencies, but with clean, fast and readable API. +Release is optimized, more compact and elegant version of OAuth server concept. +Since we've removed dependencies like `devise` a lot of the flow was not only recoded but implemented in fully new way. + +This release notes is must-read for migrating from 2.0 version. + +### Release is NOT compatible with 1.9 or older versions of barong ### + +### New features ### + +* [#655](https://github.com/rubykube/barong/pull/655): Reworked (complaining to 1.9) seed feature. Supports seeding levels, users with optional parameters and labels + +* [#679](https://github.com/rubykube/barong/pull/679): Adds an ability to upload N number of documents. Maximum number can be configured by ENV + +* [#665](https://github.com/rubykube/barong/pull/665): Support of GeeTest captcha and captcha configuration + +* [#681](https://github.com/rubykube/barong/pull/681): Move all admin functionality to API + +* [#686](https://github.com/rubykube/barong/pull/686): New authorize endpoint (White- and Black- listing features, Rails Metal based (optimized), api keys and cookies logic) + +* [#716](https://github.com/rubykube/barong/pull/716): Ability to specify and control CORS policy + +* [#726](https://github.com/rubykube/barong/pull/726): Support asian language characters, rework validations + +* [#734](https://github.com/rubykube/barong/pull/734): Add tokens blacklisting feature. Additional checks to avoid reusing tokens + +* [741](https://github.com/rubykube/barong/pull/741): Unify errors format to prepare base for translations + + +### Enhancements ### + +* [#677](https://github.com/rubykube/barong/pull/677): Improve activities coverage +* [#688](https://github.com/rubykube/barong/pull/688): Add change password endpoint +* [#693](https://github.com/rubykube/barong/pull/693): Change multiple docs uploading logic from object to array to become compatible with browser files transfering policy +* [#697](https://github.com/rubykube/barong/pull/697): Add label on profile adding without level increase to strictly +* [#709](https://github.com/rubykube/barong/pull/709): Add referral_id to users table and as an optional param on signup +* [#707](https://github.com/rubykube/barong/pull/707): Update to ruby 2.6.0 +* [#745](https://github.com/rubykube/barong/pull/745): Add event API documentation with examples +* [#767](https://github.com/rubykube/barong/pull/767): Add language field in reset pass and confirm acc events +* [#763](https://github.com/rubykube/barong/pull/763): User controller improvements + ** Fixed inconsistent indentation in user controller + ** Rework POST '/get' request to allow to receive email and phone num + ** Added several additional tests +* [#771](https://github.com/rubykube/barong/pull/771): Add pagination on /resource/users/activity endpoint +* [#769](https://github.com/rubykube/barong/pull/769): Search on user fields, admin API +* [#785](https://github.com/rubykube/barong/pull/786): Add Entities::Activity model, include it Entities::UserWithFullInfo +* [#789](https://github.com/rubykube/barong/pull/789): Use 'paginate' on api/v2/admin/users/search +* [#804](https://github.com/rubykube/barong/pull/804): DESC order in user activity API +* [#807](https://github.com/rubykube/barong/pull/807): Allow to list API Keys without providing OTP code +* [#809](https://github.com/rubykube/barong/pull/809): Update rails-related gems versions in favor of vulnerability alerts + +### Fixes ### +* [#658](https://github.com/rubykube/barong/pull/658): Fix share cookies for development, fix Bump +* [#664](https://github.com/rubykube/barong/pull/664): Fix user activity api endpoint bug https://github.com/rubykube/barong/issues/663 +* [#674](https://github.com/rubykube/barong/pull/674): Added missing validations on password update functionality +* [#675](https://github.com/rubykube/barong/pull/675): Fix unhandled totp errors +* [#683](https://github.com/rubykube/barong/pull/683): Hotfix for whitelisting public peatio and barong routes +* [#684](https://github.com/rubykube/barong/pull/684): Add missing link_config script(required for deployment process) +* [#685](https://github.com/rubykube/barong/pull/685): Update the production database configuration with DATABASE_NAME from env +* [#694](https://github.com/rubykube/barong/pull/694): Add missing event with confirmation token on signup +* [#699](https://github.com/rubykube/barong/pull/699): Add carrierwave config for production +* [#702](https://github.com/rubykube/barong/pull/702): Small fixes (drone ci, event api updated_at bug) +* [#706](https://github.com/rubykube/barong/pull/706): Add missing users entity on login and /me +* [#718](https://github.com/rubykube/barong/pull/718): Move ActionDispatch::Session::CookieStore to sessions controller +* [#720](https://github.com/rubykube/barong/pull/720): Add ability to configure expire_after time through env variable, fix hardcoded one +* [#724](https://github.com/rubykube/barong/pull/724): Fix change password api action from post to put +* [#713](https://github.com/rubykube/barong/pull/713): Split swagger doc into restful and management) +* [#737](https://github.com/rubykube/barong/pull/737): Add storage envs in config store and update fetch logic +* [#730](https://github.com/rubykube/barong/pull/730): Fix change code on phone initialize, remove code from logs +* [#739](https://github.com/rubykube/barong/pull/739): Fix Barong::CORS load problem in production env +* [#742](https://github.com/rubykube/barong/pull/742): Add missing user details in 2fa login response +* [#749](https://github.com/rubykube/barong/pull/749): Fix session_id check & add bypass session lazy load +* [#753](https://github.com/rubykube/barong/pull/753): Change default set-cookie header policy on authz to :skip +* [#757](https://github.com/rubykube/barong/pull/757): Take only data from Vault::Secret object && minor refactoring +* [#747](https://github.com/rubykube/barong/pull/747): Phone API improvements (additional error) +* [#760](https://github.com/rubykube/barong/pull/760): Add additional error on login in case of banned user +* [#768](https://github.com/rubykube/barong/pull/768): Add more API Key validations, fix error on creating with invalid algo +* [#772](https://github.com/rubykube/barong/pull/772): Split activity no records error to wrong_topic and no_activity +* [#776](https://github.com/rubykube/barong/pull/776): Use destroy instead of delete to enable missing validations +* [#805](https://github.com/rubykube/barong/pull/805): Move event api jwt private key to Barong::App.config store diff --git a/docs/releases/2.3.0.md b/docs/releases/2.3.0.md new file mode 100644 index 0000000..c276b09 --- /dev/null +++ b/docs/releases/2.3.0.md @@ -0,0 +1,114 @@ +## Barong 2.3.0 ## + +### Overview ### + + We are pleased to present Barong Open Source 2.3.0. + + This release concentrated on improving overall project flexibility, security and session updates and several new storage/phone providers and updates + + 1. Server side sessions via rails cache + 2. Restrictions and related Admin API + 3. Support Twilio Verify service (without dropping legacy sms solution) + 4. Flexible state - labels flow + 5. Support Ali Cloud storage provider and S3 custom storages + + This release notes is must-read for migrating from older versions. + +### New Features ### +- Add ability to configure sms content for phone verification [#877](https://github.com/rubykube/barong/pull/877) ([ec](https://github.com/ec)) + - This PR introduce an ability to configurate (via ENV) before deployment the future Twilio sms content: + - "Your verification code for Barong: {{code}}"" - default one + - You can change position of verification code, words and content in template + - e.q. "Hoooray! Here is magic {{code}} for you to verify your account!"" + +- Simple rake task to create users and api keys [#816](https://github.com/rubykube/barong/pull/816) ([ec](https://github.com/ec)) + - [ATTENTION] The best usage for the rake task - testing purpose + - You can generate up to N (1000 by default) users with valid api keys to simulate any platform actions +- Add Resctrictions [#891](https://github.com/rubykube/barong/pull/891) ([dnfd](https://github.com/dnfd)) + - AuthZ level validations to ban traffic on a network base. + - Available types of validations: IP IP_SUBNET CONTINENT COUNTRY + - After being created restriction can be toggled to be turned ON or OF via admin api + - Full support of CRUD functionality in the admin module +- Feature Ability to change UID prefix with BARONG_UID_PREFIX=ABC [#893](https://github.com/rubykube/barong/pull/893) ([ec](https://github.com/ec)) + - This PR introduce an ability to configurate (via ENV) before deployment the future user UIDs: + - ID{RANDOM_HASH} - default + - MYAWESOMEAPP{RANDOM_HASH} - available to configurate +- Add Management API: push document for user [#894](https://github.com/rubykube/barong/pull/894) ([ec](https://github.com/ec)) + - New management API call that gives an ability to push documents for the user in base64 + - Good base for any third-party KYC providers +- Add endpoint for delete restriction [#902](https://github.com/rubykube/barong/pull/902) ([chumaknadya](https://github.com/chumaknadya)) + - Admin endpoint to delete Restriction (remove from DB, cant be rolled back) +- Add state to Profile model [#910](https://github.com/rubykube/barong/pull/910) ([mnaichuk](https://github.com/mnaichuk)) + - Define 2 Profile states: partial and completed + - Allow to create EMPTY profile (with empty fields) + - Profile state - attributes dependency +- Ability to update and delete users profiles [#913](https://github.com/rubykube/barong/pull/913) ([ec](https://github.com/ec)) + - New admin and user API endpoints that allows to update / delete profiles +- Flexible state - labels flow [#907](https://github.com/rubykube/barong/pull/907) ([ec](https://github.com/ec)) + - State - Label dependency + - Similar to levels configuration now you can configurate requirements for each of the user states + - Separately - ACTIVE requirements + - Now on every label change (update, remove, delete) user state recalculates and forces to update + - Default values remains the same as in the <= 2.2 version +- Add AliCloud Carrierwave storage provider [#911](https://github.com/rubykube/barong/pull/911) ([dnfd](https://github.com/dnfd)) + - Now Barong support Ali Cloud storages and easily deploys on AliBaba cloud +- Add ability to restrict by geoip [#920](https://github.com/rubykube/barong/pull/920) ([dnfd](https://github.com/dnfd)) + - Add support of CONTINENT and COUNTRY restriction scopes +- Add ability to skip label creation on doc save [#922](https://github.com/rubykube/barong/pull/922) ([dnfd](https://github.com/dnfd)) +- Add ability to disable 2fa [#925](https://github.com/rubykube/barong/pull/925) ([dnfd](https://github.com/dnfd)) +- Add AliCloud Uploader [#926](https://github.com/rubykube/barong/pull/926) ([dnfd](https://github.com/dnfd)) + - Now Barong support Ali Cloud storages and easily deploys on AliBaba cloud +- Define DocumentTypes class for flexible doc type configuration [#935](https://github.com/rubykube/barong/pull/935) ([ec](https://github.com/ec)) + - This PR introduce an ability to predefine available document types before deployment. There is hardcoded freezed array anymore. + - By default, abailable document type list remains the same as <= 2.2 versions +- Make labels acts_as_eventable on: [create update] [#936](https://github.com/rubykube/barong/pull/936) ([ec](https://github.com/ec)) + - Spread events on label CUD +- Add the support of twilio verify API [#928](https://github.com/rubykube/barong/pull/928) ([ec](https://github.com/ec)) + - Now Barong supports twilio verification service ( via CALL and SMS ) + - Should be switched from SMS to Twilio Verify via ENV before deployment +- Server side sessions via cache_store [#949](https://github.com/rubykube/barong/pull/949) ([ec](https://github.com/ec)) + - Now session stores on the server side in the cache + - User receives only 1 session cookie, that proves he got it from Barong + - Session lifetime extends after every private call + - Session will automatically close if user will try to reuse cookies in another browser or IP network + - Session will expire in 24h + +### Enhancements ### +- Updating gem multisign and jwt [45f975](https://github.com/rubykube/barong/commit/45f975fcb489aaa3b0b4f56b1307258b8bc85d18) ([mod](https://github.com/mod)) +- Allow pending user to open session [#881](https://github.com/rubykube/barong/pull/881) ([ec](https://github.com/ec)) +- Update to ruby 2.6.3 and rails 5.2.3 [#867](https://github.com/rubykube/barong/pull/867) ([ec](https://github.com/ec)) +- Update models' annotations [#895](https://github.com/rubykube/barong/pull/895) ([dnfd](https://github.com/dnfd)) +- Small fixes in document and profile model [#901](https://github.com/rubykube/barong/pull/901) ([ec](https://github.com/ec)) +- Feature: Add redeploy on devkube step to drone [#874](https://github.com/rubykube/barong/pull/874) ([alinetskyi](https://github.com/alinetskyi)) +- Add validation for referral && Add ability to get referral uid [#882](https://github.com/rubykube/barong/pull/882) ([chumaknadya](https://github.com/chumaknadya)) +- Update nokogiri version [#906](https://github.com/rubykube/barong/pull/906) ([dnfd](https://github.com/dnfd)) +- Update Barong::App validations [#904](https://github.com/rubykube/barong/pull/904) ([dnfd](https://github.com/dnfd)) +- Support 'with replace' policy on labels update [#915](https://github.com/rubykube/barong/pull/915) ([ec](https://github.com/ec)) +- UPDATES: sessions delete, permissions update, log error on 500 [#917](https://github.com/rubykube/barong/pull/917) ([chumaknadya](https://github.com/chumaknadya)) +- Add :upload in list of params to be filtered [#919](https://github.com/rubykube/barong/pull/919) ([ec](https://github.com/ec)) +- Fix structure of event API messages [#912](https://github.com/rubykube/barong/pull/912) ([shal](https://github.com/shal)) +- Use Barong::App.config.barong_uid_prefix in referral validation [#923](https://github.com/rubykube/barong/pull/923) ([ec](https://github.com/ec)) +- Allow blank doc_expire for documents controllers [#927](https://github.com/rubykube/barong/pull/927) ([dnfd](https://github.com/dnfd)) +- Add system.session.create event with user and request_ip data [#916](https://github.com/rubykube/barong/pull/916) ([chumaknadya](https://github.com/chumaknadya)) +- Update seeds.yml [#934](https://github.com/rubykube/barong/pull/934) ([ec](https://github.com/ec)) +- Simple GET levels requirements endpoint for admin [#932](https://github.com/rubykube/barong/pull/932) ([ec](https://github.com/ec)) +- Get rid of 'serialize: JSON', accept 'metadata' as json instead of pure hash [#930](https://github.com/rubykube/barong/pull/930) ([ec](https://github.com/ec)) +- Add missing labels event api documentation [#937](https://github.com/rubykube/barong/pull/937) ([ec](https://github.com/ec)) +- Add session autorenew [#903](https://github.com/rubykube/barong/pull/903) ([dnfd](https://github.com/dnfd)) +- Introduce MockPhoneVerifyService [#941](https://github.com/rubykube/barong/pull/941) ([ec](https://github.com/ec)) +- Use uid instead of id on profile update && delete api [#943](https://github.com/rubykube/barong/pull/943) ([ec](https://github.com/ec)) +- Profile act as eventable on update [#945](https://github.com/rubykube/barong/pull/945) ([ec](https://github.com/ec)) +- Update ruby to 2.6.5 for security reasons [#946](https://github.com/rubykube/barong/pull/946) ([ec](https://github.com/ec)) +- Add ability to configure aws_signature_version && endpoint [#947](https://github.com/rubykube/barong/pull/947) ([ec](https://github.com/ec)) +- Feature: allow # \ () & ' : " in profile residental address field [#950](https://github.com/rubykube/barong/pull/950) ([ymasiuk](https://github.com/ymasiuk)) +- Add redis - hiredis dependency, specify driver in dev env [#951](https://github.com/rubykube/barong/pull/951) ([ec](https://github.com/ec)) +- Protect superadmin against changes from non-superadmin users [#955](https://github.com/rubykube/barong/pull/955) ([ec](https://github.com/ec)) +- Update storage-related gems [#957](https://github.com/rubykube/barong/pull/957) ([ec](https://github.com/ec)) + +### Bug Fixes ### +- Use CGI::escape instead of pure url in documents event api [#942](https://github.com/rubykube/barong/pull/942) ([ec](https://github.com/ec)) +- fix binstubs issue [#867](https://github.com/rubykube/barong/pull/867) ([ec](https://github.com/ec)) +- Fix rollback to pending state, add missing data in entities [#914](https://github.com/rubykube/barong/pull/914) ([ec](https://github.com/ec)) +- Handle Vault errors on POST /api/v2/resource/api_keys [#918](https://github.com/rubykube/barong/pull/918) ([dnfd](https://github.com/dnfd)) +- Avoid no method error UTC for nil entities [#921](https://github.com/rubykube/barong/pull/921) ([ec](https://github.com/ec)) +- Event API changes: remove blocking check for on_update events [#933](https://github.com/rubykube/barong/pull/933) ([ec](https://github.com/ec)) \ No newline at end of file diff --git a/docs/restrictions.md b/docs/restrictions.md new file mode 100644 index 0000000..ec07017 --- /dev/null +++ b/docs/restrictions.md @@ -0,0 +1,51 @@ +# Restrictions + +### Introduction +First version of restrictions appeared in 2.3 as simple blacklist feature, by IP in several scopes (by `country`, by `continent`, by `ip_subnet` and by `ip`). + +Starting from latest 2.4 restrictions will act as system of traffic control. New fields added to restrictions table - `category`, `code`, and new available value for `scope` - `all` + +##### Main points: +Every request to the server will be validated (both `public` and `private` APIs, but not `management` one ). Request IP will match with existing restrictions and a first found rule will be applayed. +`category` - one of `whitelist`, `maintenance`, `blacklist`, `blocklogin`. + +The order of matching IP with existing rules is strict and cant be changed: +1. Whitelist ( all -> ip -> ip_subnet -> country -> continent ) +2. Maintenance ( all -> ip -> ip_subnet -> country -> continent ) +3. Blacklist ( all -> ip -> ip_subnet -> country -> continent ) +4. Blocklogin ( all -> ip -> ip_subnet -> country -> continent ) + +`Whitelist` - rule, that marks IP as trusted, as IP that can have an access to the server APIs +`Maintenance` - when enabled, will return an error - 471 (by default) to any request to server in a range of rules `scope` (Typically, should be used with `all` and in maintenance platform purposes) +`Blacklist` - when enabled, will return an error - `code`, which can be customized by the rule. Code can be different for different `scopes`, and can be used to display different error or kind alert of UI. Default `code` for errors `{ continent: 423, country: 423, ip_subnet: 403, ip: 401, all: 401 }` +`Blocklogin` - acts as `Blacklist`, but applies only on `sessions` endpoint. + +##### How to configure +Restrictions can be: +1. Seeded via seed feature. +Just put restrictions in format of `- { category: whitelist, scope: country, value: UA, state: disabled }` in seed.yml under `restrictions:` module +2. Can be added by admin via `api/v2/barong/admin/restrictions`, + requires `:scope`, requires `:value`, requires `:category`, optional `:state, default: 'enabled`', optional `:code ` +3. Can be added automatically (whitelist category) via whitelink feature + +### Whitelink feature +In order to make the process of whitelisting IPs easier starting from latest 2.4 admin can create a `whitelink_token` via +`api/v2/barong/admin/restrictions/whitelink`. Created token can be sent to `api/v2/barong/identity/users/access` and if it is valid (every token has expiry time, which is 1 day by default) and it will automatically create a `whitelist` restriction with current request IP. +Note: `api/v2/barong/identity/users/access` API available to call even if all other platform traffic is under blacklist or maintenance + +### Restrictions Usage +Combining whitelist, maintenance and blacklist rules can help in controlling platform traffic, maintaining platform, developing custom cases and complex UI structure. +##### Several usecases: +##### Maintenance - rules: +`category: 'maintenance', scope: 'all', value: 'all', state: 'enabled', code: 471` +`category: 'whitelist', scope: 'ip_subnet', value: '#{dev_team_office_ip}', state: 'enabled'` + +In this case all users, that will try to access any API of the platform will get a responce with status `471` and body `authz.restrict.maintenance`. In this case frontend can show a maintenance page for users, so they will understand that platform is under service right now. Meanwhile, all requests from `#{dev_team_office_ip}` (as they are whitelisted) will still reach server and can test the update / validate any bug found, etc. + +##### Blacklisting - rules: +`category: 'blacklist', scope: 'country', value: 'US', state: 'enabled', code: 455` +`category: 'blacklist', scope: 'continent', value: 'NA', state: 'enabled', code: 456` +`category: 'whitelist', scope: 'ip', value: '#{investor_ip}', state: 'enabled'` + +In this case all users from North America, that will try to access any API of the platform will get a responce with status `456` and body `authz.restrict.blacklist`. In this case frontend can show a page for users, so they will understand that they cant yet reach this platform from their current location, but it will come soon. All users from USA will have a different code - `455`, so UI can show the page, which will indicates, that platform is banned for USA residents and cant be reached from this country. +Meanwhile, in testing and business purpose you can whitelist one concrete or subset of IPs, for you investors, for lawyers or any demo, using `whitelist` rule. \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..889bcd2 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,32 @@ +# Barong + +## v2.4 + + * Integration of Postmaster for translated emails + * CSRF security support + * Improve KYC Profile flow + * Password strenght indicator with API + * Unification of configuration and documentation + * Public configuration endpoint to fetch non-sentive config from frontend + * Refactor recaptcha with geetest integration + +## v2.5 + + * Support of KyC provider (KyCAID)[https://kycaid.com/] + * Admin comments on users + * Double verification for user profile edit + +## v2.6 + + * Vault prefix and policies + * Configurable abilities + +## v2.7 + + * Fast barong authorizer + * Dynamic routing from database + * Configuration storage in vault + +## v3.0 + + * OAuth Sign-up and Login support diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..663722b --- /dev/null +++ b/docs/security.md @@ -0,0 +1,55 @@ +# Barong Security Hardening +This document describes the available security options implemented in the application and advices for configuring with highlighting the main (high-risk) security options + +### Password hashing +Barong since 2.0 version use OpenBSD bcrypt() password hashing algorithm, that allow us easily store a secure hash of users' passwords. + +As a base Barong takes [bcrypt-ruby gem](https://github.com/codahale/bcrypt-ruby) - Ruby binding for the OpenBSD bcrypt() With [rails 5 has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html) it gives us full power of algorithm. + +Read more about [password hashing algorithm in barong](https://www.openware.com/sdk/docs/barong/general/password-hashing.html). + +Read more about additional [password strength and regexp configuration](https://www.openware.com/sdk/docs/barong/configuration.html#password-configuration). + +### Challenge–response test (captcha protection) + +In order to prevent script attacks on API and possible brute force on session- and user- related endpoints we implement captcha protection. + +Configuration manages through environment variable - BARONG_CAPTCHA. Available values - geetest, recaptcha, none. With a wrong value barong will fail on start with error: `#{KEY}` invalid, enabled values: `NONE GEETEST RECAPTCHA`. + +!!NOTE: `NONE` is a default value, but its highly not recommended to use in `production` environment. + +List of endpoints protected can be configured in `barong.yml` file, common (and the most secure) list of endpoints is: +``` +captcha_protected_endpoints: + - user_create + - session_create + - password_reset + - email_confirmation + ``` + +Captcha will be verified on `SERVER` side. All requests without captcha will be denied with `error` `captcha_response.missing` + +More about captcha you can read here [captcha policy documentation](https://www.openware.com/sdk/docs/barong/general/captcha.html). + +### CSRF protection +`Cross-Site Request Forgery` (`CSRF`) is an attack that forces an end user to execute unwanted actions on a web application in which they’re currently authenticated. CSRF attacks specifically target state-changing requests, not theft of data since the attacker has no way to see the response to the forged request. +`CSRF` has become a huge deal in the recent years and it’s a part of `OWASP` top 10 common vulnerabilities + +There are some common practices to protect your website. + +First of all only `POST, PUT, PATCH, DELETE` and `TRACE` HTML requests have to be protected, since only these methods are destructive and can cause any unwanted or unauthorized damage. +Therefore, every time we sent such a request we need to append a specific token to it, to verify that request is sent from a legit HTML form. The token has to be included in `X-CSRF-Token` (commonly used one). + +This is the flow we went for: +1. On session creation backend sends a unique crypto-function generated token +2. Frontend stores that token in the DOM, typically it’s in the meta tag. +3. Every time a destructive request is sent to the backend, this token is appended in the header +4. Backend validates the header and performs the action. Error is returned if the header is invalid. +5. When the session is destroyed, the token is also destroyed and can’t be used again. + +This approach is called a per-session token method. + +For further information check the links down below: +* https://www.owasp.org/index.php/Main_Page +* https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#javascript-guidance-for-auto-inclusion-of-csrf-tokens-as-an-ajax-request-header +* https://www.owasp.org/index.php/Category:OWASP_Top_Ten_Project diff --git a/docs/service_accounts.md b/docs/service_accounts.md new file mode 100644 index 0000000..5652d28 --- /dev/null +++ b/docs/service_accounts.md @@ -0,0 +1,13 @@ +# Service Accounts +In Barong a service account belongs to a user. + +Service accounts are currently created only via Management API (server to server API). +There is no possibility to login into service account, it's used through API using an API Key. + +User has an ability to list his service accounts. Also User can create, update, delete and list API Keys for these service accounts using his own OTP. + +A user service account has the same level as the user, the role can be different. + +If User disable OTP, all API Keys for his service accounts will become inactive. + +If a user state changes, his service accounts state will change accordingly. diff --git a/docs/tasks/import.md b/docs/tasks/import.md new file mode 100644 index 0000000..3950dd3 --- /dev/null +++ b/docs/tasks/import.md @@ -0,0 +1,23 @@ +## How to import users and referrals to Barong database + +1. Create `csv` file for users and referrals with template. + +### Users table + + | uid | email | level | role | state | referral_uid | + |---------------|-----------------|-------|--------------|---------|---------------| + | ID1000003837 | admin@barong.io | 3 | superadmin | active | ID1000003828 | + + uid, email - require params + +2. For import users + +```ruby + bundle exec rake import:users['file_name.csv'] +``` + +3. For import referrals + +```ruby + bundle exec rake import:referrals['file_name.csv'] +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..d41ffed --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,24 @@ +# Troubleshooting +## Permissions +- `401 authz.invalid_permission` on specific endpoint +1. Check list of your permissions on barong seeds.yml or tower admin panel +2. If there are no such permission just add new permission on tower admin panel + +- `401 authz.invalid_permission` after login +1. Check your permissions on barong rails console +```ruby +irb(main)013:0> Permission.all +irb(main)013:0> Rails.cache.read('permissions') +``` +2. If there are no permissions on rails cache or these permissions are wrong you need to run following command +When you delete permissions from rails cache they will be automatically fetched +```ruby +irb(main)013:0> Rails.cache.delete('permissions') +``` +3. If there are no permissions on DB you need to seed permissions +``` +bundle exec rake db:seed +``` +*** +## Restictions +*Be careful with testing restriction and don't ban your local IP!* \ No newline at end of file diff --git a/docs/vault.md b/docs/vault.md new file mode 100644 index 0000000..726bd1b --- /dev/null +++ b/docs/vault.md @@ -0,0 +1,135 @@ +# Vault configuration + +## Introduction + +This document describes how to create vault tokens in order to restrict components access to vault as following + +| Component | Abilities | +| ------------ | ------------------------------------------------------- | +| barong-rails | encrypt api keys
create TOTP
verify TOTP code | +| barong-authz | decrypt api keys | + + + +## Connect to vault +The the following variables in your environment with correct values: + +```bash +export VAULT_ADDR='http://127.0.0.1:8200' +export VAULT_TOKEN='s.ozytsgX1BcTQaR5Y07SAd2VE' +``` + +You can test that it works running the following command: + +``` +$ vault status +Type: shamir +Sealed: false +Key Shares: 1 +Key Threshold: 1 +Unseal Progress: 0 +Unseal Nonce: +Version: 1.3.4 +Cluster Name: vault-cluster-650930cf +Cluster ID: 9f40327d-ec71-9655-b728-7588ce47d0b4 + +High-Availability Enabled: false +``` + +## Create ACL groups + +### Create the following policy files + +**barong-rails.hcl** + +Replace *opendax* with your vault application name. See [barong vault configuration](https://www.openware.com/sdk/docs/barong/configuration.html#vault-configuration) for more details. + +```bash +# Access system health status +path "sys/health" { + capabilities = ["read", "list"] +} + +# Manage the transit secrets engine +path "transit/keys/*" { + capabilities = [ "create", "read", "list" ] +} + +# Encrypt engines secrets +path "transit/encrypt/opendax_apikeys_*" { + capabilities = [ "create", "read", "update" ] +} + +# Renew tokens +path "auth/token/renew" { + capabilities = [ "update" ] +} + +# Lookup tokens +path "auth/token/lookup" { + capabilities = [ "update" ] +} + +# Manage otp keys +path "totp/keys/opendax_*" { + capabilities = ["create", "read", "update", "delete"] +} + +# Verify an otp code +path "totp/code/opendax_*" { + capabilities = ["update"] +} +``` + +**barong-authz.hcl** + +```bash +# Access system health status +path "sys/health" { + capabilities = ["read", "list"] +} + +# Manage the transit secrets engine +path "transit/keys/*" { + capabilities = [ "create", "read", "list" ] +} + +# Decrypt engines secrets +path "transit/decrypt/opendax_apikeys_*" { + capabilities = [ "create", "read", "update" ] +} + +# Renew tokens +path "auth/token/renew" { + capabilities = [ "update" ] +} + +# Lookup tokens +path "auth/token/lookup" { + capabilities = [ "update" ] +} +``` + +### Create the ACL groups in vault + +```bash +vault policy write barong-rails barong-rails.hcl +vault policy write barong-authz barong-authz.hcl +``` + +### Create applications tokens + +```bash +vault token create -policy=barong-rails -period=240h +vault token create -policy=barong-authz -period=240h +``` + +## Configure Barong + +Set those variables according to your deployment: + +```bash +export BARONG_VAULT_ADDRESS=http://127.0.0.1:8200 +export BARONG_VAULT_TOKEN=s.jyH1vmrOmkZ0FZZ0NZtgRenS +export BARONG_VAULT_APP_NAME=opendax +``` diff --git a/docs/zagros/2fa/2fa.md b/docs/zagros/2fa/2fa.md new file mode 100644 index 0000000..3fd2b1e --- /dev/null +++ b/docs/zagros/2fa/2fa.md @@ -0,0 +1,104 @@ +#Registration: 2FA +###### Sprint: 2 + +### Outcome: +2 factor authentication is a feature that helps users to secure their activities. +For critical activities the app needs a code to be completed. +At first vault creates a code (that showed with QR) to sync with google authenticator. +After adding code in the user's google authenticator, every time a user needs OTP(one-time-password) can read it in his app. + +### Implementation description: + +#### Endpoints: +GET {$domain}/api/v2/barong/resource/otp/generate_qrcode + +POST {$domain}/api/v2/barong/resource/otp/enable + +POST {$domain}/api/v2/barong/resource/otp/enable_2fa + +POST {$domain}/api/v2/barong/resource/otp/disable + +POST {$domain}/api/v2/barong/resource/otp/disable_email + +#### File destination: +{$Dalan_Path}/app/api/v2/resource/otp.rb + +#### Commits: +bd7f980596 +dc95b308a6 +b3b5a08d60 +3c170120c1 +a79e06a71b +8c821f1ba4 +e04a58b012 +99c2552404 +63f69ed0d6 +7010449976 +4d55a48572 +2da71b1097 + +#### What did we implement: + +We add an authorization step to enable/disable 2FA. After entering the google authenticator code, an authorization email is sent to the user containing an OTP . Users only can enable or disable 2FA after entering the correct OTP from the email. + +New flow: + +System generates the code → User scan it with mobile → enters the google code → Receives the email →The user enters the code → 2FA enable/disable + + +```mermaid +sequenceDiagram + title: 2fa activation + User->>Ranj: generate Qr Code + Ranj-->>Dalan: get /generate_qrcode + Dalan->>Ranj: 400, message: '2FA has been enabled for this account' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 400, message: '2FA has been already sent email for this account(enabling process started)' + Dalan->>Ranj: 200, QR params + Ranj->>User: QR + User->>Ranj: User scan and enter code + Ranj-->>Dalan: post /enable params:{code:string} + Dalan->>Ranj: 400, message: '2FA has been enabled for this account or code is missing' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'OTP code is invalid' + Dalan->>Ranj: 422, message: '2FA has been already sent email for this account(enabling process started)' + Ranj->>User: show result + Dalan->>User: send email in case of success + User->>Ranj: enter authorization code + Ranj-->>Dalan: post '/enable_2fa' params:{code:string} + Dalan->>Ranj: 400, message: '2FA has been enabled for this account or code is missing + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 400. message: '2FA has been already enabled for this account' + Dalan->>Ranj: 400, message: '2FA hasnt been enable'(didnt start enabling process) + Dalan->>Ranj: 422, message: 'OTP code is invalid' + Dalan->>Ranj: 200 + Ranj->>User: success +``` + +```mermaid +sequenceDiagram + title: 2fa deactivation + User->>Ranj: enter google authentication code + Ranj-->>Dalan: post /disable params:{code:string} + Dalan->>Ranj: 400, message: '2FA has not been enabled for this account or code is missing' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'OTP code is invalid' + Dalan->>Ranj: 422, message: '2FA has been already sent email for this account(disabling process started)' + Ranj->>User: show result + Dalan->>User: send email in case of success + User->>Ranj: enter authorization code + Ranj-->>Dalan: post '/disable_email' params:{code:string} + Dalan->>Ranj: 400, message: '2FA has not been enabled for this account or code is missing + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 400. message: '2FA has been already disabled for this account' + Dalan->>Ranj: 400, message: '2FA has been already sent email for this account'(didnt start enabling process) + Dalan->>Ranj: 422, message: 'OTP code is invalid' + Dalan->>Ranj: 200 + Ranj->>User: success +``` + + diff --git a/docs/zagros/Dalan-document.pdf b/docs/zagros/Dalan-document.pdf new file mode 100644 index 0000000..e80a2b2 Binary files /dev/null and b/docs/zagros/Dalan-document.pdf differ diff --git a/docs/zagros/KYC-steps/level-one/access-mobile.md b/docs/zagros/KYC-steps/level-one/access-mobile.md new file mode 100644 index 0000000..d657a8a --- /dev/null +++ b/docs/zagros/KYC-steps/level-one/access-mobile.md @@ -0,0 +1,95 @@ +# KYC: Access mobile + +###### Sprint: 3 + +### Outcome: + +User should enter his number, after the validation app sends a sms with an otp code, the user must enter the code to prove to us that he has access to the entered number. + + +#### Endpoints: + +POST {$domain}/api/v2/barong/resource/mobiles + +POST {$domain}/api/v2/barong/resource/mobiles/send_code + +POST {$domain}/api/v2/barong/resource/mobiles/verify + +#### File destination: + +{$Dalan_Path}/app/api/v2/resource/phones.rb + +#### Commits: + +bd7f980596 +695b0b56e2 +6dd40a9c42 +e04a58b012 +36167eda10 +2089690fe0 +90cbc48649 +17a5bce849 +63722fe22a +63977c330d +bff2746686 +75490dcb22 +f13ee3f4d5 +2c05522f77 +46c053b435 +551e7cdba2 +77d7b9a9b3 +2da71b1097 + +#### What did we implement: + +First of all we added a new service for our sms&call third party service(kavenegar).it needs its own initializer for private keys.in this service we also handle caching data(for storing keys and codes with expiration time) and creating OTP code by vault. + +We created a new api for phones with our rules. +after verification every number, app create a new label(access_phone) if there were not that label. +note: + +if user wants to enter new number: + +1.if user has pending number --> legacy number will be update with new number + +2.If user has verified access label --> new number must get access label to create in db and legacy one replaced + +3.If user has verified ownership label --> new number must get both labels to create in db and legacy one replaced + +```mermaid +sequenceDiagram + title: access phone + User->>Ranj: enter phone number with +98 + Ranj-->>Dalan: post /mobiles params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'code_doesnt_expired'(there is an active process for addding new mobile) + Dalan->>Ranj: 200, message: 'New phone was added' + note left of Dalan: if there is a pending number it will be replaced with new one + Dalan->>KaveNegar: send sms to User in case of success + KaveNegar->>User: SMS with code + Ranj->>User: show result + note right of User: if didnt catch sms + + User->>Ranj: resend code + Ranj->>Dalan: Post mobiles/send_code params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found', 'phone.doesnt_exist' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'code_doesnt_expired' (expiration time doesnt finish) + Dalan->>KaveNegar: send sms to User in case of success + KaveNegar->>User: SMS with code + Ranj->>User: show result + User->>Ranj:enter code + Ranj->>Dalan: Post mobiles/verify params + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found', 'phone.doesnt_exist' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 400, message: phone.verification_invalid + note right of Ranj: if user wants to replace a new verified phone and the legacy phone has owner_phone label, we should check ownership. if it doesnt legacy phone labels all changes will be rollback. + Dalan->>Ranj: success 2xx + Ranj->>User: show result +``` diff --git a/docs/zagros/KYC-steps/level-one/bank-information.md b/docs/zagros/KYC-steps/level-one/bank-information.md new file mode 100644 index 0000000..16fc268 --- /dev/null +++ b/docs/zagros/KYC-steps/level-one/bank-information.md @@ -0,0 +1,45 @@ +# KYC: Bank information + +### Outcome: +we checking ownership of card-numbers and ibans. + +### Implementation description: + +#### Endpoints: +Post {$domain}/api/v2/barong/resource/profiles/treasury + +#### File destination: +- {$Dalan_Path}/app/api/v2/resource/profiles.rb +- {$Dalan_Path}/app/workers/kyc/local/treasury_worker.rb +- {$Dalan_Path}/app/services/jibit_service.rb + + +#### Commits: +- 40177a4f +- 816fb00e +- 9e17677b +- 63977c33 +- c76f72fe +- 90cbc486 + +#### What did we implement: +We implemented a new model and table to keep users bank information and called it treasury.
+After users entered their card_number or IBAN, we are sending this information to the Jibit service,
+and then the Jibit Service returns the first name and last name of the real owner.
+Now we can check this returned information to users' profile information to find out everything is ok.
+It is worth mentioning that the steps of getting information from the Jibit and checking it with the profile are done in the background. + +```mermaid +sequenceDiagram + + Title: bank information (treasury) + note over User,Dalan: Users enter their bank information + note over User,Dalan: `data` is card-number or iban value, `kind` is 'card' or 'iban' + User->>Dalan:data, kind, title + + note over Dalan,Jibit: Dalan send request to Jibbit service (as worker) + Dalan->>Jibit:GET: iban value or card value (data) + Jibit->>Dalan:Response: first-name and last-name of real owner + Dalan->>Dalan: checking returned information with users profile data + Dalan->>User: add a confirmed label if the names matched, otherwise rejected label was created. +``` diff --git a/docs/zagros/KYC-steps/level-one/personal-info.md b/docs/zagros/KYC-steps/level-one/personal-info.md new file mode 100644 index 0000000..0aae044 --- /dev/null +++ b/docs/zagros/KYC-steps/level-one/personal-info.md @@ -0,0 +1,38 @@ +# KYC: Personal info +###### Sprint: 3 + +### Outcome: +Users add the below information to their profile + +### Implementation description: + +#### Endpoints: +POST {$domain}/api/v2/barong/resource/profiles + + +#### File destination: +{$Dalan_Path}/app/api/v2/resource/profiles.rb + +#### Commits: +9f1cde76 +2422f5d1 + +#### What did we implement: +In the Opendax, users can create their profiles by personal info and Address info in one step, also in this step there is no way to upload identity card + +So In Zagros, we separate the step of adding personal info from adding address info step, and also we add upload identity card features + + +```mermaid +sequenceDiagram + +Title: KYC - Profile Step +note over User,Ranj:**upload** is a picture of front identity card +User->>Ranj:first_name, last_name, dob, national_code, upload +Ranj->>Dalan:Post: after client side checking +Dalan->>Ranj:4xx if any required params are empty +Dalan->>Ranj:4xx if Profile model validity fails +Dalan->>Ranj:4xx if Document model validity fails +Dalan->>Ranj:2xx the profile create successfully +Ranj->>User:notify user that profile created +``` diff --git a/docs/zagros/KYC-steps/level-two/landline.md b/docs/zagros/KYC-steps/level-two/landline.md new file mode 100644 index 0000000..dd6a917 --- /dev/null +++ b/docs/zagros/KYC-steps/level-two/landline.md @@ -0,0 +1,91 @@ +# KYC: landline + +###### Sprint: 3 + +### Outcome: + +User should enter his number, after the validation app sends a call with an otp code, the user must enter the code to prove to us that he has access to the entered number. + +#### Endpoints: + +POST {$domain}/api/v2/barong/resource/phones + +POST {$domain}/api/v2/barong/resource/phones/send_code + +POST {$domain}/api/v2/barong/resource/phones/verify + +#### File destination: + +{$Dalan_Path}/app/api/v2/resource/phones.rb + +#### Commits: + +bd7f980596 +695b0b56e2 +6dd40a9c42 +e04a58b012 +36167eda10 +2089690fe0 +90cbc48649 +17a5bce849 +63722fe22a +63977c330d +bff2746686 +75490dcb22 +f13ee3f4d5 +2c05522f77 +46c053b435 +551e7cdba2 +77d7b9a9b3 +2da71b1097 + + +#### What did we implement: + +in access mobile told about KaveNegarService +user entered landline number then system send call by kave negar with otp code +user must enter send code to prove us that has access. + +if user wants to enter new number: + +1.if user have verified phone: response error + +2.if user has pending number: update that number with new one + +3.if user doesnt have number in our system: create new one + +```mermaid +sequenceDiagram + title: access landline + User->>Ranj: enter landline number with +98 + Ranj-->>Dalan: post /phones params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'code_doesnt_expired'(there is an active process for addding new landline) and phone.exists(has verified number in system) + Dalan->>Ranj: 200, message: 'New phone was added' + note left of Dalan: if there is a pending number it will be replaced with new one + Dalan->>KaveNegar: send call to User in case of success + KaveNegar->>User: call with code + Ranj->>User: show result + note right of User: if didnt catch call + + User->>Ranj: resend code + Ranj->>Dalan: Post phones/send_code params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found', 'phone.doesnt_exist' + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 422, message: 'code_doesnt_expired' (expiration time doesnt finish) + Dalan->>KaveNegar: send call to User in case of success + KaveNegar->>User: call with code + Ranj->>User: show result + User->>Ranj:enter code + Ranj->>Dalan: Post phones/verify params + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'Record is not found', 'phone.doesnt_exist', 'verification_invalid + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: success 2xx + Ranj->>User: show result +``` diff --git a/docs/zagros/KYC-steps/level-two/mobile-ownership.md b/docs/zagros/KYC-steps/level-two/mobile-ownership.md new file mode 100644 index 0000000..157958c --- /dev/null +++ b/docs/zagros/KYC-steps/level-two/mobile-ownership.md @@ -0,0 +1,42 @@ +# KYC: mobile ownership + +### Outcome: +we checking ownership of mobile. + +### Implementation description: + +#### Endpoints: +PUT {$domain}/api/v2/barong/admin/profiles/label + +#### File destination: +- {$Dalan_Path}/app/workers/kyc/local/owner_mobile_worker.rb +- {$Dalan_Path}/app/models/phone.rb +- {$Dalan_Path}/app/services/kyc_service.rb +- {$Dalan_Path}/app/api/v2/admin/profiles.rb +- {$Dalan_Path}/app/services/jibit_service.rb + +#### Commits: +- 7f06a2e3 +- 816fb00e +- 2c05522f +- 90cbc486 +- 91e75604 +- bdfc578d + +#### What did we implement: +Ownership of mobile is done in background (worker) after last step (selfie confirmation).
+The national code of verified profile with phone number , is sent to Jibit Service for matching.
+The label key in the `labels` table is **owner_phone** + +```mermaid +sequenceDiagram + + Title: mobile ownership + note over Admin,Dalan: admin user confirm users selfie + Admin->>PUT Dalan:user_uid, label_key, label_value + + note over Dalan,Jibit: Dalan send request to Jibbit service (as worker) + Dalan->>Jibit:GET: natonal_code, phone_number + Jibit->>Dalan:Response: matched, false or true + Dalan->>Dalan: create ownership label with reject label or verified label +``` diff --git a/docs/zagros/KYC-steps/level-two/residence.md b/docs/zagros/KYC-steps/level-two/residence.md new file mode 100644 index 0000000..cf6f5ca --- /dev/null +++ b/docs/zagros/KYC-steps/level-two/residence.md @@ -0,0 +1,72 @@ +# KYC: Residence + +###### Sprint: ? + +### Outcome: + +User can add address with complete information(city and province and etc..) + + +#### Endpoints: + +POST {$domain}/api/v2/barong/resource/profiles/address + +#### File destination: + +{$Dalan_Path}/app/api/v2/resource/profiles.rb + +#### Commits: + +f7820d8 +3e0fff4 +67c41b3 +f420fa4 +bff2746 +63977c3 + +#### What did we implement: + +Opendax just implements address as a string but we need more information like city and province and document for residency. + +So first of all we implemented city and province as below steps: + +##### 1.create migrations: + +20210420111532_create_provinces.rb → have just a name + +20210420111629_create_cities.rb → have name and must connect to province(foreign key) references :province, foreign_key: true + + + +##### 2.create models + +Province model: + +* Create validation for name: only allows letters, digits "-", "\'", and space. length: 1..255 + +* Mention to rails that this model has many cities + +City model: +* Mention to rails that this model belongs to province and this column is required. +* Create validation for name: only allows letters, digits "-", "\'", and space. length: 1..255 + +we need seeds to fill city and province tables. So we used a JSON file from git and create a seed. data of the seed was put in config/seeds.yml file and the functions are in lib/barong/seed.rb.(seed_provinces and seed_cities) they check the existence of the data(city or province) then create it in db. + +After these steps, we were ready to create a new API. we check the existence of the city and province that the user has been sent and create poa label. if user submitted another doc for poa and it has not been accepted, the new document will be replaced. + +```mermaid +sequenceDiagram + title: residence + User->>Ranj: residence information + Ranj-->>Dalan: post /profiles/address params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token' + Dalan->>Ranj: 404, message: 'needed data doesnt exist' + Dalan->>Ranj: 422, message: 'Validation errors',(address.already_exist,province_doesnt_exist) + Dalan->>Ranj: 422, message: 'code_doesnt_expired'(there is an active process for addding new mobile) + Dalan->>Ranj: 200, message: 'New phone was added' + note left of Dalan: if there is a pending address it will be replaced with new one +``` + + + diff --git a/docs/zagros/KYC-steps/level-two/selfie.md b/docs/zagros/KYC-steps/level-two/selfie.md new file mode 100644 index 0000000..aee4f2a --- /dev/null +++ b/docs/zagros/KYC-steps/level-two/selfie.md @@ -0,0 +1,38 @@ +# KYC: Selfie +###### Sprint: 3 + +### Outcome: +Users can upload image for certification (the image includes selfie and signed text of the agreement ) + + +### Implementation description: + +#### Endpoints: +POST{$domain}/api/v2/barong/resource/profiles/selfie + +#### File destination: +{$Dalan_Path}/app/api/v2/resource/profiles.rb + +#### Commits: +ac083d21 +ea79c211 + +#### What did we implement: +Users can upload images for the Selfie KYC step. In the first place that image is in a `pending` state. +In the Admin panel, this image must be change state from `pending` to `confirm` or `reject` + + + +```mermaid +sequenceDiagram + + Title: KYC - Selfie + + note over User,Ranj: **upload** must be valid format of image (jpg, jpeg, png) + User->>Ranj: upload + Ranj->>Dalan:Post: after client side checking + Dalan->>Ranj:4xx if does not exist verified profile for the current user + Dalan->>Ranj:4xx if before exist verified selfie image for the current user + Dalan->>Ranj:2xx update (if exist unconfirmed ) or create new selfie + Ranj->>User:notify user that password changed +``` diff --git a/docs/zagros/KYC-steps/leveling.md b/docs/zagros/KYC-steps/leveling.md new file mode 100644 index 0000000..11060ad --- /dev/null +++ b/docs/zagros/KYC-steps/leveling.md @@ -0,0 +1,78 @@ +# KYC level logic + +###### Sprint: 3 + +### Outcome: +Users can upgrade their level by doing KYC processes + + +### Implementation description: + + +#### File destination: +{$Dalan_Path}/app/models/user.rb
+{$Dalan_Path}/app/models/document.rb
+{$Dalan_Path}/app/models/label.rb
+{$Dalan_Path}/app/models/level.rb
+{$Dalan_Path}/app/models/treasury.rb
+{$Dalan_Path}/app/models/profile.rb
+{$Dalan_Path}/app/services/kyc_service.rb + + +#### Commits: +2422f5d1 +40177a4f +ac083d21 +7f06a2e3 +816fb00e +2c05522f +9e17677b +bff27466 +63977c33 +c76f72fe +90cbc486 +6dd40a9c +695b0b56 +b3b5a08d +bd7f9805 + +#### What did we implement: +in the Opendax, KYC level handled by labels that id of them (label) shows user level, +but not support feature that per-user level has own steps +(each label is database record as Label model) +obviously per step has its own label too and the label has a unique id itself. + +so by considering this rule in the Opendax, we kept it but change some things to support multi-steps.
+we implement some kind of giving-points system. +users can achieve a new level If the sum of their label IDs reaches a certain limit that defines in the code. + +as you know, each step is a kind of record database that controlling by model so:
+bank information step is `Treasury` model +selfie and address step is `Document` model (for uploading) +mobile and landline steps are `Telephone` model +profile is `Profile` model + +so if one of the above kind records, is updated or created, we will create or update the corresponding label recording, +and if the label is created or updated we will change the users level by calculating their points (points are the sum of label ids) + +also, we keep levels (steps for KYC) in the Level model with the below data that was entered by seed + + +levels: +- key: email, id: 1 ,value: verified, description: "User enter a valid code after registration" +- key: profile, id: 2, value: verified, description: "User personal documents have been verified" +- key: access_phone, id: 3, value: verified, description: "User entered a valid code from sms" +- key: card, id: 4, value: verified, description: "User card number have been verified" +- key: iban, id: 5, value: verified, description: "User iban have been verified" +- key: poa id: 6, value: verified, description: "User card bank have been verified" +- key: telephone id: 7, value: verified, description: "User entered a valid code from ring call" +- key: selfie id: 8, value: verified, description: "User selfie documents have been verified" +- key: owner_phone id: 9, value: verified description: "User owner mobile have been verified" +- key: vip id: 10, value: verified, description: "User Vip have been verified" + +### TODO +now if the label being confirmed accidental in the admin panel, they can not delete it,
+so must implement new codes to handle this stuff + + + diff --git a/docs/zagros/admin/KYC-confirm.md b/docs/zagros/admin/KYC-confirm.md new file mode 100644 index 0000000..ba59478 --- /dev/null +++ b/docs/zagros/admin/KYC-confirm.md @@ -0,0 +1,38 @@ +# KYC: admin +###### Sprint: ? + +### Outcome: +admin can verify or reject(change state(selfie,poa)) of documents. + + +### Implementation description: + +#### Endpoints: + +POST {$domain}/api/v2/barong/admin/users/documents + +#### File destination: +{$Dalan_Path}/app/api/v2/admin/users.rb + +#### Commits: +bd7f980 + +#### What did we implement: +for admin apis, we should check access of user(authorization) at first. +after checking authorization, we checked existence of document and profile for related user. +update state of document. +we wrote some callbacks for every doc type to update labels state . + + +```mermaid +sequenceDiagram + title: admin document update + User->>Ranj: user and new state of doc + Ranj-->>Dalan: post /admin/users/documents params + Dalan->>Ranj: 400, message: 'Required params are empty' + Dalan->>Ranj: 401, message: 'Invalid bearer token', 'admin.ability.not_permitted' + Dalan->>Ranj: 404, message: 'doesnt exist'(document.user_doesnt_exist,document.document_doesnt_exist) + Dalan->>Ranj: 422, message: 'Validation errors' + Dalan->>Ranj: 200, message: 'New phone was added' + Ranj->>User: show admin result +``` diff --git a/docs/zagros/admin/treasury-list.md b/docs/zagros/admin/treasury-list.md new file mode 100644 index 0000000..da3531b --- /dev/null +++ b/docs/zagros/admin/treasury-list.md @@ -0,0 +1,28 @@ +# Admin API: list treasuries + +### Outcome: +list (Array) of users treasuries + +### Implementation description: + +#### Endpoints: +GET {$domain}/api/v2/barong/admin/profiles/treasury/list + +#### File destination: +- {$Dalan_Path}/app/api/v2/admin/profiles.rb + +#### Commits: +- 0654aa1a04 + +#### What did we implement: +We implemented one API in the panel that admin users can see a list of users' treasuries by entering user_id.
+also, admin users can filter responses as IBAN or card treasuries by sending kind value. + +```mermaid +sequenceDiagram + + Title: list treasuries + note over Admin,Dalan: **kind** is optional + Admin->>Dalan:GET user_uid, kind + Dalan->>Admin:Array of treasuries [id, title, state, data, created_at, updated_at] +``` diff --git a/docs/zagros/change-password/change-password.md b/docs/zagros/change-password/change-password.md new file mode 100644 index 0000000..417e893 --- /dev/null +++ b/docs/zagros/change-password/change-password.md @@ -0,0 +1,63 @@ +# Registration: Change password +###### Sprint: 2 + +### Outcome: +Users can change login password by entering old and new password and confirm OTP code + + +### Implementation description: + +#### Endpoints: +PUT {$domain}/api/v2/barong/resource/users/password + +POST {$domain}/api/v2/barong/identity/confirm_password + +#### File destination: +{$Dalan_Path}/app/api/v2/resource/users.rb + +#### Commits: +fb5162e5 +8b16cc49 +fb9db4ee +70104499 +63f69ed0 +0ee97595 +9e17677b +5bb29d79 + +#### What did we implement: +In the Opendax, users can change their current password without confirming the OTP code that must be sent to its mail. Also, there is not any limitation for attempting to enter the current password. + +In Zagros, if users enter the wrong current password more than 3 times, they are kicked out from the logged state and the new password is just accepted after entering the valid OTP code. + +``` +TODO: +Number of failed attempts to enter the current password must be dynamic and +changeable by the admin panel +``` + +```mermaid +sequenceDiagram + + Title: Change password + + note over User,Ranj: old password parameter is the same as current password + User->>Ranj:old_password, new_password, confirm_password + Ranj->>Dalan:Put: after client side checking + Dalan->>Ranj:4xx if attempt number for entering the valid current password is over + Dalan->>Ranj:4xx if new_password is not same to confirm_password + Dalan->>Ranj:4xx if old_password is not valid + Dalan->>Ranj:4xx if new_password is same to old_password + Dalan->>Ranj:4xx if length of new password is not valid + Dalan->>User:2xx send otp to his or her mail + Ranj->>User:Ask OTP code for confirming + User-->>Ranj:if OTP code not received, try for resend after 120 seconds + Ranj-->>Dalan: Ask for sending OTP code again + Dalan-->>User:2xx send otp to his or her mail + User->>Ranj: enter OTP code + Dalan->>Ranj:4xx if user does not exist in the system + Ranj->>Dalan:Post : OTP code for checking ice + Dalan->>Ranj:4xx if OTP code expired + Dalan->>Ranj:2xx change user password + Ranj->>User:notify user that password changed +``` diff --git a/docs/zagros/reset-password/reset-password.md b/docs/zagros/reset-password/reset-password.md new file mode 100644 index 0000000..b0f290c --- /dev/null +++ b/docs/zagros/reset-password/reset-password.md @@ -0,0 +1,53 @@ +# Registration: Reset password +###### Sprint: 2 + +### Outcome: +Users can reset login password by entering email and confirm by OTP code + +### Implementation description: + +#### Endpoints: +POST {$domain}/api/v2/barong/identity/users/password/generate_code +POST {$domain}/api/v2/barong/identity/users/password/confirm_code +POST {$domain}/api/v2/barong/identity/users/password/reset + + +#### File destination: +{$Dalan_Path}/app/api/v2/identity/users.rb + +#### Commits: +9e17677b +42b34ae2 +e04a58b0 + +#### What did we implement: +In the Opendax, users get a reset token by entering their email, after in reset password page user can reset the password by entering a valid reset token and a new password + +In Zagros, the reset password process is done in 3 steps, the first step is to generate OTP code by Vault, then is a step to check the OTP code in by separate API, and then reset the password + +```mermaid +sequenceDiagram + + Title: Reset password + + User->>Ranj:email + Ranj->>Dalan:Post: after client side checking + Dalan->>Ranj:4xx if the user does not exist + Dalan->>User:2xx send otp to his or her mail + Ranj->>User:Ask OTP code for confirming + User-->>Ranj:if OTP code not received, try for resend after 120 seconds + Ranj-->>Dalan: Ask for sending OTP code again + Dalan-->>User:2xx send otp to his or her mail + User->>Ranj: enter OTP code + Ranj->>Dalan: OTP code for checking + Dalan->>Ranj:4xx if user does not exist in the system + Dalan->>Ranj:4xx if OTP code expired + Dalan->>Ranj:2xx OTP code is check\n + Ranj->>User: show reset password page + Ranj->>Dalan: email, password, confirm_password + Dalan->>Ranj:4xx if user does not exist in the system + Dalan->>Ranj:4xx if new_password is not same to confirm_password + Dalan->>Ranj:4xx if new password is not strong + Dalan->>Ranj:2xx reset user password + Ranj->>User:notify user that password changed +``` diff --git a/docs/zagros/sign-up/sign-up.md b/docs/zagros/sign-up/sign-up.md new file mode 100644 index 0000000..3c9d113 --- /dev/null +++ b/docs/zagros/sign-up/sign-up.md @@ -0,0 +1,56 @@ +# Registration: Sign-up +###### Sprint: 2 + +### Outcome: +Users can register via an email and a strong password, then we send OTP code to this email, the user must confirm the email by entering the OTP code. + + +### Implementation description: + +#### Endpoints: +POST: {$domain}/api/v2/barong/identity/users + +POST: {$domain}/api/v2/barong/identity/confirm_email + +#### File destination: +{$Dalan_Path}/app/api/v2/identity/users.rb + +#### Commits: +42602037 +Fb5162e5 +f57aaf35 +fb9db4ee +70104499 +63f69ed0 +99c25524 +0ee97595 + +#### What did we implement? +In the Opendax, users confirm their email by a unique link that is sent to their email, also in the registration process if one person uses an incorrect email, the email locks forever so the real owner of the email can not register ever. +In the Zagros, the email will lock when confirming it and this confirming happens when the user enters the Six digits OTP code in the website. +To send OTP code, we use Vault Service and adding action variable to distinguish every use of the Vault (for example to use it in following processes: Sign up, Change password, reset password and etc) also, email duplication in the registration process is allowed until the email confirmation or ban email in the system + +`Ps: action is a new variable that passes to ruby vault service.` + +```mermaid +sequenceDiagram + +Title: Sign up + +note over User,Ranj:starred parameters are optional +User->>Ranj:email, password, *refid* +Ranj->>Dalan:Post : after client side checking +Dalan->>Ranj:4xx if user status is active or ban +Dalan->>Ranj:4xx if email or password not valid +Dalan->>User:2xx send otp to his or her mail +Ranj->>User:Ask OTP code for confirming +User-->>Ranj:if OTP code not received, try for resend after 120 seconds +Ranj-->>Dalan:Ask for sending OTP code again +Dalan-->>User:2xx send otp to his or her mail +User->>Ranj: enter OTP code +Ranj->>Dalan:Post : OTP code for checking +Dalan->>Ranj:4xx if OTP code expired +Dalan->>Ranj:4xx if user not exist in system +Dalan->>Ranj:2xx create user, return user information +Ranj->>User:Redirect user to the main page +``` diff --git a/docs/zagros/uploader/uploader.md b/docs/zagros/uploader/uploader.md new file mode 100644 index 0000000..bb3112e --- /dev/null +++ b/docs/zagros/uploader/uploader.md @@ -0,0 +1,31 @@ +# Uploader + +The uploader **principle** defines that who is responsible for saving any document type in the system and how does it. + +the Opendax support these providers as uploader type +`local` `google` `aws` `alicloud` + +for `google`, `aws`, and `local` type, the UploadUploader class is a provider, +this class is a child from this gem : +[carrierwave](https://github.com/carrierwaveuploader/carrierwave) + +**path of file** : app/uploaders/upload_uploader.rb + +but for `alicloud` type, the provider is `AliUploader` class + +**path of file** : app/uploaders/ali_uploader.rb that this class in the child of carrierwave gem too. + + +in the `Dalan` version, we save files without any encryption +and use only `local` type for uploader and this config exist in alvand/config/app.yml + +the path of uploaded file is : + +```"public/uploads/#{model.class.to_s.underscore}/upload/#{model.id}" +example : public/uploads/document/upload/1/front_card.png +``` +###TODO +#### [Fog](https://fog.io/storage/) +in app/uploaders/upload_uploader.rb , we change the config code to hardcode to always use `local` type, +if you want to use fog type you must revert these lines: +line 4 to 12 diff --git a/geolite/COPYRIGHT.txt b/geolite/COPYRIGHT.txt new file mode 100644 index 0000000..90eb0ee --- /dev/null +++ b/geolite/COPYRIGHT.txt @@ -0,0 +1 @@ +Database and Contents Copyright (c) 2021 MaxMind, Inc. diff --git a/geolite/GeoLite2-Country.mmdb b/geolite/GeoLite2-Country.mmdb new file mode 100644 index 0000000..ce4db4a Binary files /dev/null and b/geolite/GeoLite2-Country.mmdb differ diff --git a/geolite/LICENSE.txt b/geolite/LICENSE.txt new file mode 100644 index 0000000..ee7434d --- /dev/null +++ b/geolite/LICENSE.txt @@ -0,0 +1,3 @@ +Use of this MaxMind product is governed by MaxMind's GeoLite2 End User License Agreement, which can be viewed at https://www.maxmind.com/en/geolite2/eula. + +This database incorporates GeoNames [https://www.geonames.org] geographical data, which is made available under the Creative Commons Attribution 4.0 License. To view a copy of this license, visit https://creativecommons.org/licenses/by/4.0/. diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lib/barong/activity_logger.rb b/lib/barong/activity_logger.rb new file mode 100644 index 0000000..efcb503 --- /dev/null +++ b/lib/barong/activity_logger.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Barong + # admin activities log writer class + class ActivityLogger + ACTION = { post: 'create', put: 'update', get: 'read', delete: 'delete', patch: 'update' }.freeze + + def self.async_write(options = {}) + @activities ||= Queue.new + @activities.push(options) + + @thread ||= Thread.new do + begin + loop do + msg = @activities.pop + params = format_params(msg) + Rails.logger.info("Recording activity for user id: #{params[:user_id]}, topic: #{params[:topic]}," \ + " action: #{params[:action]}, result: #{params[:result]}, data: #{params[:data]}") + Activity.create(params) + rescue StandardError => e + Rails.logger.error { "Failed to create activity with params: #{params}\n" \ + "Inspect error: #{e.inspect}\n#{e.backtrace.join("\n")}" } + + # If system catch Mysql2::Error::ConnectionError + # System will reconnect to DB and push message again to the activities queue + if e.is_a? (ActiveRecord::StatementInvalid) + ActiveRecord::Base.connection.reconnect! + sleep(0.1) + @activities.push(options) + end + end + end + end + end + + def self.sync_write(options = {}) + Activity.create(format_params(options)) + end + + def self.format_params(params) + topic = params[:topic].nil? && params[:path].split('admin/')[1].nil? ? 'general' : params[:topic] || params[:path].split('admin/')[1].split('/')[0] + { + user_id: params[:user_id], + target_uid: target_user(params[:payload]) || '', + user_ip: params[:user_ip], + user_agent: params[:user_agent], + topic: topic, + action: ACTION[params[:verb].downcase.to_sym] || 'system', + result: params[:result], + category: 'admin', + data: format_payload(params[:payload]) + } + end + + def self.format_payload(payload) + return unless payload + + return payload.to_json unless valid_json?(payload.keys.first) + + payload.keys.first + end + + def self.target_user(payload) + # in case payload is missing || empty POST body: payload => {"null" => nil } + return if payload.nil? || payload.keys.first == "null" + + if valid_json?(payload.keys.first) + payload = JSON.parse(payload.keys.first) + end + + payload[:uid] || payload[:user_uid] || payload['uid'] || payload['user_uid'] + end + + def self.valid_json?(json) + JSON.parse(json) + true + rescue JSON::ParserError => e + false + end + end +end diff --git a/lib/barong/amqp/config.rb b/lib/barong/amqp/config.rb new file mode 100644 index 0000000..4ec5c86 --- /dev/null +++ b/lib/barong/amqp/config.rb @@ -0,0 +1,61 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +module AMQP + class Config + class <= Barong::App.config.apikey_nonce_lifetime + # signature should be valid + error!({ errors: ['authz.invalid_signature'] }, 401) unless api_key.verify_hmac_payload? + + current_api_key = APIKey.find_by_kid(api_key_params[:kid]) + # corresponding Api Key should be active + error!({ errors: ['authz.apikey_not_active'] }, 401) unless current_api_key.active? + + # here User is either User object or ServiceAccount object + user = current_api_key.key_holder_account + validate_user!(user) + + validate_permissions!(user) + + user # returns user(api key creator) + rescue ActiveRecord::RecordNotFound + error!({ errors: ['authz.unexistent_apikey'] }, 401) + end + + def validate_csrf! + return unless Barong::App.config.csrf_protection && @request.env['REQUEST_METHOD'].in?(STATE_CHANGING_VERBS) + + unless headers['X-CSRF-Token'] + Rails.logger.info("CSRF attack warning! Missing token for uid: #{session[:uid]} in request to #{@path} by #{@request.env['REQUEST_METHOD']}") + error!({ errors: ['authz.missing_csrf_token'] }, 401) + end + + unless headers['X-CSRF-Token'] == session[:csrf_token] + Rails.logger.info("CSRF attack warning! Token is not valid for uid: #{session[:uid]} in request to #{@path} by #{@request.env['REQUEST_METHOD']}") + error!({ errors: ['authz.csrf_token_mismatch'] }, 401) + end + end + + def validate_permissions!(user) + # Caches Permission.all result to optimize + permissions = Rails.cache.fetch('permissions', expires_in: 5.minutes) { Permission.all.to_ary } + + permissions.select! { |a| a.role == user.role && ( a.verb == @request.env['REQUEST_METHOD'] || a.verb == 'ALL' ) && @path.starts_with?(a.path) } + actions = permissions.blank? ? [] : permissions.pluck(:action).uniq + + if permissions.blank? || actions.include?('DROP') || !actions.include?('ACCEPT') + log_activity(user.id, 'denied') if user.is_a?(User) + error!({ errors: ['authz.invalid_permission'] }, 401) + end + + if actions.include?('AUDIT') + topic = permissions.select { |a| a.action == 'AUDIT' }[0].topic + log_activity(user.id, 'succeed', topic) if user.is_a?(User) + end + end + + def log_activity(user_id, result, topic = nil) + if Rails.env.test? + ActivityLogger.sync_write(activity_params(user_id, result, topic)) + else + ActivityLogger.async_write(activity_params(user_id, result, topic)) + end + end + + def activity_params(user_id, result, topic) + { + user_id: user_id, + result: result, + user_agent: @request.env['HTTP_USER_AGENT'], + user_ip: remote_ip, + path: @path, + topic: topic, + verb: @request.env['REQUEST_METHOD'], + payload: @request.params + } + end + + # black/white list validation. takes ['block', 'pass'] as a parameter + def under_path_rules?(type) + return false if @rules[type].nil? # if no authz rules provided + + @rules[type].each do |t| + return true if @path.starts_with?(t) # if request path is inside the rules list + end + false # default + end + + def remote_ip + # default behaviour, IP from HTTP_X_FORWARDED_FOR + ip = @request.remote_ip + + if Barong::App.config.gateway == 'akamai' + # custom header that contains only client IP + true_client_ip = @request.env['HTTP_TRUE_CLIENT_IP'] + # take IP from TRUE_CLIENT_IP only if its not nil or empty + ip = true_client_ip unless true_client_ip.nil? || true_client_ip.empty? + end + + return ip + end + + private + + # encode helper method + def codec + @_codec ||= Barong::JWT.new(key: Barong::App.config.keystore.private_key) + end + + # fetch authz rules from yml + def lists + YAML.safe_load( + ERB.new( + File.read( + Barong::App.config.authz_rules_file + ) + ).result + ) + end + + # checks if api key headers are present in request + def api_key_headers? + return false if headers['X-Auth-Apikey'].nil? && + headers['X-Auth-Nonce'].nil? && + headers['X-Auth-Signature'].nil? + @api_key_headers = [headers['X-Auth-Apikey'], headers['X-Auth-Nonce'], headers['X-Auth-Signature']] + validate_headers? + end + + def validate_user!(user) + unless user.state.in?(%w[active pending]) + error!({ errors: ['authz.invalid_session'] }, 401) + end + + return if skip_api_key_2fa? + + if user.is_a?(User) && !user.otp + error!({ errors: ['authz.disabled_2fa'] }, 401) + end + end + + def skip_api_key_2fa? + ENV['BARONG_SKIP_API_KEY_2FA'] == 'true' + end + + # api key headers nil, blank validation + def validate_headers? + @api_key_headers.each do |k| + error!({ errors: ['authz.invalid_api_key_headers'] }, 422) if k.blank? + end + end + + # converts header into hash of parameters + def api_key_params + { + 'kid': headers['X-Auth-Apikey'], + 'nonce': headers['X-Auth-Nonce'], + 'signature': headers['X-Auth-Signature'] + } + end + + # custom error, calls AuthError class + def error!(text, code) + Rails.logger.debug "Error raised with code #{code} and error message #{text.to_json}" + raise AuthError.new(code), text.to_json + end + + def headers + @request.headers + end + + def session + @request.session + end + end +end diff --git a/lib/barong/cors/validations.rb b/lib/barong/cors/validations.rb new file mode 100644 index 0000000..64810dc --- /dev/null +++ b/lib/barong/cors/validations.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Provides CORS variables validation. +module Barong + module CORS + # Main CORS policy logic + module Validations + Error = Class.new(StandardError) + + class << self + def validate_origins(origins) + origins.split(',').each_with_object([]) do |origin, domains| + if origin == '*' + Rails.logger.info { "WARNING: API_CORS_ORIGIN is set to '*'" } + return '*' + elsif origin.match? %r{https?:\/\/([a-zA-Z0-9]+)(\.[a-zA-Z0-9]+)*(:^[0-9]*$+)?} + domains << origin + else + raise CORS::Validations::Error, "Set right origin domain name instead of #{origin}" + end + end + end + + def validate_max_age(max_age) + if max_age.present? && max_age.match?(/^[0-9]*$/) + max_age + else + Rails.logger.info { 'WARNING: Incorect or missing API_CORS_MAX_AGE value. Using default value: 3600' } + '3600' + end + end + end + end + end +end diff --git a/lib/barong/event_api.rb b/lib/barong/event_api.rb new file mode 100644 index 0000000..aed3261 --- /dev/null +++ b/lib/barong/event_api.rb @@ -0,0 +1,235 @@ +# frozen_string_literal: true + +require 'active_support/concern' +require 'active_support/lazy_load_hooks' + +# EventAPI provides interface to platform-wide notifications in RabbitMQ. +# +# Check docs/specs/event_api.md for more details. +module EventAPI + + MAPPING = { + 'sign-up' => 'system.user.email.confirmation.code', + 'reset-password' => 'system.user.password.reset.token', + 'enable-otp' => 'system.user.email.otp.enable', + 'disable-otp' => 'system.user.email.otp.disable', + 'change-password' => 'system.user.password.confirmation.code', + }.freeze + + class << self + def notify(event_name, event_payload) + event_name = MAPPING.dig(event_name).present? ? MAPPING.dig(event_name) : event_name + raise('Dalan mailer event name is unknown') unless event_name.present? + + arguments = [event_name, event_payload] + middlewares.each do |middleware| + returned_value = middleware.call(*arguments) + case returned_value + when Array then arguments = returned_value + else return returned_value + end + rescue StandardError => e + report_exception(e) + raise + end + end + + def middlewares=(list) + @middlewares = list + end + + def middlewares + @middlewares ||= [] + end + end + + module ActiveRecord + class Mediator + attr_reader :record + + def initialize(record) + @record = record + end + + def notify(partial_event_name, event_payload) + tokens = ['model'] + tokens << record.class.event_api_settings.fetch(:prefix) { record.class.name.underscore.gsub(/\//, '_') } + tokens << partial_event_name.to_s + full_event_name = tokens.join('.') + + ::EventAPI.notify(full_event_name, event_payload) + end + + def notify_record_created + notify(:created, record: record.as_json_for_event_api.compact) + end + + def notify_record_updated + return if record.previous_changes.blank? + + current_record = record + previous_record = record.dup + record.previous_changes.each { |attribute, values| previous_record.send("#{attribute}=", values.first) } + + # Guarantee timestamps. + previous_record.created_at ||= current_record.created_at + previous_record.updated_at ||= current_record.created_at + + after = current_record.as_json_for_event_api.compact + before = previous_record.as_json_for_event_api.compact.delete_if { |atr, val| after[atr] == val } + + notify :updated, \ + record: after, + changes: before.except(:updated_at) + end + end + + module Extension + extend ActiveSupport::Concern + + included do + # We add «after_commit» callbacks immediately after inclusion. + %i[create update].each do |event| + after_commit on: event, prepend: true do + if self.class.event_api_settings[:on]&.include?(event) + event_api.public_send("notify_record_#{event}d") + end + end + end + end + + module ClassMethods + def acts_as_eventable(settings = {}) + settings[:on] = %i[create update] unless settings.key?(:on) + @event_api_settings = event_api_settings.merge(settings) + end + + def event_api_settings + @event_api_settings || superclass.instance_variable_get(:@event_api_settings) || {} + end + end + + def event_api + @event_api ||= Mediator.new(self) + end + + def as_json_for_event_api + as_json + end + end + end + + # To continue processing by further middlewares return array with event name and payload. + # To stop processing event return any value which isn't an array. + module Middlewares + class << self + def application_name + Rails.application.class.name.split('::').first.underscore + end + + def application_version + "#{application_name.camelize}::VERSION".constantize + end + end + + class IncludeEventMetadata + def call(event_name, event_payload) + event_payload[:name] = event_name + [event_name, event_payload] + end + end + + class GenerateJWT + def call(event_name, event_payload) + jwt_payload = { + iss: Middlewares.application_name, + jti: SecureRandom.uuid, + iat: Time.now.to_i, + exp: (Time.now + 1.hour).to_i, + event: event_payload + } + + private_key = Barong::App.config.keystore.private_key + algorithm = 'RS256' + + jwt = JWT::Multisig.generate_jwt jwt_payload, \ + { Middlewares.application_name.to_sym => private_key }, + { Middlewares.application_name.to_sym => algorithm } + + [event_name, jwt] + rescue KeyError + raise 'No EVENT_API_JWT_PRIVATE_KEY found in env!' + end + end + + class PrintToScreen + def call(event_name, event_payload) + Rails.logger.debug do + ['', + 'Produced new event at ' + Time.current.to_s + ': ', + 'name = ' + event_name, + 'payload = ' + event_payload.to_json, + ''].join("\n") + end + [event_name, event_payload] + end + end + + class PublishToRabbitMQ + extend Memoist + + def call(event_name, event_payload) + Rails.logger.debug do + "\nPublishing #{routing_key(event_name)} (routing key) to #{exchange_name(event_name)} (exchange name).\n" + end + exchange = bunny_exchange(exchange_name(event_name)) + exchange.publish(event_payload.to_json, routing_key: routing_key(event_name)) + [event_name, event_payload] + end + + private + + def bunny_session + Bunny::Session.new(rabbitmq_credentials).tap do |session| + session.start + Kernel.at_exit { session.stop } + end + end + memoize :bunny_session + + def bunny_channel + bunny_session.channel + end + memoize :bunny_channel + + def bunny_exchange(name) + bunny_channel.direct(name) + end + memoize :bunny_exchange + + def rabbitmq_credentials + return ENV['EVENT_API_RABBITMQ_URL'] if ENV['EVENT_API_RABBITMQ_URL'].present? + + { + host: Barong::App.config.event_api_rabbitmq_host, + port: Barong::App.config.event_api_rabbitmq_port, + username: Barong::App.config.event_api_rabbitmq_username, + password: Barong::App.config.event_api_rabbitmq_password + } + end + + def exchange_name(event_name) + "#{Middlewares.application_name}.events.#{event_name.split('.').first}" + end + + def routing_key(event_name) + event_name.split('.').drop(1).join('.') + end + end + end + + middlewares << Middlewares::IncludeEventMetadata.new + middlewares << Middlewares::GenerateJWT.new + middlewares << Middlewares::PrintToScreen.new + middlewares << Middlewares::PublishToRabbitMQ.new +end diff --git a/lib/barong/geo_ip.rb b/lib/barong/geo_ip.rb new file mode 100644 index 0000000..751123f --- /dev/null +++ b/lib/barong/geo_ip.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Barong +# MaxmindDB reader adapter + module GeoIP + class << self + attr_accessor :lang + + # Usage: city = Barong::GeoIP.get(ip: ip, key: :city) + def info(ip:, key:) + record = reader.get(ip) + return unless record + + case key.to_sym + when :country + return record['country']['names'][lang] if record['country'] + when :continent + return record['continent']['names'][lang] if record['continent'] + end + end + + private + + def reader + @reader ||= MaxMind::DB.new(Barong::App.config.maxminddb_path, mode: MaxMind::DB::MODE_MEMORY) + end + end + end +end diff --git a/lib/barong/json_log_formatter.rb b/lib/barong/json_log_formatter.rb new file mode 100644 index 0000000..26987fd --- /dev/null +++ b/lib/barong/json_log_formatter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# json formatter for logs +class JSONLogFormatter < ::Logger::Formatter + def call(severity, time, _progname, msg) + JSON.dump(level: severity, time: time, message: msg) + "\n" + end +end diff --git a/lib/barong/jwt.rb b/lib/barong/jwt.rb new file mode 100644 index 0000000..ffefc01 --- /dev/null +++ b/lib/barong/jwt.rb @@ -0,0 +1,50 @@ +module Barong + class JWT + + def initialize(options) + raise "Missing private key" unless options[:key] + @options = options.reverse_merge({ + algoritm: 'RS256', + expire: Barong::App.config.jwt_expire_time, + sub: 'session', + iss: 'barong', + aud: %w[peatio barong] + }) + end + + def encode(payload) + ::JWT.encode(merge_claims(payload), + @options[:key], @options[:algoritm]) + end + + def decode_and_verify(token, verify_options) + @verify_options = verify_options.reverse_merge({ + verify_expiration: true, + verify_not_before: true, + iss: 'barong', + verify_iss: true, + verify_iat: true, + verify_jti: true, + aud: %w[peatio barong], + verify_aud: true, + sub: 'confirmation', + verify_sub: true, + algorithms: 'RS256' + }) + payload, header = ::JWT.decode(token, @verify_options[:pub_key], true, @verify_options) + payload.keys.each { |k| payload[k.to_sym] = payload.delete(k) } + payload + end + + def merge_claims(payload) + payload.reverse_merge({ + iat: Time.now.to_i, + exp: (Time.now + @options[:expire]).to_i, + sub: @options[:sub], + iss: @options[:iss], + aud: @options[:aud], + jti: SecureRandom.hex(10) + }) + end + end +end diff --git a/lib/barong/keystore.rb b/lib/barong/keystore.rb new file mode 100644 index 0000000..0b60dc6 --- /dev/null +++ b/lib/barong/keystore.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Barong + class KeyStore + + class Fatal < StandardError; end + + def initialize(private_key) + OpenSSL::PKey.read(private_key).tap do |key| + @private_key = key + @public_key = key.public_key + end + end + + def public_key + @public_key + end + + def private_key + @private_key + end + + class << self + def open!(private_key_path) + pkeyio = File.open(private_key_path) + return OpenSSL::PKey.read(pkeyio).to_pem + rescue + raise Barong::KeyStore::Fatal + end + + def read!(private_key) + return OpenSSL::PKey.read(private_key).to_pem + rescue + raise Barong::KeyStore::Fatal + end + + def save!(key, path) + File.open(path, 'w+') { |file| file.write(key) } + rescue + raise Barong::KeyStore::Fatal + end + + def generate + OpenSSL::PKey::RSA.generate(2048) + end + end + + end +end diff --git a/lib/barong/middleware.rb b/lib/barong/middleware.rb new file mode 100644 index 0000000..7426026 --- /dev/null +++ b/lib/barong/middleware.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'barong/middleware/jwt_authenticator' + +module Barong + # Barong Rack middlewares + module Middleware; end +end diff --git a/lib/barong/middleware/jwt_authenticator.rb b/lib/barong/middleware/jwt_authenticator.rb new file mode 100644 index 0000000..cd339db --- /dev/null +++ b/lib/barong/middleware/jwt_authenticator.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Barong + module Middleware + # Authenticate a user by a bearer token + class JWTAuthenticator < Grape::Middleware::Base + def initialize(app, options) + super(app, options) + raise(Peatio::Auth::Error, 'Public key missing') unless options[:pubkey] + + @keypub = options[:pubkey] + end + + def before + return if request.path.include? 'swagger' + + raise(Peatio::Auth::Error, 'Header Authorization missing') \ + unless authorization_present? + + token = request.headers['Authorization'] + env[:current_payload] = authenticator.authenticate!(token) + end + + private + + # JWT Authenticator instance from peatio-core + # + # @return [Peatio::Auth::JWTAuthenticator] + def authenticator + @authenticator ||= + Peatio::Auth::JWTAuthenticator.new(@keypub) + end + + def authorization_present? + request.headers.key?('Authorization') + end + + # Request entity + # + # @return [Grape::Request] + def request + @request ||= Grape::Request.new(env) + end + end + end +end diff --git a/lib/barong/mock_sms.rb b/lib/barong/mock_sms.rb new file mode 100644 index 0000000..968f36c --- /dev/null +++ b/lib/barong/mock_sms.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Barong + # empty sms service + class MockSMS + cattr_accessor :messages + self.messages = [] + + def initialize(_account_sid, _auth_token) end + + def messages + self + end + + def create(params) + self.class.messages << OpenStruct.new(params) + end + end +end diff --git a/lib/barong/seed.rb b/lib/barong/seed.rb new file mode 100644 index 0000000..09d3d19 --- /dev/null +++ b/lib/barong/seed.rb @@ -0,0 +1,191 @@ +module Barong + class Seed + class ConfigError < RuntimeError; end + + def initialize + @result = [] + end + + def seeds + YAML.safe_load( + ERB.new( + File.read( + Barong::App.config.seeds_file + ) + ).result + ) + end + + def inspect + str = "Seeded users:\n" + str += @result.map do |user| + "Email: #{user[:email]}, password: #{user[:password]}" + end.join("\n") + return str + end + + def logger + @logger ||= Logger.new(STDERR, progname: "db:seed") + end + + def seed_provinces + logger.info "Seeding provinces" + seeds["provinces"].each_with_index do |province, index| + logger.info "---" + if Province.find_by(name: province["name"]).present? + logger.info "province '#{province['name']}' already exists" + next + end + province[:id] = index+1 + Province.create!(province) + end + end + + def seed_cities + logger.info "Seeding cities" + seeds["cities"].each_with_index do |city, index| + logger.info "---" + if City.find_by(name: city["name"]).present? + logger.info "province '#{city['name']}' already exists" + next + end + city[:id] = index+1 + City.create!(city) + end + end + + def seed_levels + logger.info "Seeding levels" + seeds["levels"].each_with_index do |level, index| + logger.info "---" + if Level.find_by(key: level["key"], value: level["value"]).present? + logger.info "Level '#{level['key']}:#{level['value']}' already exists" + next + end + level[:id] = index+1 + Level.create!(level) + end + end + + def seed_permissions + logger.info "Seeding permissions" + seeds["permissions"].each do |perm| + logger.info "---" + if Permission.find_by(role: perm["role"], verb: perm["verb"], path: perm["path"], action: perm["action"]).present? + logger.info "Permission for '#{perm['role']}' : '#{perm['verb']} to #{perm['path']}' already exists" + next + end + + permission = Permission.new(perm) + + unless permission.save + raise ConfigError.new("Can't create permission: #{permission.errors.full_messages.join('; ')}") + end + end + end + + def seed_users + logger.info "Seeding users" + seeds["users"]&.each do |seed| + logger.info "---" + + raise ConfigError.new("Email missing in users seed") if seed["email"].to_s.empty? + raise ConfigError.new("Level is missing for user #{seed["email"]}") unless seed["level"].is_a?(Integer) + + # Skip existing users + if User.find_by(email: seed["email"]).present? + logger.info "User '#{seed['email']}' already exists" + @result.push(email: seed["email"]) + next + end + + user = User.new(seed) + user.password ||= SecureRandom.base64(30) + + if user.save + logger.info "Created user for '#{user.email}'" + + # Set correct level with labels + # levels = Level.where(id: 1..user.level) + levels = Level.where(id: 1..Level::LEVEL_ID_BOUNDS[user.level.to_s]) + raise ConfigError.new("No enough levels found in database to grant the user to level #{user.level}") if levels.count < user.level + + levels.find_each do |level| + user.labels.create(key: level.key, value: level.value, scope: 'private') + end + + @result.push(email: user.email, password: user.password, level: user.level) + else + logger.error "Can't create user '#{user.email}': #{user.errors.full_messages.join('; ')}" + end + end + end + + def seed_superadmin + + logger.info "Seeding superadmin" + seeds["superadmin"]&.each do |seed| + logger.info "---" + raise ConfigError.new("Email missing in users seed") if seed["email"].to_s.empty? + raise ConfigError.new("Level is missing for user #{seed["email"]}") unless seed["level"].is_a?(Integer) + + # Skip existing users + if User.find_by(email: seed["email"]).present? + logger.info "User '#{seed['email']}' already exists" + @result.push(email: seed["email"]) + next + end + + user = User.new(seed) + user.password ||= SecureRandom.base64(30) + + if user.save + logger.info "Created user for '#{user.email}'" + + # Set correct level with labels + # levels = Level.where(id: 1..user.level) + levels = Level.where(id: 1..Level::LEVEL_ID_BOUNDS[user.level.to_s]) + raise ConfigError.new("No enough levels found in database to grant the user to level #{user.level}") if levels.count < user.level + + levels.find_each do |level| + user.labels.create(key: level.key, value: level.value, scope: 'private') + end + + @result.push(email: user.email, password: user.password, level: user.level) + else + logger.error "Can't create user '#{user.email}': #{user.errors.full_messages.join('; ')}" + end + end + end + + + def seed_restrictions + logger.info "Seeding restrictions" + return logger.info "Restrictions seed is empty!" if seeds["restrictions"].empty? + + seeds["restrictions"].each do |seed| + logger.info "---" + if Restriction.find_by(category: seed["category"], scope: seed["scope"], value: seed["value"], state: seed["state"]).present? + logger.info "Restriction '#{seed['category']}' '#{seed['scope']} #{seed['value']}' #{seed['state']}' already exists" + next + end + raise ConfigError.new("category missing in restrictions seed") if seed["category"].nil? + raise ConfigError.new("scope is missing in restrictions seed") if seed["scope"].nil? + raise ConfigError.new("value is missing in restrictions seed") if seed["value"].nil? + raise ConfigError.new("state is missing in restrictions seed") if seed["state"].nil? + + restriction = Restriction.new( + category: seed["category"], + scope: seed["scope"], + value: seed["value"], + state: seed["state"], + code: seed["code"] + ) + + unless restriction.save + raise ConfigError.new("Can't create restriction: #{restriction.errors.full_messages.join('; ')}") + end + end + end + end +end diff --git a/lib/barong/string.rb b/lib/barong/string.rb new file mode 100644 index 0000000..52f936f --- /dev/null +++ b/lib/barong/string.rb @@ -0,0 +1,49 @@ +class String + CHARACTER_MAPPING = { + 'ك' => 'ک', + 'دِ' => 'د', + 'بِ' => 'ب', + 'زِ' => 'ز', + 'ذِ' => 'ذ', + 'شِ' => 'ش', + 'سِ' => 'س', + 'ى' => 'ی', + 'ي' => 'ی', + '١' => '۱', + '٢' => '۲', + '٣' => '۳', + '٤' => '۴', + '٥' => '۵', + '٦' => '۶', + '٧' => '۷', + '٨' => '۸', + '٩' => '۹', + '٠' => '۰' + }.freeze + + def to_persian + gsub(Regexp.union(CHARACTER_MAPPING.keys), CHARACTER_MAPPING) + end + + def percent_match(mold) + mold = mold.to_s.to_persian + str = self.to_persian + char_cost = (100.to_f / length).round(2) + matching = 0 + str_index = 0 + mold_index = 0 + str.each_char.with_index do |_char, index| + break if mold[mold_index].blank? + + if mold[mold_index] != str[str_index] + mold_index = index - 1 if mold.length < str.length + str_index = index - 1 if mold.length > str.length + else + matching += char_cost + end + str_index += 1 + mold_index += 1 + end + matching.round + end +end diff --git a/lib/tasks/activities.rake b/lib/tasks/activities.rake new file mode 100644 index 0000000..4776c08 --- /dev/null +++ b/lib/tasks/activities.rake @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +namespace :activities do + + desc "Delete activities for specific period. From and To parameters should be on next format 'YYYY-mm-dd'" + task :delete, [:from, :to] => [:environment] do |_, args| + if args[:from].present? && args[:to].present? + + if valid_date?(args[:from]) && valid_date?(args[:to]) + activities = Activity.where('DATE(created_at) >= ? AND DATE(created_at) <= ?', args[:from].to_date, args[:to].to_date) + + puts "Found #{activities.count} activities to delete" + activities.delete_all + end + else + puts "There is no parameters (from, to)" + end + end + + def valid_date?(date) + Date.parse(date) + true + rescue ArgumentError + puts "Invalid date #{date}, should be 'YYYY-mm-dd''" + false + end +end diff --git a/lib/tasks/auto_annotate_models.rake b/lib/tasks/auto_annotate_models.rake new file mode 100644 index 0000000..cbc0505 --- /dev/null +++ b/lib/tasks/auto_annotate_models.rake @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# NOTE: only doing this in development as some production environments (Heroku) +# NOTE: are sensitive to local FS writes, and besides -- it's just not proper +# NOTE: to have a dev-mode tool do its thing in production. +if Rails.env.development? && defined?(Annotate) + task :set_annotation_options do + # You can override any of these by setting an environment variable of the + # same name. + Annotate.set_defaults \ + "routes" => "false", + "position_in_routes" => "before", + "position_in_class" => "after", + "position_in_test" => "before", + "position_in_fixture" => "before", + "position_in_factory" => "before", + "position_in_serializer" => "before", + "show_foreign_keys" => "true", + "show_complete_foreign_keys" => "true", + "show_indexes" => "true", + "simple_indexes" => "false", + "model_dir" => "app/models", + "root_dir" => "", + "include_version" => "true", + "require" => "", + "exclude_tests" => "true", + "exclude_fixtures" => "true", + "exclude_factories" => "true", + "exclude_serializers" => "true", + "exclude_scaffolds" => "true", + "exclude_controllers" => "true", + "exclude_helpers" => "true", + "exclude_sti_subclasses" => "false", + "ignore_model_sub_dir" => "false", + "ignore_columns" => nil, + "ignore_routes" => nil, + "ignore_unknown_models" => "false", + "hide_limit_column_types" => "", + "hide_default_column_types" => "", + "skip_on_db_migrate" => "false", + "format_bare" => "true", + "format_rdoc" => "false", + "format_markdown" => "false", + "sort" => "false", + "force" => "false", + "trace" => "false", + "wrapper_open" => nil, + "wrapper_close" => nil, + "with_comment" => true + end + + Annotate.load_tasks +end diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake new file mode 100644 index 0000000..d275a3e --- /dev/null +++ b/lib/tasks/docker.rake @@ -0,0 +1,8 @@ + +namespace 'docker' do + + desc "Build Application container" + task :build do + sh "docker build -t quay.io/openware/barong2:latest ." + end +end diff --git a/lib/tasks/export.rake b/lib/tasks/export.rake new file mode 100644 index 0000000..0f7d83a --- /dev/null +++ b/lib/tasks/export.rake @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'yaml' +require 'csv' + +namespace :export do + + desc 'Export all users to csv file.' + task users: :environment do + count = 0 + errors_count = 0 + begin + CSV.open('barong_users.csv', 'w') do |csv| + csv << %w[uid email level role state] + User.find_each do |user| + csv << [user.uid, user.email, user.level, user.role, user.state] + count += 1 + end + rescue StandardError => e + message = { error: e.message, email: user.email, uid: user.uid } + Rails.logger.error message + errors_count += 1 + end + end + Kernel.puts "Exported #{count} users" + Kernel.puts "Errored #{errors_count}" + end + +end diff --git a/lib/tasks/generate.rake b/lib/tasks/generate.rake new file mode 100644 index 0000000..25154bd --- /dev/null +++ b/lib/tasks/generate.rake @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# To execute with optional user and api keys amount +# rake generate:users -- --n=1000 + +namespace 'generate' do + desc 'Build Application container' + task :users => :environment do + ### Parse options + options = {} + + o = OptionParser.new + + o.banner = 'Usage: rake generate:users [-n=100]' + o.on('-n NUMBER', '--number NUMBER') { |num| options[:num] = num.to_i } + + args = o.order!(ARGV) {} + o.parse!(args) + + options[:num] = 10 if options[:num].nil? + result_arr = [] + + options[:num].times do |i| + email = Faker::Internet.email + passwd = Faker::Internet.password(min_length: 10, special_characters: true) + u = User.create(email: email, password: passwd, level: 3, state: 'active') + + api_key = APIKey.create(user_id: u.id, kid: SecureRandom.hex(8), algorithm: 'HS256') + secret = SecureRandom.hex(16) + SecretStorage.store_secret(secret, api_key.kid) + result_arr.push({ + 'uid' => u.uid, + 'email' => u.email, + 'password' => passwd, + 'kid' => api_key.kid, + 'secret' => secret + }) + end + + File.write('tmp/payload.yml', result_arr.to_yaml) + end +end diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake new file mode 100644 index 0000000..cc62f35 --- /dev/null +++ b/lib/tasks/import.rake @@ -0,0 +1,71 @@ + +#frozen_string_literal: true + +require 'csv' + +namespace :import do + # Detailed instruction https://github.com/rubykube/barong/blob/master/docs/tasks/import.md + # Required fields for import users: + # - uid + # - email + # + # Usage: + # For import users: -> bundle exec rake import:users['file_name.csv'] + + desc 'Load users from csv file.' + task :users, [:config_load_path] => [:environment] do |_, args| + csv_table = File.read(Rails.root.join(args[:config_load_path])) + import_user_log = File.open('./log/import_users.log', 'w') + count = 0 + CSV.parse(csv_table, :headers => true).map do |row| + row = row.to_h.compact.symbolize_keys! + defaults = { level: 0, role: 'member', state: 'pending', password: generate_password } + permitted_attr = %i[uid email role state] + User.new(row.slice(*permitted_attr) + .reverse_merge(defaults)) + .save! + count += 1 + rescue StandardError => e + message = { error: e.message, email: row[:email], uid: row[:uid] } + import_user_log.write(message.to_yaml + "\n") + end + import_user_log.close + Kernel.puts "Created #{count} members" + Kernel.puts "Errored #{CSV.parse(csv_table, headers: true).count - count}" + end + + # Required fields for import referrals: + # - uid + # - referral_uid + # + # Usage: + # For import users: -> bundle exec rake import:fill_referals['file_name.csv'] + + desc 'users referral relation from csv file' + task :fill_referrals, [:config_load_path] => [:environment] do |_, args| + csv_table = File.read(Rails.root.join(args[:config_load_path])) + import_affiliates_log = File.open('./log/import_referrals.log', 'w') + count = 0 + CSV.parse(csv_table, :headers => true).map do |row| + row = row.to_h.compact.symbolize_keys! + user = User.find_by_uid!(row[:uid]) + target_user = User.find_by_uid(row[:referral_uid]) + next unless target_user + + user.update!(referral_id: target_user.id) + count += 1 + rescue StandardError => e + message = { error: e.message, email: row[:email], uid: row[:uid], referral_uid: row[:referral_uid] } + import_affiliates_log.write(message.to_yaml + "\n") + end + import_affiliates_log.close + Kernel.puts "Created #{count} referrals" + end + + def generate_password + chars = ('a'..'z').to_a + numbers = ('0'..'9').to_a + special = %w[! @ # $ % & / ( ) + ? *] + chars.sort_by { rand }.join[0..3] + chars.sort_by { rand }.join[0..3].upcase + numbers.sort_by { rand }.join[0..3] + special.sort_by { rand }.join[0..3] + end +end diff --git a/lib/tasks/mailer.rake b/lib/tasks/mailer.rake new file mode 100644 index 0000000..3699753 --- /dev/null +++ b/lib/tasks/mailer.rake @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'ostruct' + +namespace 'mailer' do + desc 'Generate email to the user with UID' + task :send, [:uid] => [:environment] do |_t, args| + record = OpenStruct.new + record.user = User.find_by(uid: args[:uid]) + record.domain = 'barong.io' + record.token = 'TEST_TOKEN' + + params = { + subject: 'Test Message', + template_name: 'email_confirmation.en.html.erb', + record: record, + changes: nil, + user: User.first + } + + Postmaster.process_payload(params).deliver_now + end +end diff --git a/lib/tasks/migrate.rake b/lib/tasks/migrate.rake new file mode 100644 index 0000000..73fd954 --- /dev/null +++ b/lib/tasks/migrate.rake @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +namespace 'migrate' do + desc 'Migrate api keys from vault secrets to transit' + task "26-api-keys": [:environment] do + puts 'Migrating API keys from secrets to transit keys (new in 2.6)' + APIKey.find_in_batches do |keys| + keys.each do |key| + legacy_key_path = "secret/barong/api_key/#{key.kid}" + secret = TOTPService.read_data(legacy_key_path) + next unless secret + + value = secret.data[:value] + next unless value + + key.secret = value + key.save! + TOTPService.delete_data(legacy_key_path) + end + end + end + + desc 'Move TOTP secrets to include the vault application as prefix' + task "26-totp": [:environment] do + puts 'Moving TOTP secrets to include the vault application as prefix (new in 2.6)' + + User.find_in_batches do |users| + users.each do |u| + legacy_key_path = "totp/keys/#{u.uid}" + legacy_export_path = "totp/export/#{u.uid}" + + next unless TOTPService.read_data(legacy_key_path) + + params = TOTPService.read_data(legacy_export_path) + unless params + raise "Failed to export key (#{legacy_export_path})\n"\ + "Make sure vault is running with this image: quay.io/openware/vault:1.5.3-openware\n"\ + 'And make sure the token has read access to /totp/export/*' + end + key_path = TOTPService.totp_key(u.uid) + TOTPService.write_data(key_path, params.data.merge(generate: false)) + TOTPService.delete_data(legacy_key_path) + end + end + end +end diff --git a/lib/tasks/permissions.rake b/lib/tasks/permissions.rake new file mode 100644 index 0000000..46b6bfc --- /dev/null +++ b/lib/tasks/permissions.rake @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +namespace :permissions do + desc 'Reload user permissions' + task reload: :environment do + Permission.transaction do + # Delete old permissions + Permission.delete_all + # Clean permissions cache + Rails.cache.delete('permissions') + # Load permissions + Barong::Seed.new.seed_permissions + end + end +end diff --git a/lib/tasks/rotate.rake b/lib/tasks/rotate.rake new file mode 100644 index 0000000..ac6d5db --- /dev/null +++ b/lib/tasks/rotate.rake @@ -0,0 +1,24 @@ +#frozen_string_literal: true + +namespace :rotate do + desc 'Rotate profile keys' + task profiles: :environment do + Profile.find_each(batch_size: 100) do |profile| + profile.update({}) + end + end + + desc 'Rotate phones keys' + task phones: :environment do + Phone.find_each(batch_size: 100) do |phone| + phone.update({}) + end + end + + desc 'Rotate documents keys' + task documents: :environment do + Document.where.not(doc_number_encrypted: nil).find_each(batch_size: 100) do |document| + document.update({}) + end + end +end diff --git a/lib/tasks/superadmin.rake b/lib/tasks/superadmin.rake new file mode 100644 index 0000000..70703fb --- /dev/null +++ b/lib/tasks/superadmin.rake @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +namespace :superadmin do + desc "create superadmin" + task create: :environment do + Kernel.puts "Seeding superadmin" + Barong::Seed.new().seeds["superadmin"]&.each do |seed| + Kernel.puts "---" + raise ConfigError.new("Email missing in users seed") if seed["email"].to_s.empty? + raise ConfigError.new("Level is missing for user #{seed["email"]}") unless seed["level"].is_a?(Integer) + + # Skip existing users + if User.find_by(email: seed["email"], state: seed['state']).present? + Kernel.puts "User '#{seed['email']}' already exists" + next + elsif User.find_by(email: seed["email"]).present? + User.find_by(email: seed["email"]).update(state: seed['state']) + next + end + + user = User.new(seed) + user.password ||= SecureRandom.base64(30) + + if user.save + Kernel.puts "Created user for '#{user.email}'" + + # Set correct level with labels + # levels = Level.where(id: 1..user.level) + levels = Level.where(id: 1..Level::LEVEL_ID_BOUNDS[user.level.to_s]) + raise ConfigError.new("No enough levels found in database to grant the user to level #{user.level}") if levels.count < user.level + + levels.find_each do |level| + user.labels.create(key: level.key, value: level.value, scope: 'private') + end + + else + Kernel.puts "Can't create user '#{user.email}': #{user.errors.full_messages.join('; ')}" + end + end + + end + + task delete: :environment do + Kernel.puts "deleting superadmin" + Barong::Seed.new().seeds["superadmin"]&.each do |seed| + user = User.find_by(email: seed['email']) + user.update(state: 'inactive') + end + end + + +end diff --git a/log/.keep b/log/.keep new file mode 100644 index 0000000..e69de29 diff --git a/public/404.html b/public/404.html new file mode 100644 index 0000000..2be3af2 --- /dev/null +++ b/public/404.html @@ -0,0 +1,67 @@ + + + + The page you were looking for doesn't exist (404) + + + + + + +
+
+

The page you were looking for doesn't exist.

+

You may have mistyped the address or the page may have moved.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/422.html b/public/422.html new file mode 100644 index 0000000..c08eac0 --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
+
+

The change you wanted was rejected.

+

Maybe you tried to change something you didn't have access to.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/500.html b/public/500.html new file mode 100644 index 0000000..78a030a --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
+
+

We're sorry, but something went wrong.

+
+

If you are the application owner check the logs for more information.

+
+ + diff --git a/public/apple-touch-icon-precomposed.png b/public/apple-touch-icon-precomposed.png new file mode 100644 index 0000000..e69de29 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..e69de29 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000..09bff8e Binary files /dev/null and b/public/logo.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..37b576a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file diff --git a/scripts/benchmark/phones.rb b/scripts/benchmark/phones.rb new file mode 100644 index 0000000..eeb5438 --- /dev/null +++ b/scripts/benchmark/phones.rb @@ -0,0 +1,49 @@ +require 'benchmark' +require_relative "../../config/environment" + +# Check only Error logs +ActiveRecord::Base.logger.level = 3 + +## Before benchmark +user = User.find_or_create_by(uid: "UIDExample", email: 'test_example@gmail.com') do |u| + u.password = "PasswordExample12" +end + +# Benchmark +puts '---------------------------' +puts 'Phone creating' +puts '---------------------------' +Benchmark.bm do |x| + # Creates 1 phone + x.report(:p_1) { + phone = Phone.new(user_id: user.id, country: 'UA', number: '123123123') + phone.save(validate: false) + } + # Creates 1000 phones + x.report(:p_1000) { + 1_000.times do + phone = Phone.new(user_id: user.id, country: 'UA', number: '123123123') + phone.save(validate: false) + end + } +end + +puts '---------------------------' +puts 'Descrypts data from phone' +puts '---------------------------' +Benchmark.bm do |x| + # Descrypts 1 phone + x.report(:p_1) { + Phone.last.number + } + # Descrypts 1000 phones + x.report(:p_1000) { + Phone.where(user_id: user.id).limit(1000).each do |phone| + phone.number + end + } +end + +## After benchmark +Phone.where(user_id: user.id).delete_all +user.destroy diff --git a/scripts/benchmark/profiles.rb b/scripts/benchmark/profiles.rb new file mode 100644 index 0000000..cbad897 --- /dev/null +++ b/scripts/benchmark/profiles.rb @@ -0,0 +1,55 @@ +require 'benchmark' +require_relative "../../config/environment" + +# Check only Error logs +ActiveRecord::Base.logger.level = 3 + +## Before benchmark +user = User.find_or_create_by(uid: "UIDExample", email: 'test_example@gmail.com') do |u| + u.password = "PasswordExample12" +end + +# Benchmark +puts '---------------------------' +puts 'Profile creating' +puts '---------------------------' +Benchmark.bm do |x| + # Creates 1 profile + x.report(:p_1) { + Profile.create!(user_id: user.id, first_name: 'first_name', last_name: 'last_name', + address: 'address', dob: Time.now.to_date, state: 'verified') + } + # Creates 1000 profiles + x.report(:p_1000) { + 1_000.times do + Profile.create!(user_id: user.id, first_name: 'first_name', last_name: 'last_name', + address: 'address', dob: Time.now.to_date, state: 'verified') + end + } +end + +puts '---------------------------' +puts 'Descrypts data from profile' +puts '---------------------------' +Benchmark.bm do |x| + # Descrypts 1 profile + x.report(:p_1) { + Profile.last.first_name + Profile.last.last_name + Profile.last.address + Profile.last.dob + } + # Descrypts 1000 profiles + x.report(:p_1000) { + Profile.where(user_id: user.id).limit(1000).each do |profile| + profile.first_name + profile.last_name + profile.address + profile.dob + end + } +end + +## After benchmark +Profile.where(user_id: user.id).delete_all +user.destroy diff --git a/scripts/verify-security-pins.sh b/scripts/verify-security-pins.sh new file mode 100644 index 0000000..74f92d6 --- /dev/null +++ b/scripts/verify-security-pins.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Verify phase-1/2 minimum security gem/package versions in lockfiles. +set -euo pipefail + +semver_ge() { + printf '%s\n%s\n' "$2" "$1" | sort -V | head -1 | grep -qx "$2" +} + +check_gemfile_lock() { + local file=$1 name min + echo "Checking ${file}..." + while read -r name min; do + [[ -z "$name" ]] && continue + local ver + ver=$(grep -E "^ ${name} \(" "$file" | head -1 | sed -E 's/.*\(([0-9][0-9.a-z-]*).*/\1/' || true) + if [[ -z "$ver" ]]; then + echo "FAIL: ${name} not found in ${file}" + exit 1 + fi + if ! semver_ge "$ver" "$min"; then + echo "FAIL: ${name} ${ver} < required ${min} in ${file}" + exit 1 + fi + echo " OK ${name} ${ver} (>= ${min})" + done <<'GEMS' +rack 2.2.13 +nokogiri 1.15.5 +rexml 3.3.9 +tzinfo 1.2.10 +rails-html-sanitizer 1.4.4 +GEMS + if grep -qE '^ sidekiq \(' "$file"; then + local sidekiq_ver + sidekiq_ver=$(grep -E '^ sidekiq \(' "$file" | head -1 | sed -E 's/.*\(([0-9][0-9.a-z-]*).*/\1/') + if ! semver_ge "$sidekiq_ver" "6.4.0"; then + echo "FAIL: sidekiq ${sidekiq_ver} < required 6.4.0 in ${file}" + exit 1 + fi + echo " OK sidekiq ${sidekiq_ver} (>= 6.4.0)" + fi +} + +check_yarn_lock() { + local file=$1 + echo "Checking ${file}..." + local axios ws + axios=$(grep -A1 '^axios@\^0\.32' "$file" | grep 'version "' | head -1 | sed -E 's/.*version "([^"]+)".*/\1/') + ws=$(grep -A1 '^ws@\^7\.5' "$file" | grep 'version "' | head -1 | sed -E 's/.*version "([^"]+)".*/\1/') + if [[ -z "$axios" ]] || ! semver_ge "$axios" "0.32.0"; then + echo "FAIL: axios ${axios:-missing} < 0.32.0" + exit 1 + fi + echo " OK axios ${axios} (>= 0.32.0)" + if [[ -z "$ws" ]] || ! semver_ge "$ws" "7.5.10"; then + echo "FAIL: ws ${ws:-missing} < 7.5.10" + exit 1 + fi + echo " OK ws ${ws} (>= 7.5.10)" +} + +case "${1:-}" in + ruby) + check_gemfile_lock "${2:-Gemfile.lock}" + ;; + yarn) + check_yarn_lock "${2:-yarn.lock}" + ;; + *) + echo "Usage: $0 ruby [Gemfile.lock] | yarn [yarn.lock]" + exit 1 + ;; +esac + +echo "Security pin verification passed." diff --git a/spec/api/base_spec.rb b/spec/api/base_spec.rb new file mode 100644 index 0000000..2d39538 --- /dev/null +++ b/spec/api/base_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Base do + describe 'API::Base' do + it 'should exist' do + expect(API::Base).to eq(API::Base) + end + end +end diff --git a/spec/api/v2/admin/abilities_spec.rb b/spec/api/v2/admin/abilities_spec.rb new file mode 100644 index 0000000..33b52f3 --- /dev/null +++ b/spec/api/v2/admin/abilities_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +describe API::V2::Admin::Abilities, type: :request do + include_context 'bearer authentication' + + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + describe 'GET /api/v2/admin/abilities' do + context 'superadmin user' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'admin') } + it 'get all roles and permissions' do + get '/api/v2/admin/abilities', headers: auth_header + result = JSON.parse(response.body) + expect(response).to be_successful + expect(result).to eq( + 'read' => %w[Level APIKey Permission], + 'manage' => %w[User Activity Profile Label] + ) + end + end + + context 'member user' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'member') } + it 'get all roles and permissions' do + get '/api/v2/admin/abilities', headers: auth_header + result = JSON.parse(response.body) + expect(response).to be_successful + expect(result).to eq({}) + end + end + end +end diff --git a/spec/api/v2/admin/activities_spec.rb b/spec/api/v2/admin/activities_spec.rb new file mode 100644 index 0000000..409d638 --- /dev/null +++ b/spec/api/v2/admin/activities_spec.rb @@ -0,0 +1,145 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::V2::Admin::Activities do + include_context 'bearer authentication' + + describe 'GET /api/v2/admin/activities' do + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:do_request) { get '/api/v2/admin/activities', headers: auth_header } + + context 'admin user' do + let!(:test_user) { create(:user, email: 'testa@gmail.com', role: 'admin') } + let!(:first_user) { create(:user, email: 'test1@gmail.com') } + let!(:second_user) { create(:user, email: 'test2@gmail.com') } + + context 'user activities' do + let!(:create_actitivites) do + create(:activity, category: 'user', topic: 'session', action: 'login', user: first_user) + create(:activity, category: 'user', topic: 'session', action: 'login', user: second_user) + create(:activity, category: 'user', topic: 'otp', action: 'otp::enable', user: second_user) + create(:activity, category: 'admin', topic: 'otp', action: 'otp::enable', user: second_user) + end + + it 'doesnt return admin activities' do + get '/api/v2/admin/activities', headers: auth_header + activities = JSON.parse(response.body) + expect(Activity.where(category: 'user').count).to eq activities.count + expect(Activity.all.count).not_to eq activities.count + end + + it 'returns list of activities' do + get '/api/v2/admin/activities', headers: auth_header + activities = JSON.parse(response.body) + user_activites = Activity.where(category: 'user') + expect(user_activites.count).to eq activities.count + expect(user_activites.last.user_ip).to eq activities[0]['user_ip'] + expect(user_activites.last.user_agent).to eq activities[0]['user_agent'] + expect(user_activites.last.topic).to eq activities[0]['topic'] + expect(user_activites.last.action).to eq activities[0]['action'] + expect(user_activites.last.result).to eq activities[0]['result'] + expect(user_activites.last.user.email).to eq activities[0]['user']['email'] + expect(first_user.email).to eq activities[2]['user']['email'] + end + + it 'returns list of activities filtered by action' do + get '/api/v2/admin/activities', headers: auth_header, params: {action: 'login'} + activities = JSON.parse(response.body) + expect(activities.count).to eq 2 + end + + it 'returns list of activities filtered by uid' do + get '/api/v2/admin/activities', headers: auth_header, params: {uid: first_user.uid} + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by email' do + get '/api/v2/admin/activities', headers: auth_header, params: {email: first_user.email} + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by topic' do + get '/api/v2/admin/activities', headers: auth_header, params: {topic: 'otp'} + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by action and uid' do + get '/api/v2/admin/activities', headers: auth_header, params: {topic: 'session', action: 'login', uid: second_user.uid} + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + end + + context 'admin activities' do + let!(:create_actitivites) do + create(:activity, topic: 'session', action: 'login', user: first_user, category: 'admin', target_uid: second_user.uid) + create(:activity, topic: 'session', action: 'login', user: second_user, category: 'admin') + create(:activity, topic: 'otp', action: 'otp::enable', user: second_user, category: 'admin', target_uid: second_user.uid) + end + + it 'returns list of activities' do + get '/api/v2/admin/activities/admin', headers: auth_header + activities = JSON.parse(response.body) + + expect(Activity.count).to eq activities.count + expect(Activity.third.user_ip).to eq activities[0]['user_ip'] + expect(Activity.third.user_agent).to eq activities[0]['user_agent'] + expect(Activity.third.topic).to eq activities[0]['topic'] + expect(Activity.third.action).to eq activities[0]['action'] + expect(Activity.third.result).to eq activities[0]['result'] + expect(Activity.third.user.email).to eq activities[0]['admin']['email'] + expect(Activity.third.target.email).to eq activities[0]['target']['email'] + expect(Activity.first.user.email).to eq activities[2]['admin']['email'] + expect(Activity.first.target.email).to eq activities[2]['target']['email'] + end + + it 'returns list of activities filtered by action' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { action: 'login' } + activities = JSON.parse(response.body) + expect(activities.count).to eq 2 + end + + it 'returns list of activities filtered by uid' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { uid: first_user.uid } + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by email' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { email: first_user.email } + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by topic' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { topic: 'otp' } + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by action and uid' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { topic: 'session', action: 'login', uid: second_user.uid } + activities = JSON.parse(response.body) + expect(activities.count).to eq 1 + end + + it 'returns list of activities filtered by affected user' do + get '/api/v2/admin/activities/admin', headers: auth_header, params: { target_uid: second_user.uid } + activities = JSON.parse(response.body) + expect(activities.count).to eq 2 + end + end + end + end +end diff --git a/spec/api/v2/admin/api_keys_spec.rb b/spec/api/v2/admin/api_keys_spec.rb new file mode 100644 index 0000000..fb8d703 --- /dev/null +++ b/spec/api/v2/admin/api_keys_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +describe 'Api::V2::Admin::APIKeys' do + include_context 'bearer authentication' + + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:create_superadmin_permission) do + create :permission, + role: 'superadmin' + end + + describe 'GET /api/v2/admin/api_keys' do + let!(:test_user) { create(:user, otp: otp_enabled, role: 'superadmin') } + let(:otp_enabled) { true } + let!(:first_api_key) { create :api_key, key_holder_account: test_user } + let!(:second_api_key) { create :api_key, key_holder_account: test_user } + let(:expected_fields) do + { + kid: first_api_key.kid, + state: first_api_key.state, + scope: %w[trade] + } + end + let(:params) do + { + uid: test_user.uid + } + end + let(:do_request) { get "/api/v2/admin/api_keys", params: params, headers: auth_header } + + context 'successful' do + it 'Returns api keys for selected account' do + do_request + + expect(response.status).to eq(200) + expect(json_body.first.keys).to match_array %i[kid algorithm scope state created_at updated_at] + expect(json_body.first).to include(expected_fields) + expect(json_body.first).not_to include(:secret) + end + + it 'Returns api keys for selected account in ASC order' do + params[:ordering] = 'asc' + params[:order_by] = 'id' + do_request + + expect(response.status).to eq(200) + expect(json_body.first.keys).to match_array %i[kid algorithm scope state created_at updated_at] + expect(json_body.first).to include(expected_fields) + expect(json_body.first).not_to include(:secret) + end + + it 'Returns api keys for selected account in DESC order' do + params[:ordering] = 'desc' + params[:order_by] = 'id' + do_request + + expect(response.status).to eq(200) + expect(json_body.first.keys).to match_array %i[kid algorithm scope state created_at updated_at] + expect(json_body.second).to include(expected_fields) + expect(json_body.second).not_to include(:secret) + end + + it 'Returns empty array if no api keys exist for selected account' do + test_user.api_keys.delete_all + + do_request + expect(response.status).to eq(200) + expect(json_body).to eq([]) + end + end + + context 'error' do + let(:params) do + { + uid: 'random' + } + end + + it 'Returns error if user doesnt exist' do + do_request + expect(response.status).to eq(404) + expect(json_body.first).to include(:errors, ["admin.user.doesnt_exist"]) + end + + it 'Returns error if api key attribute doesnt exist' do + params[:uid] = test_user.uid + params[:order_by] = 'length' + do_request + expect(response.status).to eq(422) + expect(json_body.first).to include(:errors, ["api_keys.ordering.invalid_attribute"]) + end + + it 'Returns error if invalid ordering' do + params[:uid] = test_user.uid + params[:ordering] = 'res' + do_request + expect(response.status).to eq(422) + expect(json_body.first).to include(:errors, ["api_keys.ordering.invalid_ordering"]) + end + end + end +end diff --git a/spec/api/v2/admin/levels_spec.rb b/spec/api/v2/admin/levels_spec.rb new file mode 100644 index 0000000..c39ed97 --- /dev/null +++ b/spec/api/v2/admin/levels_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::V2::Admin::Levels do + include_context 'bearer authentication' + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + describe 'GET /api/v2/admin/levels' do + let(:do_request) { get '/api/v2/admin/levels', headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + it 'returns levels' do + do_request + + expect(response.status).to eq(200) + + result = JSON.parse(response.body) + + expect(result.length).to eq(Level.count) + + result.each do |level| + expected_level = Level.find(level['id']) + + expect(level['key']).to eq(expected_level.key) + expect(level['value']).to eq(expected_level.value) + end + end + end + end +end diff --git a/spec/api/v2/admin/metrics_spec.rb b/spec/api/v2/admin/metrics_spec.rb new file mode 100644 index 0000000..f084789 --- /dev/null +++ b/spec/api/v2/admin/metrics_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::V2::Admin::Metrics do + include_context 'bearer authentication' + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + describe 'GET /api/v2/admin/users' do + let(:do_request) { get '/api/v2/admin/metrics', headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + it 'returns empty hashes if no activity in database' do + do_request + + expect(response.status).to eq(200) + expect(response.body).to eq("{\"signups\":{},\"sucessful_logins\":{},\"failed_logins\":{},\"pending_applications\":0}") + + result = JSON.parse(response.body) + expect(result['signups']).to eq({}) + expect(result['sucessful_logins']).to eq({}) + expect(result['failed_logins']).to eq({}) + end + + context 'with data' do + let!(:login_activity) { create(:activity, action: 'login', result: 'succeed', topic: 'session', user: test_user) } + let!(:signup_activity) { create(:activity, action: 'signup', result: 'succeed', topic: 'account', user: test_user) } + let!(:failed_login_activity) { create(:activity, topic: 'session', action: 'login', result: 'failed', user: test_user) } + + it 'returns data as expected' do + do_request + + expect(response.status).to eq(200) + expect(JSON.parse(response.body)['failed_logins']).not_to eq({}) + expect(JSON.parse(response.body)['sucessful_logins']).not_to eq({}) + expect(JSON.parse(response.body)['signups']).not_to eq({}) + end + end + end + end +end diff --git a/spec/api/v2/admin/permissions_spec.rb b/spec/api/v2/admin/permissions_spec.rb new file mode 100644 index 0000000..6dc222e --- /dev/null +++ b/spec/api/v2/admin/permissions_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require 'spec_helper' +describe API::V2::Admin::Permissions do + include_context 'bearer authentication' + + let!(:create_admin_permission) { create(:permission, role: 'superadmin', action: 'accept', verb: 'get') } + let!(:test_user) { create(:user, role: 'superadmin') } + let!(:create_member_permission) { create(:permission, role: 'member') } + + describe 'GET /api/v2/admin/permissions' do + context 'successful response' do + it 'returns all permissions' do + get '/api/v2/admin/permissions', headers: auth_header + + result = JSON.parse(response.body) + expect(response).to be_successful + expect(result.count).to eq(Permission.count) + end + + it 'returns paginated permissions' do + get '/api/v2/admin/permissions', params: { limit: 1, page: 1 }, headers: auth_header + result = JSON.parse(response.body) + + expect(response).to be_successful + expect(response.headers.fetch('Total')).to eq '2' + expect(result.size).to eq 1 + expect(result.first['role']).to eq 'superadmin' + + get '/api/v2/admin/permissions', params: { limit: 1, page: 2 }, headers: auth_header + result = JSON.parse(response.body) + + expect(response).to be_successful + expect(response.headers.fetch('Total')).to eq '2' + expect(result.size).to eq 1 + expect(result.first['role']).to eq 'member' + end + end + end + + describe 'POST /api/v2/admin/permissions' do + context 'unsuccessful response' do + it 'returns error while invalid verb creating' do + post '/api/v2/admin/permissions', params: { role: 'admin', action: 'drop', verb: 'options', path: 'api/v2' }, headers: auth_header + result = JSON.parse(response.body) + + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.permissions.invalid_verb']) + end + + it 'returns error while invalid action creating' do + post '/api/v2/admin/permissions', params: { role: 'admin', action: 'delete', verb: 'put', path: 'api/v2' }, headers: auth_header + result = JSON.parse(response.body) + + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.permissions.invalid_action']) + end + + it 'returns error while invalid role creating' do + post '/api/v2/admin/permissions', params: { role: 'supertrader', action: 'accept', verb: 'put', path: 'api/v2' }, headers: auth_header + result = JSON.parse(response.body) + + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.permission.role_doesnt_exist']) + end + + it 'returns error while empty action creating' do + post '/api/v2/admin/permissions', params: { role: 'supertrader', action: '', verb: 'put', path: 'api/v2' }, headers: auth_header + result = JSON.parse(response.body) + + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.permission.empty_action']) + end + end + + context 'successful response' do + let(:do_request) { post '/api/v2/admin/permissions', params: { role: 'superadmin', action: 'accept', verb: 'put', path: 'api/v2/admin' }, headers: auth_header } + it 'creates new permission' do + expect { do_request }.to change { Permission.count }.by(1) + expect(response).to be_successful + end + end + end + + describe 'DELETE /api/v2/admin/permissions' do + context 'successful response' do + let(:do_request) { delete '/api/v2/admin/permissions', params: { id: create_member_permission.id }, headers: auth_header } + + it 'delete permission' do + expect { do_request }.to change { Permission.count }.by(-1) + expect(response).to be_successful + end + end + + context 'unsuccessful response' do + it 'return error while permission doesnt exist' do + delete '/api/v2/admin/permissions', params: { id: 0 }, headers: auth_header + result = JSON.parse(response.body) + expect(response.code).to eq '404' + expect(result['errors']).to eq(['admin.permission.doesnt_exist']) + end + end + end + + describe 'PUT /api/v2/admin/permissions' do + context 'successful response' do + let(:permission) { Permission.find_by(id: create_admin_permission.id) } + + it 'returns updated path for permission' do + put '/api/v2/admin/permissions', params: { id: create_admin_permission.id , path: 'api/v2/admin' }, headers: auth_header + + expect(response).to be_successful + expect(permission.path).to eq 'api/v2/admin' + end + + it 'returns updated verb for permission' do + put '/api/v2/admin/permissions', params: { id: create_admin_permission.id , verb: 'PUT' }, headers: auth_header + + expect(response).to be_successful + expect(permission.verb).to eq 'PUT' + end + + it 'returns updated fileds for permission' do + put '/api/v2/admin/permissions', params: { id: create_admin_permission.id , verb: 'POST', action: 'all', path: 'api/v2' }, headers: auth_header + + expect(response).to be_successful + expect(permission.verb).to eq 'POST' + expect(permission.action).to eq 'ALL' + expect(permission.path).to eq 'api/v2' + end + end + + context 'unsuccessful response' do + it 'return error while permission doesnt exist' do + put '/api/v2/admin/permissions', params: { id: 0, action: 'accept' }, headers: auth_header + + result = JSON.parse(response.body) + + expect(response.code).to eq '404' + expect(result['errors']).to eq(['admin.permission.doesnt_exist']) + end + end + end +end diff --git a/spec/api/v2/admin/profiles_spec.rb b/spec/api/v2/admin/profiles_spec.rb new file mode 100644 index 0000000..b146799 --- /dev/null +++ b/spec/api/v2/admin/profiles_spec.rb @@ -0,0 +1,211 @@ +# frozen_string_literal: true + +require 'spec_helper' +describe API::V2::Admin::Profiles do + include_context 'bearer authentication' + + let!(:create_admin_permission) do + create :permission, + role: 'admin' + create :permission, + role: 'superadmin' + end + + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:test_user) { create(:user, role: 'superadmin') } + let!(:profile1) { create(:profile) } + let!(:profile2) { create(:profile) } + let!(:profile3) { create(:profile) } + let!(:profile4) { create(:profile) } + let!(:profile5) { create(:profile) } + + describe 'GET /api/v2/admin/profiles' do + context 'successful response' do + it 'returns all profiles' do + get '/api/v2/admin/profiles', headers: auth_header + + result = JSON.parse(response.body) + expect(response).to be_successful + + expect(result.count).to eq(Profile.count) + expect(result.first.keys).to match_array %w[first_name last_name dob address postcode city country state metadata created_at updated_at] + end + + it 'returns paginated profiles' do + get '/api/v2/admin/profiles', params: { limit: 1, page: 1 }, headers: auth_header + result = JSON.parse(response.body) + + expect(response).to be_successful + expect(response.headers.fetch('Total')).to eq '5' + expect(result.size).to eq 1 + expect(result.first['first_name']).to eq profile1.first_name + expect(result.first['last_name']).to eq profile1.last_name + expect(result.first['dob']).to eq profile1.dob.to_s + + get '/api/v2/admin/profiles', params: { limit: 1, page: 2 }, headers: auth_header + result = JSON.parse(response.body) + + expect(response).to be_successful + expect(response.headers.fetch('Total')).to eq '5' + expect(result.size).to eq 1 + expect(result.first['first_name']).to eq profile2.first_name + expect(result.first['last_name']).to eq profile2.last_name + expect(result.first['dob']).to eq profile2.dob.to_s + + end + end + end + + describe 'POST /api/v2/admin/profiles' do + let(:user) { create :user } + let!(:request_params) do + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + end + + context 'successful response' do + it 'works correctly' do + expect { post '/api/v2/admin/profiles', params: request_params.merge(uid: user.uid), headers: auth_header } + .to change { Profile.count }.by(1) + + expect(response.status).to eq(201) + expect(json_body.keys).to match_array %i[first_name last_name dob address postcode city country state metadata created_at updated_at] + expect(json_body[:first_name]).to eq Profile.last.first_name + expect(json_body[:last_name]).to eq Profile.last.last_name + expect(json_body[:dob]).to eq Profile.last.dob.to_s + expect(json_body[:state]).to eq('submitted') + expect(json_body[:metadata]).to be_blank + expect(Profile.last.author).to eq(test_user.uid) + end + end + end + + describe 'PUT /api/v2/admin/profiles' do + context 'successful response' do + let!(:member_with_profile) do + @member = create :user, role: 'member' + create(:profile, user_id: @member.id, state: 'submitted') + end + + it 'returns profile' do + put '/api/v2/admin/profiles', params: { uid: @member.uid, state: 'verified' }, headers: auth_header + + expect(response.status).to eq(200) + profile = @member.profiles.last + expect(profile).to be + expect(json_body.keys).to match_array %i[first_name last_name dob address postcode city country state metadata created_at updated_at] + expect(json_body[:first_name]).to eq profile.first_name + expect(json_body[:last_name]).to eq profile.last_name + expect(json_body[:dob]).to eq profile.dob.to_s + expect(json_body[:state]).to eq('verified') + expect(profile.state).to eq('verified') + end + + let!(:superadmin_with_profile) do + @user = create :user, role: 'superadmin' + create(:profile, user_id: @user.id, state: 'submitted') + end + + it 'return profiles when superadmin updates superadmin' do + test_user.update!(role: 'superadmin') + put '/api/v2/admin/profiles', params: { uid: @user.uid, state: 'verified' }, headers: auth_header + + expect(response.status).to eq(200) + profile = @user.profiles.last + expect(profile).to be + expect(json_body.keys).to match_array %i[first_name last_name dob address postcode city country state metadata created_at updated_at] + expect(json_body[:first_name]).to eq profile.first_name + expect(json_body[:last_name]).to eq profile.last_name + expect(json_body[:dob]).to eq profile.dob.to_s + expect(json_body[:state]).to eq('verified') + expect(profile.state).to eq('verified') + expect(profile.metadata).to be_blank + end + end + + context 'unsuccessful response' do + let!(:admin_test) { test_user.update(role: 'admin') } + + let!(:superadmin_with_profile) do + @user = create :user, role: 'superadmin' + create(:profile, user_id: @user.id, state: 'submitted', author: test_user.uid) + + @user_admin = create :user, role: 'admin' + create(:profile, user_id: @user_admin.id, state: 'submitted', author: test_user.uid) + end + + it 'return error when non-superadmin user updates superadmin' do + put '/api/v2/admin/profiles', params: { uid: @user.uid, state: 'rejected' }, headers: auth_header + + result = JSON.parse(response.body) + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.profiles.superadmin_change']) + end + + it 'return error when author is trying to approve himself' do + allow(Barong::App.config).to receive_messages(profile_double_verification: 'true') + + put '/api/v2/admin/profiles', params: { uid: @user_admin.uid, state: 'rejected' }, headers: auth_header + + result = JSON.parse(response.body) + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.profiles.second_admin_approval']) + end + + it 'renders an error when profile doesnt exist' do + put '/api/v2/admin/profiles', params: { uid: '0', state: 'rejected' }, headers: auth_header + expect_status.to eq(404) + expect_body.to eq(errors: ['admin.profiles.doesnt_exist_or_not_editable']) + end + + it 'renders an error when profile is not editable' do + put '/api/v2/admin/profiles', params: { uid: profile3.user.uid, state: 'rejected' }, headers: auth_header + expect_status.to eq(404) + expect_body.to eq(errors: ['admin.profiles.doesnt_exist_or_not_editable']) + end + end + + context 'user with different amount of profile fields' do + let!(:profile) { create(:profile, user: test_user, last_name: nil, first_name: nil, state: 'submitted') } + + it 'returns partial updated profile' do + put '/api/v2/admin/profiles', params: { uid: test_user.uid, state: 'rejected' }, headers: auth_header + + expect(response.status).to eq(200) + profile = test_user.profiles.last + expect(profile).to be + expect(json_body.keys).to match_array %i[first_name last_name dob address postcode city country state metadata created_at updated_at] + expect(json_body[:first_name]).to eq profile.first_name + expect(json_body[:last_name]).to eq profile.last_name + expect(json_body[:dob]).to eq profile.dob.to_s + expect(json_body[:state]).to eq('rejected') + expect(profile.state).to eq('rejected') + end + + it 'returns full updated profile' do + put '/api/v2/admin/profiles', params: { uid: test_user.uid, state: 'rejected' }, headers: auth_header + + expect(response.status).to eq(200) + profile = test_user.profiles.last + expect(profile).to be + expect(json_body.keys).to match_array %i[first_name last_name dob address postcode city country state metadata created_at updated_at] + expect(json_body[:first_name]).to eq profile.first_name + expect(json_body[:last_name]).to eq profile.last_name + expect(json_body[:dob]).to eq profile.dob.to_s + expect(json_body[:state]).to eq('rejected') + expect(profile.state).to eq('rejected') + expect(profile.metadata).to be_blank + end + end + end +end diff --git a/spec/api/v2/admin/restrictions_spec.rb b/spec/api/v2/admin/restrictions_spec.rb new file mode 100644 index 0000000..cc71341 --- /dev/null +++ b/spec/api/v2/admin/restrictions_spec.rb @@ -0,0 +1,202 @@ +# frozen_string_literal: true + +require 'spec_helper' +describe API::V2::Admin::Restrictions do + include_context 'bearer authentication' + + before { create(:permission, role: 'superadmin') } + let!(:test_user) { create(:user, role: 'superadmin') } + + describe 'GET /api/v2/admin/restrictions' do + before do + create(:restriction, scope: 'ip', value: '0.0.0.1', category: 'blacklist', updated_at: 1.days.ago) + create(:restriction, scope: 'ip', value: '0.0.0.0', category: 'blacklist', updated_at: 3.days.ago) + create(:restriction, scope: 'ip_subnet', value: '1.2.3.4/24', category: 'blacklist', updated_at: 3.days.ago) + end + + context 'successful response' do + it 'returns all restrictions' do + get '/api/v2/admin/restrictions', headers: auth_header + + expect(response).to be_successful + expect(json_body.count).to eq(Restriction.count) + end + + it 'filters by scope' do + get '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip' } + + expect(json_body.length).to eq Restriction.where(scope: 'ip').count + expect(json_body.map { |r| r[:scope] }).to all eq 'ip' + end + + it 'filters by category' do + get '/api/v2/admin/restrictions', headers: auth_header, params: { category: 'blacklist' } + + expect(json_body.length).to eq Restriction.where(category: 'blacklist').count + expect(json_body.map { |r| r[:category] }).to all eq 'blacklist' + end + + it 'filters with date range' do + get '/api/v2/admin/restrictions', headers: auth_header, params: { from: 2.days.ago.to_i, range: 'updated' } + + expected = Restriction.where("updated_at >= ?", 2.days.ago) + expect(json_body.map { |r| r[:id] }).to match_array expected.map(&:id) + end + + it 'returns paginated restrictions' do + get '/api/v2/admin/restrictions', headers: auth_header, params: { limit: 1 } + result = json_body + + expect(response).to be_successful + expect(response.headers.fetch('Total')).to eq '3' + expect(result.size).to eq 1 + end + end + end + + describe 'POST /api/v2/admin/whitelink' do + context 'with default expire time' do + it 'create a whitelink token' do + post '/api/v2/admin/restrictions/whitelink', headers: auth_header + + expect(response).to be_successful + expect(json_body[:whitelink_token]).not_to be nil + end + end + + context 'with custom expire time' do + it 'create a whitelink token' do + post '/api/v2/admin/restrictions/whitelink', headers: auth_header, params: { expire_time: 30 } + + expect(response).to be_successful + expect(json_body[:whitelink_token]).not_to be nil + end + + it 'returns error if expire time too big' do + post '/api/v2/admin/restrictions/whitelink', headers: auth_header, params: { expire_time: 31 } + + expect(response.status).to eq 422 + expect(json_body[:whitelink_token]).to be nil + expect(json_body[:errors]).to include "invalid_expire" + end + end + end + + describe 'POST /api/v2/admin/restrictions' do + it 'creates new restriction' do + expect { + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip', value: '127.0.0.0', category: 'blacklist' } + }.to change { Restriction.count }.by(1) + + expect(response).to be_successful + end + + it 'creates new restriction with code' do + expect { + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip', value: '127.0.0.0', category: 'blacklist', code: 522 } + }.to change { Restriction.count }.by(1) + + expect(response).to be_successful + end + + it 'creates new whitelist restriction' do + expect { + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip', value: '127.0.0.0', category: 'whitelist' } + }.to change { Restriction.count }.by(1) + + expect(response).to be_successful + end + + it 'creates new maintenance restriction' do + expect { + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip', value: '127.0.0.0', category: 'maintenance' } + }.to change { Restriction.count }.by(1) + + expect(response).to be_successful + end + + context 'validation' do + it 'scope and state' do + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'office', value: '127.0.0.0', state: 'running' } + + expect(json_body[:errors]).to include "admin.restriction.invalid_scope" + expect(json_body[:errors]).to include "admin.restriction.invalid_state" + end + + it 'value' do + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'ip', value: '127.a.b.c', category: 'blacklist' } + + expect(json_body[:errors]).to include "value.invalid" + end + + it 'category' do + post '/api/v2/admin/restrictions', headers: auth_header, params: { scope: 'country', value: 'Ukraine', category: 'allow' } + + expect(json_body[:errors]).to include "admin.restriction.invalid_category" + end + end + end + + describe 'PUT /api/v2/admin/restrictions' do + let!(:restriction) { create(:restriction, scope: 'ip', value: '127.0.0.1', category: 'blacklist') } + + it 'updates state' do + expect { + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, state: 'disabled' } + }.to change { restriction.reload.state }.to('disabled') + end + + it 'updates value' do + expect { + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, value: '192.168.0.1' } + }.to change { restriction.reload.value }.to('192.168.0.1') + end + + it 'does not change scope if value becomes invalid' do + expect { + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, scope: 'ip_subnet' } + }.not_to change { restriction.reload.scope } + end + + it 'validates value' do + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, value: '192.168.0.abc' } + + expect(response).not_to be_successful + end + + it 'updates scope' do + expect { + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, scope: 'ip_subnet', value: '192.168.0.1/24' } + }.to change { restriction.reload.scope }.to('ip_subnet').and change { restriction.reload.value }.to '192.168.0.1/24' + end + + it 'updates category' do + expect { + put '/api/v2/admin/restrictions', headers: auth_header, params: { id: restriction.id, category: 'whitelist' } + }.to change { restriction.reload.category }.to('whitelist') + end + end + + describe 'DELETE /api/v2/admin/restrictions' do + let!(:restriction) { create(:restriction, scope: 'ip', value: '127.0.0.1', category: 'blacklist') } + + context 'successful response' do + let(:do_request) { delete '/api/v2/admin/restrictions', params: { id: restriction.id }, headers: auth_header } + + it 'delete restriction' do + expect { do_request }.to change { Restriction.count }.by(-1) + expect(response).to be_successful + end + end + + context 'unsuccessful response' do + it 'return error while restriction doesnt exist' do + delete '/api/v2/admin/restrictions', headers: auth_header, params: { id: 0 } + + result = JSON.parse(response.body) + expect(response.code).to eq '404' + expect(result['errors']).to eq(['admin.restriction.doesnt_exist']) + end + end + end +end diff --git a/spec/api/v2/admin/users_spec.rb b/spec/api/v2/admin/users_spec.rb new file mode 100644 index 0000000..83c667a --- /dev/null +++ b/spec/api/v2/admin/users_spec.rb @@ -0,0 +1,1176 @@ +# frozen_string_literal: true + +require 'spec_helper' +describe API::V2::Admin::Users do + include_context 'bearer authentication' + let!(:create_admin_permission) do + create :permission, + role: 'admin' + create :permission, + role: 'superadmin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let(:experimental_user) { create(:user, :with_document_phone_profile, state: "pending") } + + describe 'GET /api/v2/admin/users' do + let(:do_request) { get '/api/v2/admin/users', headers: auth_header } + + context 'admin user' do + let!(:test_user) { create(:user, email: 'testa@gmail.com', role: 'admin') } + let!(:second_user) { create(:user, email: 'testb@gmail.com', level: 2, state: 'active') } + let!(:third_user) { create(:user, email: 'testd@gmail.com', level: 2, state: 'pending') } + let!(:fourth_user) { create(:user, email: 'testc@gmail.com', level: 1, state: 'active') } + + let(:params) { {} } + let(:do_search_request) { get '/api/v2/admin/users', headers: auth_header, params: params } + + def validate_fields(user) + user.attributes.slice('email', 'role', 'level', 'otp', 'state', 'uid', 'data').symbolize_keys + end + + it 'returns list of users' do + do_request + + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(User.count).to eq json_body.count + expect(validate_fields(User.first)).to eq json_body.first.except(:referral_uid) + expect(validate_fields(User.second)).to eq json_body.second.except(:referral_uid) + expect(validate_fields(User.third)).to eq json_body.third.except(:referral_uid) + expect(validate_fields(User.fourth)).to eq json_body.fourth.except(:referral_uid) + + expect(json_body.first.keys).to_not include(:profiles) + + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '100' + end + + it 'returns list of users in ASC order' do + params[:ordering] = 'asc' + params[:order_by] = 'id' + do_search_request + + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(User.count).to eq json_body.count + expect(validate_fields(User.first)).to eq json_body.first.except(:referral_uid) + expect(validate_fields(User.second)).to eq json_body.second.except(:referral_uid) + expect(validate_fields(User.third)).to eq json_body.third.except(:referral_uid) + expect(validate_fields(User.fourth)).to eq json_body.fourth.except(:referral_uid) + end + + it 'returns list of users in DESC order' do + params[:ordering] = 'desc' + params[:order_by] = 'id' + do_search_request + + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(User.count).to eq json_body.count + expect(validate_fields(User.first)).to eq json_body.fifth.except(:referral_uid) + expect(validate_fields(User.second)).to eq json_body.fourth.except(:referral_uid) + expect(validate_fields(User.third)).to eq json_body.third.except(:referral_uid) + expect(validate_fields(User.fourth)).to eq json_body.second.except(:referral_uid) + end + + it 'returns error if invalid ordering' do + params[:ordering] = 'resc' + params[:order_by] = 'id' + do_search_request + + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"user.ordering.invalid_ordering\"]}" + end + + it 'returns error when user attribute doesnt exist' do + params[:ordering] = 'asc' + params[:order_by] = 'algorithm' + do_search_request + + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"user.ordering.invalid_attribute\"]}" + end + + it 'returns filtered list of users when only one filter param given created_at and from' do + params[:range] = 'created' + test_user.update(created_at: 1.day.ago) + params[:from] = fourth_user.created_at.to_i + do_search_request + + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body).not_to include(test_user) + end + + it 'returns filtered list of users when only one filter param given (user attribute) level' do + params[:level] = 2 + do_search_request + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body.count).to eq User.where(level: 2).count + end + + it 'returns filtered list of users when only one filter param given (user attribute) state' do + params[:state] = 'active' + do_search_request + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body.count).to eq User.where(state: 'active').count + end + + it 'returns filtered list of users when several params given (user attribute) : state and level' do + params[:level] = 2 + params[:state] = 'active' + do_search_request + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body.count).to eq User.where(level: 2, state: 'active').count + end + + let!(:user_with_profiles) do + user = create :user, email: 'peatio@barong.com' + create :profile, user_id: user.id, first_name: 'peatio', last_name: 'barong', country: 'us', state: 'rejected' + create :profile, user_id: user.id, first_name: 'peatio', last_name: 'barong', country: 'us', state: 'rejected' + end + + it 'returns only uniq set of users' do + user_with_profiles + + do_search_request + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body.count).to eq User.count + end + + it 'returns filtered list of users when only one filter param given (profile attribute) first_name' do + params[:first_name] = 'peatio' + do_search_request + expect(response.status).to eq 200 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + end + + it 'returns filtered list of users when several params given (profile attribute) : first_name and country' do + params[:first_name] = 'peatio' + params[:last_name] = 'barong' + params[:country] = 'barong' + do_search_request + expect(response.status).to eq 200 + end + + context 'extended params' do + let(:extended_params) { { extended: true } } + let(:do_extended_info_request) { get '/api/v2/admin/users', headers: auth_header, params: extended_params } + let!(:user) { create(:user, :with_profile) } + + it 'returns list of users with full info' do + do_extended_info_request + expect(User.count).to eq json_body.count + + expect(json_body.first.keys).to include(:profiles) + + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '100' + expect(json_body.first.keys).to match_array %i[email uid role level otp state data profiles referral_uid created_at updated_at] + expect(json_body.last[:profiles][0][:first_name]).to eq user.profiles.first.first_name + expect(json_body.last[:profiles][0][:last_name]).to eq user.profiles.first.last_name + expect(json_body.last[:profiles][0][:address]).to eq user.profiles.first.address + expect(json_body.last[:profiles][0][:dob]).to eq user.profiles.first.dob.to_s + end + end + + it 'returns list of users (ASC ordered) in search' do + params[:email] = 'testa@gmail.com' + do_search_request + + expect(json_body.count).to eq 1 + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(json_body[0][:email]).to eq 'testa@gmail.com' + end + + it 'returns all users (ASC ordered) in search req if field is not in the list' do + params.clear + params[:field] = 'bazz' + do_search_request + + expect(json_body.count).to eq User.all.count + + expect_status.to eq(200) + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + end + + context 'pagination test' do + it 'returns 1st page as default, limit 2 users per page' do + get '/api/v2/admin/users', headers: auth_header, params: { + limit: 2 + } + expect(validate_fields(User.first)).to eq json_body.first.except(:referral_uid) + expect(validate_fields(User.second)).to eq json_body.second.except(:referral_uid) + + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '2' + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + end + + it 'returns 2nd page, limit 2 users per page' do + get '/api/v2/admin/users', headers: auth_header, params: { + limit: 2, + page: 2 + } + expect(validate_fields(User.third)).to eq json_body.first.except(:referral_uid) + expect(validate_fields(User.fourth)).to eq json_body.second.except(:referral_uid) + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '2' + expect(response.headers.fetch('Per-Page')).to eq '2' + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + end + end + end + end + + describe 'PUT /api/v2/admin/users' do + let(:do_request) { put '/api/v2/admin/users', headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, role: "admin") } + let(:user_with_api_keys) { create(:user, state: "active", otp: true) } + let!(:api_key1) { create(:api_key, key_holder_account: user_with_api_keys) } + let!(:api_key2) { create(:api_key, key_holder_account: user_with_api_keys) } + + it 'renders error if uid is misssing' do + put '/api/v2/admin/users', headers: auth_header, params: { + state: 'active' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_uid\",\"admin.user.empty_uid\"]}" + end + + let!(:superadmin) { create(:user, role: 'superadmin') } + + it 'renders error when non-superadmin user updates superadmin' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: superadmin.uid, + state: 'active' + } + + result = JSON.parse(response.body) + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.user.superadmin_change']) + end + + it 'renders error if state is misssing' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.one_of_state_otp_email\"]}" + end + + it 'renders error if otp is misssing' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.one_of_state_otp_email\"]}" + end + + it 'renders error if role is misssing' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.one_of_state_otp_email\"]}" + end + + it 'renders error if uid is incorrect' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: 'asdasdasd', + state: 'active' + } + expect(response.status).to eq 404 + expect(response.body).to eq "{\"errors\":[\"admin.user.doesnt_exist\"]}" + end + + it 'sets state to active' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid, + state: 'active' + } + expect(response.status).to eq 200 + expect(experimental_user.reload.state).to eq 'active' + end + + it 'sets otp to false' do + experimental_user.update(otp: 'true') + experimental_user.labels.create(key: :otp, value: :enabled, scope: :private) + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid, + otp: 'false' + } + expect(response.status).to eq 200 + expect(experimental_user.reload.otp).to eq false + expect(experimental_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + it 'sets role to admin' do + post '/api/v2/admin/users/role', headers: auth_header, params: { + uid: experimental_user.uid, + role: 'admin' + } + expect(response.status).to eq 200 + expect(experimental_user.reload.role).to eq 'admin' + end + + it 'renders error when state is the same' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: experimental_user.uid, + state: 'pending' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.state_no_change\"]}" + end + + it 'doesnt disable api keys for enabling otp' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: user_with_api_keys.uid, + otp: true + } + expect(api_key1.reload.state).to eq 'active' + expect(api_key2.reload.state).to eq 'active' + end + + it 'doesnt disable api keys for activating user' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: user_with_api_keys.uid, + state: 'active' + } + expect(api_key1.reload.state).to eq 'active' + expect(api_key2.reload.state).to eq 'active' + end + + it 'disables api_keys when state changes' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: user_with_api_keys.uid, + state: 'banned' + } + expect(api_key1.reload.state).to eq 'inactive' + expect(api_key2.reload.state).to eq 'inactive' + end + + it 'disables api_keys when sets otp to false' do + put '/api/v2/admin/users', headers: auth_header, params: { + uid: user_with_api_keys.uid, + otp: false + } + expect(api_key1.reload.state).to eq 'inactive' + expect(api_key2.reload.state).to eq 'inactive' + end + end + + context 'superadmin user' do + let(:do_request) { put '/api/v2/admin/users', params: params, headers: auth_header } + let(:test_user) { create(:user, role: "superadmin") } + + let(:params) do + { + uid: experimental_user.uid, + email: new_email, + } + end + let(:new_email) { '' } + context "when email is blank" do + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["admin.user.empty_email"]) + end + end + context 'when email is invalid' do + let(:new_email) { 'bad_format' } + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["email.invalid"]) + end + end + context 'when email is valid' do + let(:new_email) { 'valid.email@gmail.com' } + it 'change an email' do + do_request + expect_status_to_eq 200 + expect(experimental_user.reload.email).to eq 'valid.email@gmail.com' + end + end + context 'when non-superadmin user updates email' do + let(:test_user) { create(:user, role: "admin") } + let(:new_email) { 'valid.email@gmail.com' } + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["superadmin.user.update_email"]) + end + end + end + end + + describe 'Get /api/v2/admin/users/:uid' do + let(:do_request) { get '/api/v2/admin/users/' + experimental_user.uid, headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, :with_document_phone_profile, role: "admin") } + + it 'renders error if uid is invalid' do + get '/api/v2/admin/users/asdasdsad', headers: auth_header + expect(response.status).to eq 404 + expect(response.body).to eq "{\"errors\":[\"admin.user.doesnt_exist\"]}" + end + + it 'returns user info' do + do_request + + expect(response.status).to eq 200 + + expect(json_body.keys).to match_array %i[email uid role level otp state data profiles labels phones documents data_storages comments referral_uid created_at updated_at] + expect(json_body[:uid]).to eq experimental_user.uid + expect(json_body[:role]).to eq experimental_user.role + expect(json_body[:email]).to eq experimental_user.email + expect(json_body[:level]).to eq experimental_user.level + expect(json_body[:otp]).to eq experimental_user.otp + expect(json_body[:state]).to eq experimental_user.state + expect(json_body[:profiles][0][:first_name]).to eq experimental_user.profiles[0].first_name + expect(json_body[:profiles][0][:last_name]).to eq experimental_user.profiles[0].last_name + expect(json_body[:profiles][0][:address]).to eq experimental_user.profiles[0].address + expect(json_body[:profiles][0][:dob]).to eq experimental_user.profiles[0].dob.to_s + expect(json_body[:documents][0][:doc_number]).to eq experimental_user.documents[0].doc_number + expect(json_body[:phones][0][:number]).to eq experimental_user.phones[0].number + end + end + end + + describe 'GET /api/v2/admin/labels/list' do + let(:do_request) { get '/api/v2/admin/users/labels/list', headers: auth_header } + let!(:test_user) { create(:user, role: 'admin') } + + context 'it returns array of labels attributes' do + let(:create_labels) { 10.times do create(:label, scope: 'private') end } + + it 'acts as expected' do + create_labels + do_request + labels_from_db = Label.where(scope: 'private').group(:key, :value).size + expect(response.body).to eq(labels_from_db.to_json) + expect(response.status).to eq 200 + end + end + + context 'no labels in database' do + it 'returns empty array' do + do_request + + expect(response.body).to eq '{}' + expect(response.status).to eq 200 + end + end + end + + describe 'POST /api/v2/admin/labels' do + + let(:do_request) { post '/api/v2/admin/users/labels', headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + it 'renders error when uid is missing' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + value: 'vedified' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_uid\",\"admin.user.empty_uid\"]}" + end + + let!(:superadmin) { create(:user, role: 'superadmin') } + + it 'renders error when non-superadmin user updates superadmin' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: superadmin.uid, + key: 'email', + value: 'vedified' + } + + result = JSON.parse(response.body) + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.user.superadmin_change']) + end + + it 'renders error when key is missing' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + value: 'vedified' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_key\",\"admin.user.empty_key\"]}" + end + + it 'renders error when description is empty' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + key: 'email', + value: 'vedified', + description: '' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.empty_description\"]}" + end + + it 'renders error when value is missing' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + key: 'email' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_value\",\"admin.user.empty_value\"]}" + end + + it 'renders error when uid is invalid' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: 'asdasds', + key: 'email', + value: 'verified' + } + expect(response.status).to eq 404 + expect(response.body).to eq "{\"errors\":[\"admin.user.doesnt_exist\"]}" + end + + it 'adds label with default public scope and description' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + key: 'email', + value: 'verified', + description: 'public label test' + } + updated_user = experimental_user.reload + added_label = updated_user.labels.last + expect(response.status).to eq 200 + expect(added_label.key).to eq 'email' + expect(added_label.value).to eq 'verified' + expect(added_label.scope).to eq 'public' + expect(added_label.description).to eq 'public label test' + end + + it 'adds label with private scope' do + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + key: 'email', + value: 'verified', + scope: 'private' + } + updated_user = experimental_user.reload + added_label = updated_user.labels.last + expect(response.status).to eq 200 + expect(added_label.key).to eq 'email' + expect(added_label.value).to eq 'verified' + expect(added_label.scope).to eq 'private' + end + end + end + + describe 'DELETE /api/v2/admin/labels' do + let(:do_request) { delete '/api/v2/admin/users/labels', headers: auth_header } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + let(:add_label) { + post '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + key: 'email', + value: 'verified', + description: 'experimental QA signup' + } + } + + it 'renders error when uid is missing' do + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + scope: 'public' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_uid\",\"admin.user.empty_uid\"]}" + end + + it 'renders error when key is missing' do + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + uid: experimental_user.uid, + scope: 'public' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_key\",\"admin.user.empty_key\"]}" + end + + it 'renders error when scope is missing' do + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + uid: experimental_user.uid + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_scope\",\"admin.user.empty_scope\"]}" + end + + it 'renders error when uid is invalid' do + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + uid: 'asdasdasdsadas', + scope: 'public' + } + expect(response.status).to eq 404 + expect(response.body).to eq "{\"errors\":[\"admin.user.doesnt_exist\"]}" + end + + it 'renders error when label does not exist' do + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + uid: experimental_user.uid, + scope: 'public' + } + expect(response.status).to eq 404 + expect(response.body).to eq "{\"errors\":[\"admin.label.doesnt_exist\"]}" + end + + it 'deletes label' do + add_label + delete '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'email', + uid: experimental_user.uid, + scope: 'public' + } + experimental_user.reload + expect(response.status).to eq 200 + expect(experimental_user.labels.count).to eq 2 + end + end + end + + describe 'POST /api/v2/admin/users/labels/update' do + let(:user) { create(:user, role: 'admin') } + let!(:create_superadmin_permission) { create(:permission, role: 'superadmin', action: 'accept', verb: 'get') } + let!(:test_user) { create(:user, role: 'superadmin') } + let(:data) do + { + uid: user.uid, + key: 'phone', + value: 'verified', + scope: 'private', + description: 'experimental phone verification' + } + end + let(:do_request) do + post '/api/v2/admin/users/labels/update', headers: auth_header, params: data + end + + context 'with default replace policy(true)' do + it 'creates a label' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 200 + expect(user.labels.find_by(key: 'phone')).not_to eq(nil) + end + + context 'when data is incomplete and description is empty' do + let(:data) do + { + uid: user.uid, + key: 'phone', + scope: 'private', + description: '' + } + end + + it 'receive an error' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_value\",\"admin.user.empty_value\",\"admin.user.empty_description\"]}" + expect(user.labels.find_by(key: 'phone')).to eq(nil) + end + end + end + + context 'with false replace policy' do + let(:data) do + { + uid: user.uid, + key: 'phone', + value: 'verified', + scope: 'private', + replace: false + } + end + + it 'receives an error' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 404 + expect(user.labels.find_by(key: 'phone')).to eq(nil) + expect(response.body).to eq "{\"errors\":[\"admin.label.doesnt_exist\"]}" + end + end + end + + describe 'GET /api/v2/admin/users/labels' do + let(:params) { { key: 'document', value: 'pending' } } + let(:do_request) { get '/api/v2/admin/users/labels', headers: auth_header, params: params } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + let(:document_pending_count) { 3 } + let(:document_rejected_count) { 2 } + let(:public_labels_count) { 2 } + + before(:example) do + document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'private') + end + + document_rejected_count.times do |i| + create(:label, key: 'document', value: 'rejected', scope: 'private') + end + end + + it 'renders error when key is missing' do + get '/api/v2/admin/users/labels', headers: auth_header, params: { + value: 'pending' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_key\"]}" + end + + it 'renders error when value is missing' do + get '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'document' + } + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.missing_value\"]}" + end + + it 'returns users' do + get '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'document', + value: 'pending' + } + + expect(json_body.count).to eq document_pending_count + end + + context 'pagination test' do + it 'returns 1st page as default, limit 2 users per page' do + get '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'document', + value: 'pending', + limit: 2 + } + + expect(json_body.count).to eq 2 + + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(User.first.email).to eq json_body.first[:email] + expect(User.second.email).to eq json_body.second[:email] + + expect(response.headers.fetch('Total')).to eq document_pending_count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + + it 'returns 2nd page, limit 2 users per page' do + get '/api/v2/admin/users/labels', headers: auth_header, params: { + key: 'document', + value: 'pending', + limit: 2, + page: 2 + } + + expect(User.third.email).to eq json_body.first[:email] + + expect(json_body.first.keys).to match_array %i[email uid role level otp state referral_uid data] + expect(response.headers.fetch('Total')).to eq document_pending_count.to_s + expect(response.headers.fetch('Page')).to eq '2' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + end + end + end + + describe 'POST /api/v2/admin/users/update' do + let!(:user) { create(:user, otp: true) } + let(:new_state) { 'banned' } + let(:request) { '/api/v2/admin/users/update' } + let!(:superadmin) { create(:user, role: 'superadmin') } + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + it 'changes state' do + post request, headers: auth_header, params: { uid: user.uid, state: new_state } + + expect(response.status).to eq 200 + expect(user.reload.state). to eq new_state + end + + it 'changes state when superadmin user updates superadmin' do + test_user.update!(role: 'superadmin') + post request, headers: auth_header, params: { uid: superadmin.uid, state: 'banned' } + + expect(response.status).to eq 200 + expect(superadmin.reload.state). to eq 'banned' + end + + it 'disables otp' do + post request, headers: auth_header, params: { uid: user.uid, otp: false } + + expect(response.status).to eq 200 + expect(user.reload.otp).to eq false + end + + it 'renders error when non-superadmin user updates superadmin' do + post request, headers: auth_header, params: { uid: superadmin.uid, state: 'banned' } + + result = JSON.parse(response.body) + expect(response.code).to eq '422' + expect(result['errors']).to eq(['admin.user.superadmin_change']) + end + + it 'renders error when state does not change' do + post request, headers: auth_header, params: { uid: user.uid, state: user.state } + + expect(response.status).to eq 422 + expect(response.body).to eq "{\"errors\":[\"admin.user.state_no_change\"]}" + end + end + end + + describe 'GET /api/v2/admin/users/documents/pending' do + let(:do_request) { get '/api/v2/admin/users/documents/pending', headers: auth_header} + + context 'admin user' do + let(:test_user) { create(:user, role: 'admin') } + + let(:private_document_pending_count) { 3 } + let(:private_document_replaced_count) { 3 } + let(:public_document_pending_count) { 2 } + + context 'private pending documents' do + before(:example) do + private_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'private') + end + public_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'public') + end + end + + it 'returns users with profile and documents if params extended' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: { extended: true } + + expect(json_body.first.keys).to include(:profiles) + expect(json_body.first.keys).to include(:documents) + expect(json_body.count).to eq private_document_pending_count + end + + it 'doesnt returns users with profile and documents if extended false' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: { extended: false } + + expect(json_body.first.keys).not_to include(:profile) + expect(json_body.first.keys).not_to include(:documents) + expect(json_body.count).to eq private_document_pending_count + end + + it 'doesnt returns users with profile and documents if extended not provided' do + get '/api/v2/admin/users/documents/pending', headers: auth_header + + expect(json_body.first.keys).not_to include(:profile) + expect(json_body.first.keys).not_to include(:documents) + expect(json_body.count).to eq private_document_pending_count + end + + it 'returns users' do + get '/api/v2/admin/users/documents/pending', headers: auth_header + + expect(json_body.first.keys).to_not include(:profile) + expect(json_body.count).to eq private_document_pending_count + end + + it 'returns users users with extended info' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: {extended: true} + + expect(json_body.first.keys).to include(:profiles) + expect(json_body.count).to eq private_document_pending_count + end + end + + context 'private pending and replaced documents' do + before(:example) do + private_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'private') + end + + private_document_replaced_count.times do |i| + create(:label, key: 'document', value: 'replaced', scope: 'private') + end + + public_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'public') + end + end + + it 'returns users with profile and documents with value pending and replaces' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: { extended: true } + + expect(json_body.first.keys).to include(:profiles) + expect(json_body.first.keys).to include(:documents) + expect(json_body.count).to eq private_document_pending_count + private_document_replaced_count + end + end + + context 'filtering' do + before(:example) do + private_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'private') + end + public_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'public') + end + end + + let(:params) { {} } + let(:do_search_request) { get '/api/v2/admin/users/documents/pending', headers: auth_header, params: params } + + it 'returns filtered list of users when only one filter param given created_at and from' do + params[:range] = 'created' + User.first.update(created_at: 1.day.ago) + params[:from] = 8.hours.ago.to_i + do_search_request + + expect(response.status).to eq 200 + expect(json_body.count).to eq (private_document_pending_count - 1) + end + + it 'returns filtered list of users when only one filter param given updated_at' do + params[:range] = 'updated' + User.first.update(updated_at: 1.day.ago) + params[:from] = 8.hours.ago.to_i + do_search_request + + expect(response.status).to eq 200 + expect(json_body.count).to eq (private_document_pending_count - 1) + end + + it 'returns filtered list of users when only one filter param given (user attribute) level' do + User.first.update(level: 2) + params[:level] = 2 + do_search_request + + expect(json_body.count).to eq User.joins(:labels).where(labels: { key: 'document', value: 'pending', scope: 'private' }).where(level: 2).count + end + + it 'returns filtered list of users when only one filter param given (user attribute) state' do + params[:state] = 'active' + do_search_request + expect(response.status).to eq 200 + expect(json_body.count).to eq User.joins(:labels).where(labels: { key: 'document', value: 'pending', scope: 'private' }).where(state: 'active').count + end + + it 'returns filtered list of users when several params given (user attribute) : state and level' do + User.first.update(level: 2) + params[:level] = 2 + params[:state] = 'active' + do_search_request + expect(response.status).to eq 200 + expect(json_body.count).to eq User.joins(:labels).where(labels: { key: 'document', value: 'pending', scope: 'private' }).where(level: 2, state: 'active').count + end + + let(:profile) do + create :profile, first_name: 'peatio', + last_name: 'barong', + country: 'us' + Label.create(key: 'document', value: 'pending', scope: 'private', user_id: Profile.last.user_id) + end + + it 'returns filtered list of users when only one filter param given (profile attribute) first_name' do + profile + params[:first_name] = 'peatio' + do_search_request + + expect(response.status).to eq 200 + end + + it 'returns filtered list of users when several params given (profile attribute) : first_name and country' do + profile + params[:first_name] = 'peatio' + params[:last_name] = 'barong' + params[:country] = 'us' + do_search_request + + expect(response.status).to eq 200 + end + end + + context 'sorting test' do + let!(:first_user) { create(:user) } + let!(:second_user) { create(:user) } + let!(:test_user) { create(:user, role: 'admin') } + + before(:example) do + create(:label, key: 'document', value: 'pending', scope: 'private', user_id: second_user.id, created_at: 10.minutes.ago) + create(:label, key: 'document', value: 'pending', scope: 'private', user_id: first_user.id, created_at: 5.minutes.ago) + end + + it 'returns users sorted by time of label creation' do + get '/api/v2/admin/users/documents/pending', headers: auth_header + expect(json_body.last[:email]).to eq first_user.email + end + end + + context 'pagination test' do + before(:example) do + private_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'private') + end + public_document_pending_count.times do |i| + create(:label, key: 'document', value: 'pending', scope: 'public') + end + end + + it 'returns 1st page as default, limit 2 users per page' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: { + limit: 2 + } + + expect(json_body.count).to eq 2 + expect(response.headers.fetch('Total')).to eq private_document_pending_count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + + it 'returns 2nd page, limit 2 users per page' do + get '/api/v2/admin/users/documents/pending', headers: auth_header, params: { + limit: 2, + page: 2 + } + + expect(response.headers.fetch('Total')).to eq private_document_pending_count.to_s + expect(response.headers.fetch('Page')).to eq '2' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + end + end + end + + context 'comments' do + describe 'POST /api/v2/admin/users/comments' do + let(:url) { '/api/v2/admin/users/comments' } + let(:params) { {} } + let(:do_request) { post url, headers: auth_header, params: params } + + + context 'successfull request' do + let(:params) do + { + uid: experimental_user.uid, + title: 'some useful title', + data: 'some useless data.', + } + end + let(:do_request) { post url, headers: auth_header, params: params } + + it 'creates new comment' do + expect { do_request }.to change { Comment.count }.by 1 + expect(response.status).to eq 201 + end + + it 'saves author_uid' do + do_request + + expect(json_body.keys).to match_array %i[email uid role level otp state data profiles labels phones documents data_storages comments referral_uid created_at updated_at] + expect(Comment.last.author_uid).to eq test_user.uid + expect(json_body[:comments][0][:author_uid]).to eq test_user.uid + expect(json_body[:profiles][0][:first_name]).to eq experimental_user.profiles[0].first_name + expect(json_body[:profiles][0][:last_name]).to eq experimental_user.profiles[0].last_name + expect(json_body[:profiles][0][:address]).to eq experimental_user.profiles[0].address + expect(json_body[:profiles][0][:dob]).to eq experimental_user.profiles[0].dob.to_s + expect(json_body[:documents][0][:doc_number]).to eq experimental_user.documents[0].doc_number + expect(json_body[:phones][0][:number]).to eq experimental_user.phones[0].number + expect(response.status).to eq 201 + end + end + + context 'errored request' do + let(:params) do + { + uid: experimental_user.uid, + title: Faker::Lorem.characters(number: 65), + data: 'some useless data.', + } + end + + it 'returns error because of length of comment' do + expect { do_request }.to change { Comment.count }.by 0 + expect(json_body[:errors]).to eq ['admin.comments.title_too_long'] + expect(response.status).to eq 422 + end + end + end + + describe 'PUT /api/v2/admin/users/comments' do + let(:url) { '/api/v2/admin/users/comments' } + let!(:seeded) { Comment.create(author_uid: test_user.uid, user_id: experimental_user.id, title: 'preseeded record', data: 'prev') } + let(:params) do + { + id: seeded.id, + uid: experimental_user.uid, + data: 'next.', + } + end + let(:do_request) { put url, headers: auth_header, params: params } + + it 'does not create new comment' do + expect { do_request }.not_to change { Comment.count } + expect(response.status).to eq 200 + end + + it 'comment does not exist' do + put url, headers: auth_header, params: { id: seeded.id+1, uid: experimental_user.uid, title: 'vv', data: 'ww' } + expect(response.status).to eq 404 + end + + it 'changes data' do + do_request + expect(seeded.reload.data).to eq 'next.' + expect(seeded.reload.title).to eq 'preseeded record' + expect(response.status).to eq 200 + end + end + + describe 'DELETE /api/v2/admin/users/comments' do + let(:url) { '/api/v2/admin/users/comments' } + let!(:seeded) { Comment.create(author_uid: test_user.uid, user_id: experimental_user.id, title: 'preseeded record', data: 'prev') } + let(:params) { { id: seeded.id } } + let(:do_request) { delete url, headers: auth_header, params: params } + + it 'does not create new comment' do + expect { do_request }.to change { Comment.count }.by -1 + expect(response.status).to eq 200 + end + + it 'comment does not exist' do + put url, headers: auth_header, params: { id: seeded.id+1, uid: experimental_user.uid, title: 'vv', data: 'ww' } + expect(response.status).to eq 404 + end + end + end +end diff --git a/spec/api/v2/auth/audit_spec.rb b/spec/api/v2/auth/audit_spec.rb new file mode 100644 index 0000000..6417a28 --- /dev/null +++ b/spec/api/v2/auth/audit_spec.rb @@ -0,0 +1,307 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '/api/v2/auth functionality test' do + include_context 'geoip mock' + before do + end + before do + create :permission, role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users' + create :permission, role: 'member' + create :permission, role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets' + create :permission, role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets' + @user = create(:user) + @accountant_user = create(:user, role: 'accountant') + @admin_user = create(:user, role: 'admin') + @technical_user = create(:user, role: 'technical') + + Permission.create(role: 'admin', action: 'ACCEPT', verb: 'get', path: 'api/v2/admin/users/list') + Permission.create(role: 'accountant', action: 'ACCEPT', verb: 'post', path: 'api/v2/accountant/documents') + end + + let(:uri) { '/api/v2/identity/sessions' } + + let(:do_create_session_request_acc) { post uri, params: { email: @accountant_user.email, password: @accountant_user.password }, headers: { 'HTTP_USER_AGENT' => 'random-browser' }} + let(:do_create_session_request_adm) { post uri, params: { email: @admin_user.email, password: @admin_user.password, otp_code: '1357' }, headers: { 'HTTP_USER_AGENT' => 'random-browser' } } + let(:do_create_session_request_tech) { post uri, params: { email: @technical_user.email, password: @technical_user.password }, headers: { 'HTTP_USER_AGENT' => 'random-browser' } } + + let(:do_create_session_request) { post uri, params: { email: @user.email, password: @user.password }, headers: { 'HTTP_USER_AGENT' => 'random-browser' } } + let(:auth_request) { '/api/v2/auth/not_in_the_rules_path' } + let(:protected_request) { '/api/v2/resource/users/me' } + + describe 'audit permissions testing' do + let!(:turn_off_2fa) do + User.all.each { |u| u.update(otp: false) } + end + let(:do_some_requests) do + delete '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + post '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + put '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + end + + context 'records activity if path doesnt match with AUDIT permission path but matches with DROP or BLANK' do + context 'acts as expected with different roles with another permission type match' do + before do + Permission.delete_all + Rails.cache.delete('permissions') + end + + it 'for accountant' do + expect(Permission.all.count).to eq(0) + # accountant + do_create_session_request_acc + do_some_requests + expect(Activity.where(category: 'admin', result: 'denied').count).to eq(3) + + end + + it 'for admin' do + expect(Permission.all.count).to eq(0) + # admin + do_create_session_request_adm + do_some_requests + expect(Activity.where(category: 'admin', result: 'denied').count).to eq(3) + end + end + + context 'acts as expected with different roles without any permissions' do + before do + Permission.delete_all + Rails.cache.delete('permissions') + expect(Permission.all.count).to eq(0) + end + + it 'for accountant' do + # accountant + do_create_session_request_acc + do_some_requests + expect(response.status).to eq(401) + expect(Activity.where(category: 'admin', result: 'denied').count).to eq(3) + end + + it 'for admin' do + # admin + do_create_session_request_adm + do_some_requests + expect(response.status).to eq(401) + expect(Activity.where(category: 'admin', result: 'denied').count).to eq(3) + end + end + end + + context 'records failed activity if path matches with AUDIT permission path' do + context 'creates only one thread' do + before do + Permission.delete_all + Permission.create(role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets') + Permission.create(role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users') + Permission.create(role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets') + Rails.cache.delete('permissions') + end + end + + context 'without topic specified and without user_uid in params' do + let!(:create_audit_permissions) do + Permission.create(role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets') + Permission.create(role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users') + Permission.create(role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets') + Rails.cache.delete('permissions') + end + + it 'works for general topic' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + get '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('general') + expect(Activity.last.result).to eq('denied') + end + + it 'works for patch request' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + patch '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('general') + expect(Activity.last.action).to eq('update') + expect(Activity.last.result).to eq('denied') + end + + it 'works for non - put post patch get delete requests' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + head '/api/v2/auth/api/v2/admin', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('general') + expect(Activity.last.action).to eq('system') + expect(Activity.last.result).to eq('denied') + end + + it 'for account role' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('denied') + end + + it 'for admin role' do + do_create_session_request_adm + expect(Activity.where(category: 'admin').count).to eq(0) + put '/api/v2/auth/api/v2/admin/users', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('users') + expect(Activity.last.result).to eq('denied') + end + + it 'for technical role' do + do_create_session_request_tech + expect(Activity.where(category: 'admin').count).to eq(0) + post '/api/v2/auth/api/v2/admin/wallets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('wallets') + expect(Activity.last.result).to eq('denied') + end + end + + context 'with specified topic and without user_uid' do + let!(:create_audit_permissions) do + Permission.delete_all + Permission.create(role: 'technical', action: 'ACCEPT', verb: 'post', path: 'api/v2/admin/wallets', topic: 'tech_support') + Permission.create(role: 'admin', action: 'ACCEPT', verb: 'put', path: 'api/v2/admin/users', topic: 'administrating') + Permission.create(role: 'accountant', action: 'ACCEPT', verb: 'delete', path: 'api/v2/admin/markets', topic: 'accounting') + + Permission.create(role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets', topic: 'tech_support') + Permission.create(role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users', topic: 'administrating') + Permission.create(role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets', topic: 'accounting') + Rails.cache.delete('permissions') + end + context 'for different roles' do + it 'accountant' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('accounting') + expect(Activity.last.result).to eq('succeed') + end + + it 'technical' do + do_create_session_request_tech + expect(Activity.where(category: 'admin').count).to eq(0) + post '/api/v2/auth/api/v2/admin/wallets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('tech_support') + expect(Activity.last.result).to eq('succeed') + end + + it 'admin' do + do_create_session_request_adm + expect(Activity.where(category: 'admin').count).to eq(0) + put '/api/v2/auth/api/v2/admin/users', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('administrating') + expect(Activity.last.result).to eq('succeed') + end + end + end + + context 'with specified topic and user_uid' do + let!(:create_audit_permissions) do + Permission.create(role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets', topic: 'tech_support') + Permission.create(role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users', topic: 'administrating') + Permission.create(role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets', topic: 'accounting') + Rails.cache.delete('permissions') + end + + it 'for different roles' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', params: { uid: @user.uid } ,headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('denied') + end + + it 'includes data in denied activity' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' }, params: { user_uid: @user.uid } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('denied') + expect(Activity.last.target_uid).to eq(@user.uid) + expect(JSON.parse(Activity.last.data).keys).to include('user_uid', 'path', 'note') + end + end + end + + context 'records succesfull activity if path matches with AUDIT permission path' do + context 'with different params combination' do + let!(:create_audit_permissions) do + Permission.create(role: 'accountant', action: 'ACCEPT', verb: 'delete', path: 'api/v2/admin/markets') + Permission.create(role: 'admin', action: 'ACCEPT', verb: 'put', path: 'api/v2/admin/users') + Permission.create(role: 'technical', action: 'ACCEPT', verb: 'post', path: 'api/v2/admin/wallets') + Permission.create(role: 'technical', action: 'AUDIT', verb: 'post', path: 'api/v2/admin/wallets') + Permission.create(role: 'admin', action: 'AUDIT', verb: 'put', path: 'api/v2/admin/users') + Permission.create(role: 'accountant', action: 'AUDIT', verb: 'delete', path: 'api/v2/admin/markets') + Rails.cache.delete('permissions') + end + + context 'for different roles without params' do + it 'accountant' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('succeed') + end + + it 'technical' do + do_create_session_request_tech + expect(Activity.where(category: 'admin').count).to eq(0) + post '/api/v2/auth/api/v2/admin/wallets', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('wallets') + expect(Activity.last.result).to eq('succeed') + end + + it 'admin' do + do_create_session_request_adm + expect(Activity.where(category: 'admin').count).to eq(0) + put '/api/v2/auth/api/v2/admin/users', headers: { 'HTTP_USER_AGENT' => 'random-browser' } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('users') + expect(Activity.last.result).to eq('succeed') + end + end + + context 'for different roles with user_uid in params' do + it 'includes data in succesfull activity' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' }, params: { user_uid: @user.uid } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('succeed') + expect(Activity.last.target_uid).to eq(@user.uid) + expect(JSON.parse(Activity.last.data).keys).to include('user_uid', 'path', 'note') + end + + it 'create activity and save target_uid for accountant if user with this uid exists' do + do_create_session_request_acc + expect(Activity.where(category: 'admin').count).to eq(0) + delete '/api/v2/auth/api/v2/admin/markets', headers: { 'HTTP_USER_AGENT' => 'random-browser' }, params: { user_uid: @user.uid } + expect(Activity.where(category: 'admin').count).to eq(1) + expect(Activity.last.topic).to eq('markets') + expect(Activity.last.result).to eq('succeed') + expect(Activity.last.target_uid).to eq(@user.uid) + end + end + end + end + end +end diff --git a/spec/api/v2/auth/auth_spec.rb b/spec/api/v2/auth/auth_spec.rb new file mode 100644 index 0000000..e3012a7 --- /dev/null +++ b/spec/api/v2/auth/auth_spec.rb @@ -0,0 +1,366 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '/api/v2/auth functionality test' do + include_context 'geoip mock' + + let(:uri) { '/api/v2/identity/sessions' } + let!(:create_permissions) do + create :permission, role: 'admin' + create :permission, role: 'member', action: 'ACCEPT', verb: 'all', path: 'not_in_the_rules_path' + create :permission, role: 'member', action: 'ACCEPT', verb: 'get', path: '/api/v2/resource/users/me' + create :permission, role: 'accountant' + end + let!(:user) { create(:user) } + let(:params) do + { + email: user.email, + password: user.password + } + end + + let(:do_create_session_request) { post uri, params: params } + let(:auth_request) { '/api/v2/auth/not_in_the_rules_path' } + let(:protected_request) { '/api/v2/resource/users/me' } + + describe 'testing workability with session' do + before do + Rails.cache.delete('permissions') + end + + context 'with valid session' do + before do + do_create_session_request + end + + it 'returns bearer token on valid session' do + get auth_request + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'allows any type of request' do + available_types = %w[post get put head delete patch] + available_types.each do |ping| + method("#{ping}").call auth_request + + expect(response.headers['Authorization']).not_to be_nil + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(200) + end + end + end + + context 'ip related' do + let(:create_session_uri) { uri } + let(:auth_uri) { '/api/v2/auth/not_in_the_rules_path' } + let(:ip_to_be_equal) { '168.238.57.64' } + let(:ip_that_differs) { '168.27.3.225' } + + context 'default ip behaviour from HTTP_X_FORWARDED_FOR via action_dispatch.remote_ip' do + it 'works if ip from session matches request ip' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'denies access if ip from session doesnt match request ip' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_that_differs } + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + end + + context 'ip behaviour from TRUE_CLIENT_IP header' do + before do + allow(Barong::App.config).to receive_messages(gateway: 'akamai') + end + + it 'works if ip from session matches request ip' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal, 'HTTP_TRUE_CLIENT_IP' => ip_to_be_equal } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_that_differs, 'HTTP_TRUE_CLIENT_IP' => ip_to_be_equal } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'uses HTTP_X_FORWARDED_FOR if TRUE_CLIENT_IP missing and works' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'uses HTTP_X_FORWARDED_FOR if TRUE_CLIENT_IP empty and works' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal, 'HTTP_TRUE_CLIENT_IP' => '' } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal, 'HTTP_TRUE_CLIENT_IP' => '' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'denies access if ip from session doesnt match request ip in HTTP_TRUE_CLIENT_IP' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal, 'HTTP_TRUE_CLIENT_IP' => ip_to_be_equal } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_that_differs, 'HTTP_TRUE_CLIENT_IP' => ip_that_differs } + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + + it 'uses HTTP_X_FORWARDED_FOR if TRUE_CLIENT_IP empty or missing and denies access' do + post create_session_uri, params: params, headers: { 'HTTP_X_FORWARDED_FOR' => ip_to_be_equal, 'HTTP_TRUE_CLIENT_IP' => '' } + get auth_uri, headers: { 'HTTP_X_FORWARDED_FOR' => ip_that_differs } + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + end + end + + context 'testing session related errors' do + it 'renders error if no session or api key headers provided' do + get auth_request + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_session\"]}") + end + + it 'renders error if session belongs to non-active user' do + do_create_session_request + expect(response.status).to eq(200) + user.update(state: 'banned') + + get auth_request + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.user_not_active\"]}") + end + end + + context 'testing restrictions' do + before do + Rails.cache.delete('permissions') + do_create_session_request + expect(response.status).to eq(200) + end + + let(:do_restricted_request) { put '/api/v2/auth/api/v2/peatio/management/ping' } + + it 'receives access error if path is blacklisted' do + do_restricted_request + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.permission_denied\"]}") + end + + let(:do_whitelisted_request) { put '/api/v2/auth/api/v2/peatio/public/ping' } + + it 'receives access error if path is blacklisted' do + do_whitelisted_request + expect(response.status).to eq(200) + expect(response.body).to be_empty + expect(response.headers['Authorization']).to be_nil + end + end + end + + describe 'testing workability with api keys' do + include_context 'bearer authentication' + let!(:test_user) { create(:user, otp: otp_enabled) } + let(:otp_enabled) { true } + let!(:api_key) { create :api_key, key_holder_account: test_user } + let(:otp_code) { '1357' } + let(:nonce) { (Time.now.to_f * 1000).to_i } + let(:kid) { api_key.kid } + let(:secret) { api_key.secret } + let(:data) { nonce.to_s + kid } + let(:algorithm) { 'SHA' + api_key.algorithm[2..4]} + let(:signature) { OpenSSL::HMAC.hexdigest(algorithm, secret, data) } + + before do + Rails.cache.delete('permissions') + SecretStorage.store_secret(secret, api_key.kid) + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, otp_code) { true } + end + + context 'testing api key related errors' do + it 'catches api key headers and renders error on missing header' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + } + expect(response.status).to eq(422) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_api_key_headers\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'catches api key headers and renders error if nonce is string' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => 'nonce', + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.nonce_not_valid_timestamp\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'catches api key headers and renders error if nonce is zero' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => 0, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.nonce_not_valid_timestamp\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'catches api key headers and renders error if nonce is older than default 5 seconds in milliseconds' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => ((Time.now - 10.seconds).to_f * 1000).to_i, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.nonce_expired\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'catches api key headers and renders error if nonce is from future with milisecond' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => ((Time.now + 10.seconds).to_f * 1000).to_i, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.nonce_expired\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'catches api key headers and renders error on blank header' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => ' ' + } + expect(response.status).to eq(422) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_api_key_headers\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'renders error when signature is invalid' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => 'some-random-signature' + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_signature\"]}") + expect(response.headers['Authorization']).to be_nil + end + + let(:ban_api_key) { APIKey.last.update(state: 'banned') } + + it 'renders error when api key state is not active' do + ban_api_key + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.apikey_not_active\"]}") + expect(response.headers['Authorization']).to be_nil + end + + let(:ban_user) { test_user.update(state: 'banned') } + + it 'renders error when api key state is not active' do + ban_user + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.apikey_not_active\"]}") + expect(response.headers['Authorization']).to be_nil + end + + let(:disable_user_2fa) { test_user.update(otp: false) } + + it 'renders error when api key is valid but user have disabled 2fa' do + disable_user_2fa + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.apikey_not_active\"]}") + expect(response.headers['Authorization']).to be_nil + end + end + + context 'testing api key with valid params' do + it 'catches api key headers and works as expected with milisecond nonce' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(200) + expect(response.body).to be_empty + expect(response.headers['Authorization']).to include "Bearer" + expect(response.headers['Authorization']).not_to be_nil + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(200) + end + end + + context 'testing restrictions' do + let(:do_restricted_request) { + put '/api/v2/auth/api/v2/peatio/management/ping', headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + } + + it 'receives access error if path is blacklisted' do + do_restricted_request + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.permission_denied\"]}") + end + + let(:do_whitelisted_request) { + put '/api/v2/auth/api/v2/peatio/public/ping', headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + } + + it 'receives access error if path is whitelisted' do + do_whitelisted_request + expect(response.status).to eq(200) + expect(response.body).to be_empty + expect(response.headers['Authorization']).to be_nil + end + end + end +end diff --git a/spec/api/v2/auth/csrf_spec.rb b/spec/api/v2/auth/csrf_spec.rb new file mode 100644 index 0000000..a70f08f --- /dev/null +++ b/spec/api/v2/auth/csrf_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '/api/v2/auth CSRF functionality test' do + include_context 'geoip mock' + + before do + allow_any_instance_of(Barong::Authorize).to receive(:validate_csrf!).and_call_original + end + let(:uri) { '/api/v2/identity/sessions' } + let!(:create_permissions) do + create :permission, role: 'admin' + create :permission, role: 'member', action: 'ACCEPT', verb: 'all', path: 'not_in_the_rules_path' + create :permission, role: 'member', action: 'ACCEPT', verb: 'get', path: '/api/v2/resource/users/me' + create :permission, role: 'accountant' + end + let!(:user) { create(:user) } + let(:params) do + { + email: user.email, + password: user.password + } + end + + let(:do_create_session_request) { post uri, params: params } + let(:auth_request) { '/api/v2/auth/not_in_the_rules_path' } + let(:protected_request) { '/api/v2/resource/users/me' } + + describe 'testing workability with session' do + context 'without CSRF token' do + before do + Rails.cache.delete('permissions') + do_create_session_request + end + + it 'doesnt return bearer token on valid session without CSRF' do + post auth_request + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + expect(response.body).to eq("{\"errors\":[\"authz.missing_csrf_token\"]}") + end + + it 'works with any type of changing state verb request' do + available_types = %w[post put delete patch] + available_types.each do |ping| + method("#{ping}").call auth_request + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + expect(response.body).to eq("{\"errors\":[\"authz.missing_csrf_token\"]}") + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(401) + end + end + + it 'works without CSRF token on any type of safe verb request' do + available_types = %w[get head] + available_types.each do |ping| + method("#{ping}").call auth_request + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(200) + end + end + end + + context 'with CSRF token' do + before do + Rails.cache.delete('permissions') + do_create_session_request + @csrf = json_body[:csrf_token] + end + + it 'doesnt return bearer token on valid session without CSRF' do + get auth_request, headers: { 'X-CSRF-Token': @csrf } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + end + + it 'doesnt work on any type of request without csrf' do + available_types = %w[post get put delete patch] + available_types.each do |ping| + method("#{ping}").call auth_request, headers: { 'X-CSRF-Token': @csrf } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(200) + end + end + end + end + + describe 'testing workability with api keys' do + let!(:test_user) { create(:user, otp: otp_enabled) } + let(:otp_enabled) { true } + let!(:api_key) { create :api_key, key_holder_account: test_user } + let(:otp_code) { '1357' } + let(:nonce) { (Time.now.to_f * 1000).to_i } + let(:kid) { api_key.kid } + let(:secret) { api_key.secret } + let(:data) { nonce.to_s + kid } + let(:algorithm) { 'SHA' + api_key.algorithm[2..4]} + let(:signature) { OpenSSL::HMAC.hexdigest(algorithm, secret, data) } + + before do + Rails.cache.delete('permissions') + SecretStorage.store_secret(secret, api_key.kid) + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, otp_code) { true } + end + + context 'with valid api keys' do + it 'works without CSRF' do + get auth_request, headers: { + 'X-Auth-Apikey' => kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(200) + expect(response.body).to be_empty + expect(response.headers['Authorization']).to include 'Bearer' + expect(response.headers['Authorization']).not_to be_nil + + get protected_request, headers: { 'Authorization' => response.headers['Authorization'] } + expect(response.status).to eq(200) + end + end + end +end diff --git a/spec/api/v2/auth/rbac_spec.rb b/spec/api/v2/auth/rbac_spec.rb new file mode 100644 index 0000000..45ea4a9 --- /dev/null +++ b/spec/api/v2/auth/rbac_spec.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '/api/v2/auth functionality test' do + include_context 'geoip mock' + + let(:do_protected_request) { get '/api/v2/auth/api/v2/resource/users/me' } + + describe 'testing rbac workability' do + before(:example) do + Thread.list.last.kill if Thread.list.last.to_s.include?('activity') + end + let!(:create_permissions) do + Permission.create(role: 'superadmin', action: 'ACCEPT', verb: 'get', path: 'api/v2/admin/users/list') + Rails.cache.delete('permissions') + @admin = User.create(email: 'superadmin@admin.io', password: 'Tecohvi0', role: 'superadmin', state: 'active') + end + + context 'with cookies' do + let(:do_create_session_request_superadm) { post '/api/v2/identity/sessions', params: { email: 'superadmin@admin.io', password: 'Tecohvi0' }} + + context 'not enough permissions' do + it 'denies access for user with missing cookies' do + do_protected_request + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_session\"]}") + end + + it 'denies access for non-accountant user with valid cookies trying to GET accountant api' do + do_create_session_request_superadm + + get '/api/v2/auth/api/v2/accountant/documents' + + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_permission\"]}") + expect(response.headers['Authorization']).to be_nil + end + + it 'denies POST for endpoint but allowing GET for admin user according to permissions' do + do_create_session_request_superadm + + post '/api/v2/auth/api/v2/admin/users/list' + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_permission\"]}") + expect(response.headers['Authorization']).to be_nil + + get '/api/v2/auth/api/v2/admin/users/list' + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + it 'denies access because of the typo in the path' do + do_create_session_request_superadm + get '/api/v2/auth/api/v2/admon/users/list' + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_permission\"]}") + end + end + + context 'enough permissions' do + it 'allowes access with for user with valid cookies, verb, role and path' do + do_create_session_request_superadm + + get '/api/v2/auth/api/v2/admin/users/list' + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + end + end + + context 'with api_keys' do + let!(:admin_api_key) { create :api_key, key_holder_account: @admin } + let(:nonce) { (Time.now.to_f * 1000).to_i } + let(:secret) { admin_api_key.secret } + let(:signature) { OpenSSL::HMAC.hexdigest('SHA256', secret, nonce.to_s + admin_api_key.kid ) } + let!(:turn_on_2fa) { @admin.update(otp: true) } + + context 'with valid api key headers' do + context 'enough permissions' do + it 'allowes access with for api key owner for valid verb, owner role and path' do + allow(TOTPService).to receive(:validate?) + .with(@admin.uid, '1357') { true } + + get '/api/v2/auth/api/v2/admin/users/list', headers: { + 'X-Auth-Apikey' => admin_api_key.kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(200) + + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + end + + context 'not enough permissions' do + it 'denies access for non-accountant api key owner with valid api_key trying to POST accountant api' do + allow(TOTPService).to receive(:validate?) + .with(@admin.uid, '1357') { true } + allow(SecretStorage).to receive(:get_secret) + .with(admin_api_key.kid) { Vault::Secret.new(data: { value: secret }) } + + post '/api/v2/auth/api/v2/accountant/documents', headers: { + 'X-Auth-Apikey' => admin_api_key.kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_permission\"]}") + end + + it 'denies POST for endpoint but allowing GET for api key owner according to permissions' do + allow(TOTPService).to receive(:validate?) + .with(@admin.uid, '1357') { true } + allow(SecretStorage).to receive(:get_secret) + .with(admin_api_key.kid) { Vault::Secret.new(data: { value: secret }) } + + post '/api/v2/auth/api/v2/accountant/documents', headers: { + 'X-Auth-Apikey' => admin_api_key.kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + Rails.cache.delete(admin_api_key.kid) + expect(response.status).to eq(401) + expect(response.body).to eq("{\"errors\":[\"authz.invalid_permission\"]}") + + get '/api/v2/auth/api/v2/admin/users/list', headers: { + 'X-Auth-Apikey' => admin_api_key.kid, + 'X-Auth-Nonce' => nonce, + 'X-Auth-Signature' => signature + } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + end + end + end + end +end diff --git a/spec/api/v2/auth/restriction_spec.rb b/spec/api/v2/auth/restriction_spec.rb new file mode 100644 index 0000000..fadf814 --- /dev/null +++ b/spec/api/v2/auth/restriction_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe '/api/v2/auth functionality test' do + include_context 'geoip mock' + + let(:uri) { '/api/v2/identity/sessions' } + let!(:create_permissions) do + create :permission, role: 'member', action: 'ACCEPT', verb: 'all', path: 'tasty_endpoint' + Rails.cache.delete('permissions') + end + let!(:user) { create(:user) } + let(:params) do + { + email: user.email, + password: user.password + } + end + let(:do_create_session_request) { post uri, params: params } + let(:auth_request) { '/api/v2/auth/tasty_endpoint' } + let(:auth_session_create_request) { '/api/v2/auth/api/v2/barong/identity/sessions' } + + + describe 'test blocklogin restriction' do + before do + allow(Rails.cache).to receive(:delete_matched).and_return(nil) + Rails.cache.delete('restrictions') + end + + context 'block session creation' do + let!(:restriction) { create(:restriction, value: 'EUROPE', scope: 'continent', category: 'blocklogin', code: 425) } + before do + allow_any_instance_of(Barong::Authorize).to receive(:validate_session!).and_return(true) + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip) + end + + it do + post auth_session_create_request, params: params + expect(response.status).to eq(restriction.code) + end + + it do + do_create_session_request # This request will be successful because path doesn't include 'api/v2/auth' + get auth_request + expect(response.status).to eq(200) + end + end + end + describe 'test blacklist restrictions' do + before do + allow_any_instance_of(Barong::Authorize).to receive(:validate_session!).and_return(true) + Rails.cache.delete('restrictions') + do_create_session_request + end + + context 'restrict by ip' do + let!(:restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'blacklist') } + let!(:disabled) { create(:restriction, value: '192.168.0.3', scope: 'ip', state: 'disabled', category: 'blacklist') } + + it 'request with restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.0.1') + get auth_request + + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + + it 'request with non-restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.0.2') + get auth_request + expect(response.status).to eq(200) + end + + it 'request with disabled restriction' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.0.3') + get auth_request + expect(response.status).to eq(200) + end + end + + context 'restricts with ip subnet' do + let!(:restriction) { create(:restriction, value: '192.168.32.0/24', scope: 'ip_subnet', category: 'blacklist') } + + it 'request with restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.32.42') + get auth_request + + expect(response.status).to eq(403) + expect(response.headers['Authorization']).to be_nil + expect(response.body).to eq("{\"errors\":[\"authz.restrict.blacklist\"]}") + end + + it 'request with non-restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.33.3') + get auth_request + + expect(response.status).to eq(200) + end + end + + context 'geoip' do + context 'restricts with country' do + let!(:restriction) { create(:restriction, value: 'japan', scope: 'country', category: 'blacklist') } + + it 'with restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(tokyo_ip) + get auth_request + expect(response.status).to eq(423) + expect(response.headers['Authorization']).to be_nil + expect(response.body).to eq("{\"errors\":[\"authz.restrict.blacklist\"]}") + end + + it 'with non-restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip) + get auth_request + expect(response.status).to eq(200) + end + end + + context 'restricts with continent' do + let!(:restriction) { create(:restriction, value: 'EUROPE', scope: 'continent', category: 'blacklist') } + + it 'with restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(london_ip) + get auth_request + expect(response.status).to eq(423) + expect(response.headers['Authorization']).to be_nil + expect(response.body).to eq("{\"errors\":[\"authz.restrict.blacklist\"]}") + end + + it 'with non-restricted ip' do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return(tokyo_ip) + get auth_request + expect(response.status).to eq(200) + end + end + end + end + + describe 'test restriction ierarchy' do + before do + allow_any_instance_of(Barong::Authorize).to receive(:validate_session!).and_return(true) + Rails.cache.delete('restrictions') + do_create_session_request + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.0.1') + end + + context 'whitelist -> maintenance' do + let!(:maintenance_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'maintenance') } + let!(:whitelist_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'whitelist') } + + it '200' do + get auth_request + + expect(response.status).to eq(200) + end + end + + + context 'maintenance -> blacklist' do + let!(:blacklist_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'blacklist') } + let!(:maintenance_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'maintenance') } + + context 'standard code error' do + it '471' do + get auth_request + + expect(response.status).to eq(471) + end + end + end + + context 'blacklist' do + context 'standard code error' do + let!(:blacklist_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'blacklist') } + + it '401' do + get auth_request + + expect(response.status).to eq(401) + end + end + + context 'custom code error' do + let!(:blacklist_restriction) { create(:restriction, value: '192.168.0.1', scope: 'ip', category: 'blacklist', code: 403) } + + it '403' do + get auth_request + + expect(response.status).to eq(403) + end + end + end + end +end diff --git a/spec/api/v2/auth/sessions_spec.rb b/spec/api/v2/auth/sessions_spec.rb new file mode 100644 index 0000000..cc99bd8 --- /dev/null +++ b/spec/api/v2/auth/sessions_spec.rb @@ -0,0 +1,158 @@ +# frozen_string_literal: true + +require 'spec_helper' +include ActiveSupport::Testing::TimeHelpers + +describe '/api/v2/auth functionality test' do + include_context 'geoip mock' + + let(:session_expire_time) do + Barong::App.config.session_expire_time.to_i.seconds + end + let!(:create_permissions) do + create :permission, role: 'admin' + create :permission, role: 'member', action: 'ACCEPT', verb: 'all', path: 'not_in_the_rules_path' + create :permission, role: 'member', action: 'ACCEPT', verb: 'get', path: '/api/v2/resource/users/me' + create :permission, role: 'accountant' + end + let!(:user) { create(:user) } + let(:params) do + { + email: user.email, + password: user.password + } + end + + let(:do_destroy_session_request) { delete '/api/v2/identity/sessions', headers: { 'HTTP_USER_AGENT': 'legacy-browser' } } + let(:do_create_session_request) { post '/api/v2/identity/sessions', params: params, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } } + let(:auth_request) { '/api/v2/auth/not_in_the_rules_path' } + + describe 'testing session hash validations' do + before do + Rails.cache.delete('permissions') + end + + context 'with valid ip, browser' do + it 'authorize traffic' do + do_create_session_request + expect(response.status).to eq(200) + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + end + + context 'when session params has changed after session opening' do + it 'return error if USER_AGENT changes' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'new-browser' } + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + + it 'return error if IP changes' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'REMOTE_ADDR': '128.0.0.12' } + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + + it 'return error if everything changes' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'REMOTE_ADDR': '128.0.0.12', 'HTTP_USER_AGENT': 'new-browser' } + expect(response.status).to eq(401) + expect(response.headers['Authorization']).to be_nil + end + end + end + + describe 'testing session renewal' do + before do + Rails.cache.delete('permissions') + end + + context 'with valid session' do + it 'authorize traffic and renew session every request' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + start_time = Time.current + + 30.times do + # 5 minute before session will expire + travel session_expire_time - 5.minutes + + # renew session with private request + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + end + + expect(Time.current).to be > (start_time + session_expire_time) + + travel session_expire_time + 10.minutes + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + expect(response.status).to eq(401) + end + end + end + + describe 'testing session destroy' do + before do + Rails.cache.delete('permissions') + end + + context 'with valid session' do + it 'deletes session from cache on #logout' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + + do_destroy_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + expect(response.status).to eq(401) + end + end + + context 'with invalid session params' do + it 'deletes session from cache on auth if params is wrong' do + do_create_session_request + expect(response.status).to eq(200) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + + expect(response.status).to eq(200) + expect(response.headers['Authorization']).not_to be_nil + expect(response.headers['Authorization']).to include "Bearer" + + post auth_request, headers: { 'HTTP_USER_AGENT': 'new-browser' } + expect(response.status).to eq(401) + + post auth_request, headers: { 'HTTP_USER_AGENT': 'legacy-browser' } + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/api/v2/cors/cors_spec.rb b/spec/api/v2/cors/cors_spec.rb new file mode 100644 index 0000000..95caa51 --- /dev/null +++ b/spec/api/v2/cors/cors_spec.rb @@ -0,0 +1,141 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +require 'rack/cors' +require 'env-tweaks' + +describe Rack::Cors, type: :request do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:member) { create(:member, :level_3) } + let(:frontend_url) { 'https://frontend.io' } + let(:local_url) { 'http://localhost:3000' } + let(:token) { jwt_for(member) } + + let(:app) { + Rack::Builder.new do + use Rack::Cors do + allow do + origins Barong::CORS::Validations.validate_origins(ENV['API_CORS_ORIGINS']) + resource '/api/*', + methods: %i[get post delete put patch options head], + headers: :any, + credentials: ENV.true?('API_CORS_ALLOW_CREDENTIALS'), + max_age: Barong::CORS::Validations.validate_max_age(ENV['API_CORS_MAX_AGE']) + end + end + run Rails.application + end + } + + def check_cors(response, origin, allow_crendentails, max_age = '3600') + expect(response.headers['Access-Control-Allow-Origin']).to eq(origin) + expect(response.headers['Access-Control-Allow-Methods']).to eq('GET, POST, DELETE, PUT, PATCH, OPTIONS, HEAD') + expect(response.headers['Access-Control-Allow-Credentials']).to eq(allow_crendentails) + expect(response.headers['Access-Control-Max-Age']).to eq(max_age) + end + + def without_cors(response) + expect(response.headers['Access-Control-Allow-Origin']).to eq(nil) + expect(response.headers['Access-Control-Allow-Methods']).to eq(nil) + expect(response.headers['Access-Control-Allow-Credentials']).to eq(nil) + expect(response.headers['Access-Control-Max-Age']).to eq(nil) + end + + context 'set API_CORS_ORIGINS as "*"' do + let(:origin) { '*' } + let(:allow_crendentails) { nil } + let(:max_age) { '3600' } + + before do + ENV['API_CORS_ORIGINS'] = origin + ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails + ENV['API_CORS_MAX_AGE'] = max_age + end + + after do + ENV['API_CORS_ORIGINS'] = nil + ENV['API_CORS_ALLOW_CREDENTIALS'] = nil + ENV['API_CORS_MAX_AGE'] = nil + end + + it 'sends CORS headers when requesting using GET from frontend url' do + get '/api/v2/identity/ping', headers: { 'origin' => frontend_url } + expect(response).to be_successful + check_cors(response, '*', allow_crendentails, max_age) + end + + it 'sends CORS headers when requesting using GET from localhost' do + get '/api/v2/identity/ping', headers: { 'origin' => local_url } + expect(response).to be_successful + check_cors(response, '*', allow_crendentails, max_age) + end + end + + context 'set multiple API_CORS_ORIGINS for frontend and localhost' do + let(:allow_crendentails) { 'true' } + let(:max_age) { '6200' } + + before do + ENV['API_CORS_ORIGINS'] = "#{frontend_url},#{local_url}" + ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails + ENV['API_CORS_MAX_AGE'] = max_age + end + + after do + ENV['API_CORS_ORIGINS'] = nil + ENV['API_CORS_ALLOW_CREDENTIALS'] = nil + ENV['API_CORS_MAX_AGE'] = nil + end + + it 'sends CORS headers when requesting using GET from frontend url' do + get '/api/v2/identity/ping', headers: { 'Origin' => frontend_url } + expect(response).to be_successful + check_cors(response, frontend_url, allow_crendentails, max_age) + end + + it 'sends CORS headers when requesting using GET from localhost' do + get '/api/v2/identity/ping', headers: { 'Origin' => local_url } + expect(response).to be_successful + check_cors(response, local_url, allow_crendentails, max_age) + end + + it 'doesn\'t sends CORS headers when requesting using GET from unkown domain' do + get '/api/v2/identity/ping', headers: { 'Origin' => 'http://domain.com' } + expect(response).to be_successful + without_cors(response) + end + end + + context 'send invalid request' do + let(:allow_crendentails) { 'true' } + let(:max_age) { '3600' } + + before do + ENV['API_CORS_ORIGINS'] = "#{frontend_url},#{local_url}" + ENV['API_CORS_ALLOW_CREDENTIALS'] = allow_crendentails + ENV['API_CORS_MAX_AGE'] = max_age + end + + after do + ENV['API_CORS_ORIGINS'] = nil + ENV['API_CORS_ALLOW_CREDENTIALS'] = nil + ENV['API_CORS_MAX_AGE'] = nil + end + + it 'sends CORS headers ever when user is not authenticated' do + get '/api/v2/resource/users/me', headers: { 'Origin' => local_url } + expect(response).to have_http_status 401 + check_cors(response, local_url, allow_crendentails) + end + + it 'sends CORS headers when invalid parameter supplied' do + get '/api/v2/resource/users/activity/topic', headers: auth_header.merge!('Origin' => local_url ) + expect(response).to have_http_status 422 + check_cors(response, local_url, allow_crendentails) + end + end +end diff --git a/spec/api/v2/cors/validations_spec.rb b/spec/api/v2/cors/validations_spec.rb new file mode 100644 index 0000000..2f40789 --- /dev/null +++ b/spec/api/v2/cors/validations_spec.rb @@ -0,0 +1,82 @@ +# encoding: UTF-8 +# frozen_string_literal: true + +describe Barong::CORS::Validations do + describe 'validate origins' do + subject { Barong::CORS::Validations.validate_origins(ENV['API_CORS_ORIGINS']) } + + context 'set API_CORS_ORIGINS as "*"' do + before do + ENV['API_CORS_ORIGINS'] = '*' + end + + it do + is_expected.to eq('*') + end + end + + context 'set mulitple API_CORS_ORIGINS with "*"' do + before do + ENV['API_CORS_ORIGINS'] = 'https://localhost,*,https://domain.com' + end + + it do + is_expected.to eq('*') + end + end + + context 'set multiple API_CORS_ORIGINS' do + before do + ENV['API_CORS_ORIGINS'] = 'https://localhost,https://domain.com' + end + + it do + is_expected.to eq(['https://localhost','https://domain.com']) + end + end + + context 'set invalid domain into API_CORS_ORIGINS' do + before do + ENV['API_CORS_ORIGINS'] = 'htt:://localhost' + end + + it do + expect { subject }.to raise_error(Barong::CORS::Validations::Error) + end + end + end + + describe 'validate max age' do + subject { Barong::CORS::Validations.validate_max_age(ENV['API_CORS_MAX_AGE']) } + + context 'set API_CORS_MAX_AGE as "6200"' do + before do + ENV['API_CORS_MAX_AGE'] = '6200' + end + + it do + is_expected.to eq('6200') + end + end + + context 'set API_CORS_MAX_AGE as "6200.1"' do + before do + ENV['API_CORS_MAX_AGE'] = '6200.1' + end + + it do + is_expected.to eq('3600') + end + end + + context 'doesn\'t set API_CORS_MAX_AGE"' do + before do + ENV['API_CORS_MAX_AGE'] = nil + end + + it do + is_expected.to eq('3600') + end + end + end +end diff --git a/spec/api/v2/entities/activities_spec.rb b/spec/api/v2/entities/activities_spec.rb new file mode 100644 index 0000000..3b94f9b --- /dev/null +++ b/spec/api/v2/entities/activities_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +describe API::V2::Entities::Activity do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:record) { create(:activity, topic: 'otp', result: 'succeed', action: 'login') } + + subject { OpenStruct.new API::V2::Entities::Activity.represent(record).serializable_hash } + + it { expect(subject.topic).to eq record.topic } + it { expect(subject.result).to eq record.result } + it { expect(subject.user_ip).to eq record.user_ip } + it { expect(subject.action).to eq record.action } + it { expect(subject.data).to eq record.data } + it { expect(subject.user_agent).to eq record.user_agent } + + it { expect(subject.created_at).to eq record.created_at.iso8601 } +end diff --git a/spec/api/v2/entities/labels_spec.rb b/spec/api/v2/entities/labels_spec.rb new file mode 100644 index 0000000..01072ba --- /dev/null +++ b/spec/api/v2/entities/labels_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +describe API::V2::Entities::Label do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:record) { create(:label) } + + subject { OpenStruct.new API::V2::Entities::Label.represent(record).serializable_hash } + + it { expect(subject.key).to eq record.key } + it { expect(subject.value).to eq record.value } + it { expect(subject.scope).to eq record.scope } + it { expect(subject.created_at).to eq record.created_at.iso8601 } + it { expect(subject.updated_at).to eq record.updated_at&.iso8601 } +end diff --git a/spec/api/v2/entities/user_with_full_spec.rb b/spec/api/v2/entities/user_with_full_spec.rb new file mode 100644 index 0000000..5a2584d --- /dev/null +++ b/spec/api/v2/entities/user_with_full_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +describe API::V2::Entities::UserWithKYC do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:record) { create(:user) } + + subject { OpenStruct.new API::V2::Entities::UserWithKYC.represent(record).serializable_hash } + + it { expect(subject.email).to eq record.email } + it { expect(subject.uid).to eq record.uid } + it { expect(subject.role).to eq record.role } + it { expect(subject.level).to eq record.level } + it { expect(subject.otp).to eq record.otp } + it { expect(subject.state).to eq record.state } + it { expect(subject.profiles).to eq record.profiles } + it { expect(subject.labels).to eq record.labels } + it { expect(subject.phones).to eq record.phones } + it { expect(subject.documents).to eq record.documents } + it { expect(subject.to_h.keys).not_to include(:activities) } + + it { expect(subject.created_at).to eq record.created_at.iso8601 } + it { expect(subject.updated_at).to eq record.updated_at&.iso8601 } +end + diff --git a/spec/api/v2/identity/general_spec.rb b/spec/api/v2/identity/general_spec.rb new file mode 100644 index 0000000..49574f0 --- /dev/null +++ b/spec/api/v2/identity/general_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::V2::Identity::General do + describe 'GET /api/v2/identity/time' do + it 'returns a server status' do + get '/api/v2/identity/ping' + expect_status_to_eq(200) + expect(json_body[:ping]).to eq('pong') + end + end + + describe 'GET /api/v2/identity/time' do + it 'returns a current UNIX time' do + get '/api/v2/identity/time' + expect_status_to_eq(200) + expect(json_body[:time].to_i).to be <= Time.now.to_i + end + end + + describe 'GET /api/v2/identity/configs' do + it 'returns some of the configurations' do + get '/api/v2/identity/configs' + expect_status_to_eq(200) + expect(json_body[:session_expire_time]).to eq(Barong::App.config.session_expire_time) + expect(json_body[:captcha_type]).to eq(Barong::App.config.captcha) + expect(json_body[:phone_verification_type]).to eq(Barong::App.config.phone_verification) + end + + it 'returns all of the configurations with defaults' do + get '/api/v2/identity/configs' + expect_status_to_eq(200) + expect(json_body[:session_expire_time]).to eq(Barong::App.config.session_expire_time) + expect(json_body[:captcha_type]).to eq(Barong::App.config.captcha) + expect(json_body[:phone_verification_type]).to eq(Barong::App.config.phone_verification) + expect(json_body[:password_min_entropy]).to eq(Barong::App.config.password_min_entropy) + expect(json_body[:password_regexp]).to eq(Barong::App.config.password_regexp.to_s) + end + end +end diff --git a/spec/api/v2/identity/sessions_spec.rb b/spec/api/v2/identity/sessions_spec.rb new file mode 100644 index 0000000..ba9fb29 --- /dev/null +++ b/spec/api/v2/identity/sessions_spec.rb @@ -0,0 +1,462 @@ +# frozen_string_literal: true + +describe API::V2::Identity::Sessions do + include_context 'geoip mock' + + include ActiveSupport::Testing::TimeHelpers + let!(:create_member_permission) do + create :permission, + role: 'member', + verb: 'all' + end + before do + Rails.cache.delete('permissions') + allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') + end + + describe 'POST /api/v2/identity/sessions' do + before { allow(Barong::App.config).to receive_messages(captcha: 'none') } + + let!(:email) { 'user@gmail.com' } + let!(:password) { 'testPassword111' } + let(:uri) { '/api/v2/identity/sessions' } + subject!(:user) do + create :user, + :with_profile, + email: email, + password: password, + password_confirmation: password + end + let(:otp_enabled) { false } + + context 'With valid params' do + let(:do_request) { post uri, params: params } + let(:session_expire_time) do + Barong::App.config.session_expire_time + end + let(:check_session) do + get '/api/v2/auth/api/v2/tasty_endpoint' + end + let(:params) do + { + email: email, + password: password + } + end + + context 'captcha behaviour when captcha policy is recaptcha' do + before { allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') } + + it 'doesnt require captcha if endpoint is not in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create"]}) + do_request + + expect_status_to_eq 200 + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq user.profiles.first.sub_masked_last_name + expect(result['profiles'][0]['dob']).to eq user.profiles.first.sub_masked_dob + end + + it 'require captcha if endpoint is in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create"]}) + + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + + it 'Check current credentials and returns session' do + do_request + + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(user.uid) + expect_status.to eq(200) + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq user.profiles.first.sub_masked_last_name + expect(result['profiles'][0]['dob']).to eq user.profiles.first.sub_masked_dob + + check_session + expect(response.status).to eq(200) + end + + it 'Expires a session after configured time' do + do_request + travel session_expire_time + 30.minutes + check_session + expect(response.status).to eq(401) + end + + let(:captcha_response) { nil } + let(:valid_response) { 'valid' } + let(:invalid_response) { 'invalid' } + + before do + allow_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) + .with(model: user, + skip_remote_ip: true, + response: valid_response) { true } + + allow_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) + .with(model: user, + skip_remote_ip: true, + response: invalid_response) { raise StandardError } + end + + context 'when captcha response is blank' do + let(:params) do + { + email: email, + password: password, + captcha_response: captcha_response + } + end + end + + context 'when captcha response is not valid' do + let(:params) do + { + email: email, + password: password, + captcha_response: invalid_response + } + end + + before do + expect_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) { false } + end + + it 'renders an error' do + allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') + do_request + expect(json_body[:errors]).to eq(["identity.captcha.verification_failed"]) + expect_status_to_eq 422 + end + end + + context 'when captcha response is valid' do + let(:params) do + { + email: email, + password: password, + captcha_response: valid_response + } + end + + before do + expect_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) { true } + end + end + end + + context 'With Invalid params' do + context 'Checks current credentials and returns error' do + it 'when email, password is missing' do + post uri + expect_body.to eq(errors: ["identity.session.missing_email", "identity.session.missing_password"]) + expect(response.status).to eq(422) + end + + it 'when password is missing' do + post uri, params: { email: email } + expect_body.to eq(errors: ["identity.session.missing_password"]) + expect(response.status).to eq(422) + end + + it 'when email is missing' do + post uri, params: { password: password } + expect_body.to eq(errors: ["identity.session.missing_email"]) + expect(response.status).to eq(422) + end + + it 'when email is blank' do + post uri, params: { email: '',password: password } + expect_body.to eq(errors: ["identity.session.invalid_params"]) + expect(response.status).to eq(401) + end + + it 'when password is blank' do + post uri, params: { email: email, password: '' } + expect_body.to eq(errors: ["identity.session.invalid_params"]) + expect(response.status).to eq(401) + end + + it 'when email is space' do + post uri, params: { email: ' ',password: password } + expect_body.to eq(errors: ["identity.session.invalid_params"]) + expect(response.status).to eq(401) + end + + it 'when password is space' do + post uri, params: { email: email, password: ' ' } + expect_body.to eq(errors: ["identity.session.invalid_params"]) + expect(response.status).to eq(401) + end + + context 'when Password is wrong' do + it 'returns errror' do + post uri, params: { email: email, password: 'password' } + expect_body.to eq(errors: ["identity.session.invalid_params"]) + expect(response.status).to eq(401) + end + end + end + end + + context 'User state related errors' do + context 'When user is banned' do + let!(:banned_email) { 'email@random.com' } + let!(:user_banned) do + create :user, + email: banned_email, + password: password, + password_confirmation: password, + state: 'banned' + end + + it 'returns error on banned user' do + post uri, params: { email: banned_email, password: password } + expect_body.to eq(errors: ["identity.session.banned"]) + expect(response.status).to eq(401) + end + end + + let!(:pending_email) { 'pendingemail@random.com' } + let!(:user_pending) do + create :user, + email: pending_email, + password: password, + password_confirmation: password, + state: 'pending' + end + + context 'Allow pending user to login by default' do + it 'returns error on non-active user' do + user_pending.update(state: 'not-active') + post uri, params: { email: pending_email, password: password } + expect_body.to eq(errors: ["identity.session.not_active"]) + expect(response.status).to eq(401) + end + + it 'sucessfull login for pending user' do + user_pending.update(state: 'pending') + expect(user_pending.state).to eq('pending') + + post uri, params: { email: pending_email, password: password } + expect(response.status).to eq(200) + end + end + end + + context 'event API behavior' do + before do + allow(EventAPI).to receive(:notify) + end + + it 'receive system.session.create notify' do + allow_any_instance_of(API::V2::Utils).to receive(:remote_ip).and_return('192.168.0.1') + post uri, params: { email: email, password: password }, headers: { 'HTTP_USER_AGENT' => 'random-browser' } + + expect(EventAPI).to have_received(:notify).with('system.session.create', + hash_including({ record: hash_including(user: anything, user_ip: '192.168.0.1', user_agent: 'random-browser') }) + ) + end + end + end + + describe 'DELETE /api/v2/identity/sessions' do + before { allow(Barong::App.config).to receive_messages(captcha: 'none') } + + let!(:email) { 'user@gmail.com' } + let!(:password) { 'testPassword111' } + let(:uri) { '/api/v2/identity/sessions' } + let(:params) do + { + email: email, + password: password + } + end + subject!(:user) do + create :user, + email: email, + password: password, + password_confirmation: password + end + + context 'With invalid session' do + let(:do_create_session_request) { post uri, params: params } + let(:do_delete_session_request) { delete uri } + + + it 'receives 404 on delete session' do + do_delete_session_request + expect(response.status).to eq(404) + expect(response.body).to eq("{\"errors\":[\"identity.session.not_found\"]}") + end + end + + context 'With valid session' do + let(:do_create_session_request) { post uri, params: params } + let(:do_delete_session_request) { delete uri } + + it 'Deletes session' do + do_create_session_request + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(user.uid) + + do_delete_session_request + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(nil) + end + + it "return invalid set-cookie header on #logout" do + do_create_session_request + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(user.uid) + + do_delete_session_request + expect(response.status).to eq(200) + expect(response.headers['Set-Cookie']).not_to be_nil + expect(response.headers['Set-Cookie']).to include "barong_session" + end + end + end + + # describe 'POST /api/v2/indentity/sessions/auth0' do + # let(:uri) { '/api/v2/identity/sessions/auth0' } + # + # context 'user doesnt exist' do + # context 'email verified' do + # let(:payload) do + # [ + # { + # 'email': 'example@barong.io', + # 'email_verified': true, + # 'iss': 'https://domain.name/', + # 'sub': 'google-oauth2|100484476630231723', + # 'aud': 'test audience', + # 'iat': Time.now.to_i, + # 'exp': (Time.now + 5.minutes).to_i + # }.with_indifferent_access, + # { + # 'alg': 'RS256', + # 'typ': 'JWT', + # 'kid': 'ptd2123vE-G12GoDvJ8MQ' + # } + # ] + # end + # + # before do + # allow(Barong::Auth0::JWT).to receive(:verify).and_return(payload) + # end + # + # it 'create user and label' do + # expect(User.find_by(email: 'example@barong.io')).to eq nil + # post uri, params: { id_token: 'TestToken' } + # + # expect(response.status).to eq(201) + # result = JSON.parse(response.body) + # user = User.find_by(email: result['email']) + # expect(user).not_to be nil + # expect(user.level).to eq 1 + # expect(user.state).to eq 'active' + # expect(user.labels.count).to eq 1 + # expect(user.labels.find_by(key: 'email').value).to eq 'verified' + # end + # end + # end + # + # context 'user exists' do + # let(:payload) do + # [ + # { + # 'email': 'example@barong.io', + # 'email_verified': true, + # 'iss': 'https://domain.name/', + # 'sub': 'google-oauth2|100484476630231723', + # 'aud': 'test audience', + # 'iat': Time.now.to_i, + # 'exp': (Time.now + 5.minutes).to_i + # }.with_indifferent_access, + # { + # 'alg': 'RS256', + # 'typ': 'JWT', + # 'kid': 'ptd2123vE-G12GoDvJ8MQ' + # } + # ] + # end + # + # before do + # allow(Barong::Auth0::JWT).to receive(:verify).and_return(payload) + # end + # + # let!(:user) { create(:user, email: 'example@barong.io')} + # + # it 'returns existing user with session' do + # expect(User.find_by(email: 'example@barong.io')).not_to eq nil + # post uri, params: { id_token: 'TestToken' } + # + # expect(response.status).to eq(201) + # result = JSON.parse(response.body) + # expect(result['email']).to eq user.email + # expect(result.keys).to match_array(['email','uid','role','level','otp','state','referral_uid','csrf_token','data','labels','phones','profiles','data_storages', 'created_at', 'updated_at']) + # end + # end + # + # context 'invalid params' do + # it 'without params' do + # post uri + # expect_body.to eq(errors: ['identity.session.missing_id_token', 'identity.session.empty_id_token']) + # expect(response.status).to eq(422) + # end + # + # it 'with empty param' do + # post uri, params: { id_token: '' } + # expect_body.to eq(errors: ['identity.session.empty_id_token']) + # expect(response.status).to eq(422) + # end + # + # context 'jwt expired' do + # before do + # allow(Barong::Auth0::JWT).to receive(:verify).and_raise(JWT::ExpiredSignature) + # end + # + # it 'raise an error' do + # post uri, params: { id_token: 'TestToken' } + # expect_body.to eq(errors: ['identity.session.auth0.invalid_params']) + # expect(response.status).to eq(422) + # end + # end + # + # context 'email is not verified' do + # let(:payload) do + # [ + # { + # 'email': 'example@barong.io', + # 'email_verified': false, + # 'iss': 'https://domain.name/', + # 'sub': 'google-oauth2|100484476630231723', + # 'aud': 'test audience', + # 'iat': Time.now.to_i, + # 'exp': (Time.now + 5.minutes).to_i + # }.with_indifferent_access, + # { + # 'alg': 'RS256', + # 'typ': 'JWT', + # 'kid': 'ptd2123vE-G12GoDvJ8MQ' + # } + # ] + # end + # + # before do + # allow(Barong::Auth0::JWT).to receive(:verify).and_return(payload) + # end + # + # it 'doesnt create user and label' do + # expect(User.find_by(email: 'example@barong.io')).to eq nil + # post uri, params: { id_token: 'TestToken' } + # + # expect(User.find_by(email: 'example@barong.io')).to eq nil + # expect_body.to eq(errors: ['identity.session.auth0.invalid_params']) + # expect(response.status).to eq(401) + # end + # end + # end + # end +end diff --git a/spec/api/v2/identity/users_spec.rb b/spec/api/v2/identity/users_spec.rb new file mode 100644 index 0000000..275f1f7 --- /dev/null +++ b/spec/api/v2/identity/users_spec.rb @@ -0,0 +1,717 @@ +# frozen_string_literal: true + +require 'spec_helper' +include ActiveSupport::Testing::TimeHelpers + +describe API::V2::Identity::Users do + include_context 'geoip mock' + + before do + allow(Barong::App.config).to receive_messages(first_registration_superadmin: false) + end + + let!(:create_member_permission) do + create :permission, + role: 'member', + verb: 'all' + create :permission, + role: 'superadmin', + verb: 'all' + create :permission, + role: 'member', + verb: 'all', + path: 'tasty_endpoint' + end + + describe 'POST /api/v2/identity/users/access' do + context 'success' do + before do + allow_any_instance_of(ActionDispatch::Request).to receive(:remote_ip).and_return('192.168.0.2') + allow(Rails.cache).to receive(:read).and_return('active') + end + + it 'creates a restriction in database with my ip' do + expect { + post '/api/v2/identity/users/access', params: { whitelink_token: 'testtoken' } + }.to change { Restriction.count }.by(1) + + expect(response.status).to eq(201) + end + + it 'works only first time' do + expect { + post '/api/v2/identity/users/access', params: { whitelink_token: 'testtoken' } + }.to change { Restriction.count }.by(1) + expect(response.status).to eq(201) + + post '/api/v2/identity/users/access', params: { whitelink_token: 'testtoken' } + + expect(response.status).to eq(422) + expect(json_body[:errors]).to include "value.taken" + end + end + + context 'returns error' do + it 'if token is missing' do + post '/api/v2/identity/users/access' + + expect(response.status).to eq(422) + expect(json_body[:errors]).to include "identity.user.missing_whitelink_token" + end + + it 'if incorrect whitelink_token' do + post '/api/v2/identity/users/access', params: { whitelink_token: 'testtoken' } + + expect(response.status).to eq(422) + expect(json_body[:errors]).to include "identity.user.access.invalid_token" + end + end + end + + describe 'POST /api/v2/identity/users with default Barong::App.config.captcha' do + let(:do_request) { post '/api/v2/identity/users', params: params } + + context 'when email is invalid' do + let(:params) { { email: 'bad_format', password: 'Password1' } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["email.invalid", "password.weak"]) + end + end + + context 'when referral is unexist' do + let(:params) { { email: 'valid.email@gmail.com', password: 'Password1', refid: 'ID1231231231' } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.referral_doesnt_exist"]) + end + end + + context 'when referral id is invalid' do + let(:params) { { email: 'valid.email@gmail.com', password: 'Password1', refid: 'UID123' } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.invalid_referral_format"]) + end + end + + context 'when Password is invalid' do + let(:params) { { email: 'vadid.email@gmail.com', password: 'password' } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["password.requirements"]) + end + end + + context 'when email and password are absent' do + let(:params) {} + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.missing_email", "identity.user.empty_email", "identity.user.missing_password", "identity.user.empty_password"]) + end + end + + context 'when email is blank' do + let(:params) { { email: '', password: 'zieV0Kai' } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.empty_email"]) + end + end + + context 'when email is valid' do + let(:params) { { email: 'valid.email@gmail.com', password: 'eeC2BiCucxWEQ' } } + + it 'creates an account' do + do_request + expect_status_to_eq 201 + end + + context 'first user registration' do + before do + allow(Barong::App.config).to receive_messages(first_registration_superadmin: true) + end + + it 'creates superadmin user' do + post '/api/v2/identity/users', params: params + + expect(response.status).to eq(201) + + expect(json_body.keys).to match_array %i[email uid role level otp state referral_uid csrf_token data created_at updated_at labels phones profiles data_storages] + expect(json_body[:email]).to eq 'valid.email@gmail.com' + expect(json_body[:level]).to eq 1 + expect(json_body[:role]).to eq 'superadmin' + expect(json_body[:state]).to eq 'active' + expect(json_body[:labels].count).to eq 1 + expect(json_body[:labels][0][:key]).to eq 'email' + expect(json_body[:labels][0][:value]).to eq 'verified' + expect(json_body[:labels][0][:scope]).to eq 'private' + end + end + end + end + + describe 'POST /api/v2/identity/users with reCAPTCHA Barong::App.config.captcha' do + before { allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') } + + + let(:do_request_with_captcha) { post '/api/v2/identity/users', params: params_with_captcha } + let(:params_with_captcha) { { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ', captcha_response: 'response' } } + let(:do_request) { post '/api/v2/identity/users', params: params } + let(:params) { { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ' } } + + context 'when reCAPTCHA is valid' do + + it 'creates an account' do + allow_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) { true } + + do_request_with_captcha + expect_status_to_eq 201 + end + + it 'doesnt require captcha if endpoint is not in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["session_create"]}) + + do_request + expect_status_to_eq 201 + end + + it 'doesnt require captcha if protection list is empty' do + allow(BarongConfig).to receive(:list).and_return({}) + + do_request + expect_status_to_eq 201 + end + + it 'require captcha if endpoint is in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create"]}) + + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + + context 'when reCAPTCHA is invalid' do + before { allow_any_instance_of(CaptchaService::RecaptchaVerifier).to receive(:verify_recaptcha) { false } } + + it 'renders an error' do + do_request_with_captcha + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.captcha.verification_failed"]) + end + end + + context 'when captcha_response is blank but Barong::App.config.captcha requires reCAPTCHA response' do + let(:params) { { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ' } } + + it 'renders an error' do + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + end + + describe 'POST /api/v2/identity/users with GeeTest Barong::App.config.captcha' do + before { allow(Barong::App.config).to receive_messages(captcha: 'geetest') } + + let(:do_request) { post '/api/v2/identity/users', params: params } + let(:params) do + { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ', + captcha_response: { geetest_challenge: 'challenge', + geetest_validate: 'validate', + geetest_seccode: 'seccode' } } + end + + context 'when GeeTest is valid' do + before { allow_any_instance_of(CaptchaService::GeetestVerifier).to receive(:validate) { true } } + + it 'creates an account' do + do_request + expect_status_to_eq 201 + end + end + + context 'when GeeTest is invalid' do + before { allow_any_instance_of(CaptchaService::GeetestVerifier).to receive(:validate) { false } } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.captcha.verification_failed"]) + end + end + + context 'when captcha_response is blank but Barong::App.config.captcha requires Geetest response' do + let(:params) { { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ' } } + + it 'renders an error' do + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + + context 'when captcha_response has incorrect format' do + let(:params) do + { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ', + captcha_response: { empty: 'string' } } + end + + it 'renders an error' do + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.mandatory_fields"]) + end + end + end + + describe 'POST /api/v2/identity/users with data field' do + let(:do_request) { post '/api/v2/identity/users', params: params } + let(:params) do + { email: 'vadid.email@gmail.com', password: 'eeC2BiCucxWEQ', + data: data } + end + + context 'when data is not json compatible' do + let(:data) { 'phone_number: 380969999999' } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["data.invalid_format"]) + end + end + + context 'valid data' do + let(:data) { "{\"phone_number\":\"380969999999\"}" } + + it 'creates user' do + do_request + expect_status_to_eq 201 + end + end + end + + describe 'session opening on /api/v2/identity/users' do + before do + Rails.cache.delete('permissions') + end + + let(:email) { 'valid@email.com' } + let(:do_request) { post '/api/v2/identity/users', params: params } + let(:params) { { email: email, password: 'Tecohvi0' } } + let(:session_expire_time) do + Barong::App.config.session_expire_time + end + let(:check_session) do + get '/api/v2/auth/tasty_endpoint' + end + + it 'Check current credentials and returns session' do + do_request + user = User.find_by(email: email) + + expect(user).not_to be(nil) + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(user.uid) + expect_status.to eq(201) + + check_session + expect(response.status).to eq(200) + end + end + + describe 'POST /api/v2/identity/users/email/generate_code' do + before { allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create"]}) } + let(:params) { { email: 'invalid@email.com' } } + let(:do_request) { post '/api/v2/identity/users/email/generate_code', params: params } + + context 'when user is invalid' do + it 'doesnt render an error to prevent user enumeration' do + do_request + expect_status_to_eq 201 + end + end + + let(:params) { { email: 'valid-confirmed@email.com' } } + context 'when user is valid, email confirmed' do + it 'doesnt render an error to prevent user enumeration' do + create(:user, email: 'valid-confirmed@email.com', state: 'active') + do_request + expect_status_to_eq 201 + end + end + + context 'when user is valid' do + let(:user) { create(:user, state: 'pending') } + let(:params) { { email: user.email } } + it 'returns a success' do + do_request + expect_status_to_eq 201 + end + end + + context 'captcha behaviour when captcha policy is recaptcha' do + let(:user) { create(:user, state: 'pending') } + let(:params) { { email: user.email } } + before { allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') } + + it 'doesnt require captcha if endpoint is not in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create"]}) + + do_request + expect_status_to_eq 201 + end + + it 'require captcha if endpoint is in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create", "email_confirmation"]}) + + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + end + + describe 'session opening on /api/v2/identity/users/email/confirm_code' do + before do + Rails.cache.delete('permissions') + end + + let(:user) { create(:user, state: 'pending', email: 'valid_email@email.com') } + let(:do_request) { post '/api/v2/identity/users/email/confirm_code', params: params } + let(:params) { { token: codec.encode(sub: 'confirmation', email: user.email, uid: user.uid) } } + let(:session_expire_time) do + Barong::App.config.session_expire_time + end + let(:check_session) do + get '/api/v2/auth/tasty_endpoint' + end + + it 'Gives label email verified and opens a session' do + do_request + + expect(user).not_to be(nil) + expect(session.instance_variable_get(:@delegate)[:uid]).to eq(user.uid) + expect_status.to eq(201) + + check_session + expect(response.status).to eq(200) + end + end + + describe 'POST /api/v2/identity/users/email/confirm_code' do + let(:do_request) { post '/api/v2/identity/users/email/confirm_code', params: params } + let(:params) { {} } + + context 'when token is missing' do + it 'returns an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.missing_token", "identity.user.empty_token"]) + end + end + + context 'when token is invalid' do + let(:params) { { token: 'invalid token' } } + + it 'returns an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["jwt.decode_and_verify.segments"]) + end + end + + context 'when token is valid' do + let(:user) { create(:user, :with_profile, state: 'pending', email: 'valid_email@email.com') } + let(:params) { { token: codec.encode(sub: 'confirmation', email: user.email, uid: user.uid) } } + it 'updates state to active' do + do_request + expect_status_to_eq 201 + + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq user.profiles.first.sub_masked_last_name + expect(result['profiles'][0]['dob']).to eq user.profiles.first.sub_masked_dob + end + + it 'returns utilized on the second attempt' do + token_params = params + post '/api/v2/identity/users/email/confirm_code', params: token_params + expect_status_to_eq 201 + + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq user.profiles.first.sub_masked_last_name + expect(result['profiles'][0]['dob']).to eq user.profiles.first.sub_masked_dob + + user.reload.update(state: 'pending') + post '/api/v2/identity/users/email/confirm_code', params: token_params + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.utilized_token"]) + end + + it 'returns expired error on second attempt after lifetime' do + token_params = params + post '/api/v2/identity/users/email/confirm_code', params: token_params + expect_status_to_eq 201 + + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq user.profiles.first.sub_masked_last_name + expect(result['profiles'][0]['dob']).to eq user.profiles.first.sub_masked_dob + + user.reload.update(state: 'pending') + travel Barong::App.config.jwt_expire_time + 10.seconds + + post '/api/v2/identity/users/email/confirm_code', params: token_params + expect_status_to_eq 422 + expect_body.to eq(errors: ["jwt.decode_and_verify.expired"]) + end + end + end + + describe 'POST /api/v2/identity/users/password/generate_code' do + let(:do_request) do + post '/api/v2/identity/users/password/generate_code', params: params + end + let(:params) { { email: email } } + + context 'when email is unknown' do + let(:email) { 'unknown@gmail.com' } + + it 'renders 201' do + do_request + expect_status_to_eq 201 + expect_body.not_to eq(errors: ["identity.password.user_doesnt_exist"]) + end + end + + context 'when user is found by email' do + let!(:user) { create(:user, email: email) } + let(:email) { 'email@gmail.com' } + + it 'sends reset password instructions' do + do_request + expect_status_to_eq 201 + end + end + + context 'multiple password reset requests' do + let!(:user) { create(:user, email: email) } + let(:email) { 'email@gmail.com' } + let(:password) { 'ZahSh8ei' } + let(:confirm_password) { 'ZahSh8ei' } + + let(:log_in) { post '/api/v2/identity/sessions', params: { email: user.email, password: password } } + + it 'works with last email' do + post '/api/v2/identity/users/password/generate_code', params: params + expect_status_to_eq 201 + + post '/api/v2/identity/users/password/generate_code', params: params + expect_status_to_eq 201 + + reset_token = Rails.cache.read("reset_password_#{user.email}") + reset_password_token = codec.encode(sub: 'reset', email: user.email, uid: user.uid, reset_token: reset_token) + post '/api/v2/identity/users/password/confirm_code', params: { + reset_password_token: reset_password_token, + password: password, + confirm_password: confirm_password + } + expect_status_to_eq 201 + log_in + expect_status_to_eq 200 + end + + it 'returns error if prev link (non-utilized) is used' do + post '/api/v2/identity/users/password/generate_code', params: params + reset_token = Rails.cache.read("reset_password_#{user.email}") + expect_status_to_eq 201 + + post '/api/v2/identity/users/password/generate_code', params: params + expect_status_to_eq 201 + + reset_password_token = codec.encode(sub: 'reset', email: user.email, uid: user.uid, reset_token: reset_token) + post '/api/v2/identity/users/password/confirm_code', params: { + reset_password_token: reset_password_token, + password: password, + confirm_password: confirm_password + } + expect_status_to_eq 422 + expect(response.body).to eq("{\"errors\":[\"identity.user.utilized_token\"]}") + end + + it 'expires utilized token after lifetime but still returns error' do + post '/api/v2/identity/users/password/generate_code', params: params + reset_token = Rails.cache.read("reset_password_#{user.email}") + expect_status_to_eq 201 + + post '/api/v2/identity/users/password/generate_code', params: params + expect_status_to_eq 201 + + reset_password_token = codec.encode(sub: 'reset', email: user.email, uid: user.uid, reset_token: reset_token) + travel Barong::App.config.jwt_expire_time + 10.seconds + post '/api/v2/identity/users/password/confirm_code', params: { + reset_password_token: reset_password_token, + password: password, + confirm_password: confirm_password + } + + expect_status_to_eq 422 + expect(response.body).to eq("{\"errors\":[\"jwt.decode_and_verify.expired\"]}") + end + end + + context 'captcha behaviour when captcha policy is recaptcha' do + let!(:user) { create(:user, email: email) } + let(:email) { 'email@gmail.com' } + before { allow(Barong::App.config).to receive_messages(captcha: 'recaptcha') } + + it 'doesnt require captcha if endpoint is not in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create"]}) + + do_request + expect_status_to_eq 201 + end + + it 'require captcha if endpoint is in the protection list' do + allow(BarongConfig).to receive(:list).and_return({"captcha_protected_endpoints"=>["user_create", "session_create", "password_reset"]}) + + do_request + expect_status_to_eq 400 + expect_body.to eq(errors: ["identity.captcha.required"]) + end + end + end + + describe 'PUT /api/v2/identity/users/password/confirm_code' do + let(:do_request) do + post '/api/v2/identity/users/password/confirm_code', params: params + end + let(:params) do + { + reset_password_token: reset_password_token, + password: password, + confirm_password: confirm_password + } + end + let(:reset_password_token) { '' } + let(:password) { '' } + let(:confirm_password) { '' } + + context 'when params are blank' do + it 'renders 422 error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.empty_reset_password_token", "identity.user.empty_password", "identity.user.empty_confirm_password"]) + end + end + + context 'when Reset Password Token is invalid' do + let(:reset_password_token) { 'invalid' } + let(:password) { 'Gol4aid2' } + let(:confirm_password) { 'Gol4aid2' } + + it 'renders 422 error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["jwt.decode_and_verify.segments"]) + end + end + + context 'when Reset Password Token and Password are valid ' do + let!(:user) { create(:user) } + let(:reset_password_token) { codec.encode(sub: 'reset', email: user.email, uid: user.uid) } + let(:password) { 'ZahSh8ei' } + let(:confirm_password) { 'ZahSh8ei' } + let(:log_in) { post '/api/v2/identity/sessions', params: { email: user.email, password: password } } + + it 'resets a password' do + do_request + expect_status_to_eq 201 + log_in + expect_status_to_eq 200 + end + end + + context 'When Reset Password Token is valid, passwords are weak' do + let!(:user) { create(:user) } + let(:reset_password_token) { codec.encode(sub:'reset', email: user.email, uid: user.uid) } + let(:password) { 'Simple' } + let(:confirm_password) { 'Simple' } + + it 'returns weak password error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["password.requirements"]) + end + end + + context 'When Reset Password Token is valid, passwords don\'t match' do + let!(:user) { create(:user) } + let(:reset_password_token) { codec.encode(sub: 'reset', email: user.email, uid: user.uid) } + let(:password) { 'ZahSh8exwdi' } + let(:confirm_password) { 'ZahSh8ei' } + + it 'returns 422 error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.passwords_doesnt_match"]) + end + end + end + describe 'POST /api/v2/identity/users/email/confirm_email' do + let(:test_user) { create(:user, state: 'pending', email: 'valid.email@gmail.com') } + let(:do_request) { post '/api/v2/identity/users/email/confirm_email', params: params } + let(:params) { {} } + let(:valid_otp_code) { '1357' } + let(:invalid_otp_code) { '1234' } + + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, valid_otp_code) { true } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, invalid_otp_code) { false } + end + + context 'when token is missing' do + it 'returns an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.missing_code", "identity.user.empty_code", + "identity.user.missing_email", "identity.user.empty_email"]) + end + end + context 'when code is invalid' do + let(:params) { { email: test_user.email, code: invalid_otp_code } } + + it 'returns an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["identity.user.active_or_doesnt_exist"]) + end + end + context 'when code is valid' do + let(:params) { { email: test_user.email, code: valid_otp_code } } + + it 'returns an error' do + do_request + expect_status_to_eq 201 + expect(json_body.keys).to match_array %i[email uid role level otp state referral_uid csrf_token data created_at updated_at labels phones profiles data_storages] + expect(json_body[:email]).to eq 'valid.email@gmail.com' + expect(json_body[:state]).to eq 'active' + end + end + end +end diff --git a/spec/api/v2/management/api_keys_spec.rb b/spec/api/v2/management/api_keys_spec.rb new file mode 100644 index 0000000..4b20b27 --- /dev/null +++ b/spec/api/v2/management/api_keys_spec.rb @@ -0,0 +1,374 @@ +# frozen_string_literal: true + +describe API::V2::Management::APIKeys, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + write_apikeys: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + } + end + + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:create_service_account_permission) do + create :permission, + role: 'service_account' + end + + describe 'POST /api_keys' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_apikeys) } + + let(:do_request) do + post_json '/api/v2/management/api_keys', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do + api_keys_params + end + + context 'valid request' do + context 'service account' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let!(:service_account) { create(:service_account) } + + let(:api_keys_params) do + { + uid: service_account.uid, + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'creates a api key' do + do_request + expect(response.status).to eq(201) + + result = JSON.parse(response.body) + expect(result.keys).to match_array %w[kid algorithm scope state secret created_at updated_at] + expect(result['state']).to eq service_account.state + expect(result['scope']).to eq ['trade'] + expect(result['algorithm']).to eq 'HS256' + end + end + + context 'user' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_user: true) + end + + let!(:user) { create(:user) } + + let(:api_keys_params) do + { + uid: user.uid, + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'creates a api key' do + do_request + expect(response.status).to eq(201) + + result = JSON.parse(response.body) + expect(result.keys).to match_array %w[kid algorithm scope state secret created_at updated_at] + expect(result['state']).to eq user.state + expect(result['scope']).to eq ['trade'] + expect(result['algorithm']).to eq 'HS256' + end + end + end + + context 'invalid request' do + context 'vault inaccessible' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let!(:service_account) { create(:service_account) } + let(:api_keys_params) do + { + uid: service_account.uid, + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'should raise an error' do + allow(Vault::Rails).to receive(:encrypt).and_raise(Vault::VaultError) + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["api_key.could_not_save_secret"]) + end + end + + context 'service account doesnt exist' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let(:api_keys_params) do + { + uid: ServiceAccount::UID_PREFIX + 'random', + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'service_account_doesnt_exist') + end + end + + context 'user doesnt exists' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_user: true) + end + + let(:api_keys_params) do + { + uid: UIDGenerator.generate(Barong::App.config.uid_prefix), + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'user_doesnt_exist') + end + end + + context 'algorithm is invalid' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let!(:service_account) { create(:service_account) } + let(:api_keys_params) do + { + uid: service_account.uid, + algorithm: 'random', + scopes: 'trade' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ['kid.blank', 'algorithm.inclusion']) + end + end + + context 'disabled endpoint' do + let(:api_keys_params) do + { + uid: UIDGenerator.generate(Barong::App.config.uid_prefix), + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'disabled_management_endpoint') + end + end + + context 'unexisting uid' do + + let(:api_keys_params) do + { + uid: 'random', + algorithm: 'HS256', + scopes: 'trade' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'uid_prefix_doesnt_exist') + end + end + end + end + + describe 'POST /api_keys/update' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_apikeys) } + + let(:do_request) do + post_json '/api/v2/management/api_keys/update', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do + api_keys_params + end + + let!(:api_key) { create(:api_key, :with_service_account) } + + context 'valid request' do + context 'user' do + let!(:api_key) { create(:api_key, :with_user) } + + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_user: true) + end + + let(:params) do + { + uid: api_key.key_holder_account.uid, + kid: api_key.kid, + state: 'inactive', + scopes: 'sell' + } + end + + it 'updates api key' do + do_request + expect(response.status).to eq(201) + + result = JSON.parse(response.body) + expect(result.keys).to match_array %w[kid algorithm scope state created_at updated_at] + expect(result['state']).to eq 'inactive' + expect(result['scope']).to eq ['sell'] + end + end + + context 'service account' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let(:params) do + { + uid: api_key.key_holder_account.uid, + kid: api_key.kid, + state: 'inactive', + scopes: 'sell' + } + end + + it 'updates api key' do + do_request + expect(response.status).to eq(201) + + result = JSON.parse(response.body) + expect(result.keys).to match_array %w[kid algorithm scope state created_at updated_at] + expect(result['state']).to eq 'inactive' + expect(result['scope']).to eq ['sell'] + end + end + end + + context 'invalid request' do + context 'api keys doesnt exist' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let(:api_keys_params) do + { + uid: api_key.key_holder_account.uid, + kid: 'random kid' + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'api_key_doesnt_exist') + end + end + + context 'service account doesnt exist' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_sa: true) + end + + let(:api_keys_params) do + { + uid: ServiceAccount::UID_PREFIX + 'random', + kid: api_key.kid + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'service_account_doesnt_exist') + end + end + + context 'user doesnt exist' do + before do + allow(Barong::App.config).to receive_messages(mgn_api_keys_user: true) + end + + let(:api_keys_params) do + { + uid: UIDGenerator.generate(Barong::App.config.uid_prefix), + kid: api_key.kid + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'user_doesnt_exist') + end + end + + context 'disabled endpoint' do + let(:api_keys_params) do + { + uid: UIDGenerator.generate(Barong::App.config.uid_prefix), + kid: api_key.kid + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'disabled_management_endpoint') + end + end + + context 'unexisting uid' do + + let(:api_keys_params) do + { + uid: 'random', + kid: api_key.kid + } + end + + it 'should raise an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(error: 'uid_prefix_doesnt_exist') + end + end + end + end +end diff --git a/spec/api/v2/management/documents_spec.rb b/spec/api/v2/management/documents_spec.rb new file mode 100644 index 0000000..dcc678f --- /dev/null +++ b/spec/api/v2/management/documents_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe API::V2::Management::Documents, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + write_documents: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + } + end + + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user, :with_profile) } + + describe 'Show user info' do + let(:data) do + { + scope: :write_documents + } + end + let!(:image) { fixture_file_upload('/files/documents_test.jpg', 'image/jpg') } + let(:signers) { %i[alex jeff] } + let(:params) do + { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [ + image + ] + } + end + let!(:optional_params) do + { + metadata: { + country: Faker::Address.country + } + } + end + let(:do_request) do + post_json '/api/v2/management/documents', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do { + uid: user.uid, + doc_type: 'Passport', + filename: 'documents_test', + file_ext: '.jpg', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: Base64.strict_encode64(File.open('spec/fixtures/files/documents_test.jpg').read) + } + end + + it 'push documents to user' do + post_json '/api/v2/management/documents', multisig_jwt_management_api_v2({ data: params }, *signers) + + expect(response.status).to eq 201 + expect(user.documents.length).to eq(1) + end + + it 'push documents to user twice' do + post_json '/api/v2/management/documents', multisig_jwt_management_api_v2({ data: params }, *signers) + + expect(response.status).to eq 201 + expect(user.documents.length).to eq(1) + + post_json '/api/v2/management/documents', multisig_jwt_management_api_v2({ data: params }, *signers) + expect(response.status).to eq 201 + expect(user.documents.length).to eq(1) + end + + context 'with empty doc_expire' do + it 'required doc expire' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(true) + expect { + post_json '/api/v2/management/documents', multisig_jwt_management_api_v2({ data: params.merge(doc_expire: nil) }, *signers) + }.not_to change { Document.count } + expect(response.status).to eq 422 + end + + it 'optional doc expire' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(false) + expect { + post_json '/api/v2/management/documents', multisig_jwt_management_api_v2({ data: params.merge(doc_expire: nil) }, *signers) + }.to change { Document.count }.by 1 + expect(response.status).to eq 201 + end + end + end +end diff --git a/spec/api/v2/management/jwt_authentication_middleware_spec.rb b/spec/api/v2/management/jwt_authentication_middleware_spec.rb new file mode 100644 index 0000000..1a54859 --- /dev/null +++ b/spec/api/v2/management/jwt_authentication_middleware_spec.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe API::V2::Management::JWTAuthenticationMiddleware, type: :request do + let(:member) { create(:member, :verified_identity) } + let(:config) { management_api_v2_security_configuration } + + before do + defaults_for_management_api_v2_security_configuration! + config.merge! \ + scopes: { + tools: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + + it 'works in standard conditions' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({}, :alex) + expect(response).to be_successful + end + # FIXME fix swagger_doc + # it 'allows GET and doesn\'t require authentication for documentation' do + # get '/api/v2/management/swagger_doc' + # expect(response).to be_successful + # end + + it 'doesn\'t allow query parameters' do + post '/api/v2/management/timestamp?foo=baz&baz=qux' + expect(response).to have_http_status(400) + expect(response.body).to match(/query parameters/i) + end + + it 'requires JSON in the request body' do + post '/api/v2/management/timestamp', params: 'foo=baz&baz=qux' + expect(response).to have_http_status(400) + expect(response.body).to match(/only json/i) + end + + it 'denies access when not enough signatures are supplied' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({}) + expect(response).to have_http_status(401) + expect(response.body).to match(/not enough signatures/i) + end + + it 'denies access when token is expired' do + config[:jwt][:verify_expiration] = true + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ exp: 1.minute.ago.to_i }, :alex) + expect(response).to have_http_status(401) + expect(response.body).to match(/failed to verify jwt/i) + end + + context 'valid issuer' do + before { config[:jwt][:verify_iss] = true } + before { config[:jwt].merge!(iss: 'qux') } + it 'validates issuer' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ iss: 'qux' }, :alex) + expect(response).to be_successful + end + end + + context 'invalid issuer' do + before { config[:jwt][:verify_iss] = true } + before { config[:jwt].merge!(iss: 'qux') } + it 'validates issuer' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ iss: 'hacker' }, :alex) + expect(response).to have_http_status(401) + expect(response.body).to match(/failed to verify jwt/i) + end + end + + context 'valid audience' do + before { config[:jwt][:verify_aud] = true } + before { config[:jwt].merge!(aud: 'qux') } + it 'validates audience' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ aud: 'qux' }, :alex) + expect(response).to be_successful + end + end + + context 'invalid audience' do + before { config[:jwt][:verify_aud] = true } + before { config[:jwt].merge!(aud: 'qux') } + it 'validates audience' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ aud: 'hacker' }, :alex) + expect(response).to have_http_status(401) + expect(response.body).to match(/failed to verify jwt/i) + end + end + + context 'missing JWT ID' do + before { config[:jwt][:verify_jti] = true } + it 'requires JTI' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({}, :alex) + expect(response).to have_http_status(401) + expect(response.body).to match(/failed to verify jwt/i) + end + end + + context 'issued at in future' do + before { config[:jwt][:verify_iat] = true } + it 'denies access' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ iat: 200.seconds.from_now.to_i }, :alex) + expect(response).to have_http_status(401) + expect(response.body).to match(/failed to verify jwt/i) + end + end + + context 'issued at before future' do + before { config[:jwt][:verify_iat] = true } + it 'allows access' do + post_json '/api/v2/management/timestamp', multisig_jwt_management_api_v2({ iat: 3.seconds.ago.to_i }, :alex) + expect(response).to have_http_status(200) + end + end + end diff --git a/spec/api/v2/management/labels_spec.rb b/spec/api/v2/management/labels_spec.rb new file mode 100644 index 0000000..55637eb --- /dev/null +++ b/spec/api/v2/management/labels_spec.rb @@ -0,0 +1,304 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pry' + +describe API::V2::Management::Labels, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + write_labels: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + read_labels: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + read_users: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user) } + let!(:user_with_private_label) { create(:user) } + let!(:user_with_public_label) { create(:user) } + + describe '#filter/users' do + let!(:private_label) do + create(:label, scope: 'private', key: 'profile', value: 'pending', user: user_with_private_label) + end + let!(:public_label) do + create(:label, scope: 'public', key: 'profile', value: 'pending', user: user_with_public_label) + end + let(:data) { { key: 'profile', value: 'pending', scope: 'private' } } + let(:signers) { %i[alex] } + let(:do_request) do + post_json '/api/v2/management/labels/filter/users', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'filters users based on label attributes. doesnt mix PUBLIC and PRIVATE labels' do + # filtering params key: 'profile', value: 'pending', scope: 'private' + do_request + expect(json_body.count == 1) + expect(json_body.first[:email]).to eq(user_with_private_label.attributes.symbolize_keys[:email]) + expect(json_body.first[:uid]).to eq(user_with_private_label.attributes.symbolize_keys[:uid]) + end + + it 'filters users based on label timestamp attributes' do + # filtering params key: 'profile', to: 1.day.ago + data.clear + data[:to] = 1.day.ago.to_i + data[:key] = 'profile' + public_label.update(created_at: 3.days.ago) + do_request + + expect(json_body.count == 1) + expect(json_body.first[:email]).to eq(user_with_public_label.attributes.symbolize_keys[:email]) + expect(json_body.first[:uid]).to eq(user_with_public_label.attributes.symbolize_keys[:uid]) + end + + it 'returns empty array if no mathes found' do + # filtering params key: 'documents', value: 'pending', scope: 'private' + data[:key] = 'documents' + do_request + + expect(json_body.count == 0) + expect(json_body).to eq([]) + end + + it 'returns user with profile if extended is true' do + data[:extended] = true + do_request + + expect(json_body.first).to include(:profiles) + end + end + + describe '#list' do + let!(:private_label) do + create(:label, scope: 'private', user: user) + end + let!(:public_label) do + create(:label, scope: 'public', user: user) + end + let(:data) do + { + user_uid: user.uid + } + end + let(:signers) { %i[alex] } + + let(:do_request) do + post_json '/api/v2/management/labels/list', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'get list of user labels' do + do_request + expect(response.status).to eq 201 + expect(json_body.size).to eq 2 + end + end + + describe 'create label' do + let(:data) do + { + user_uid: user.uid, + key: 'email', + value: 'verified', + scope: 'private' + } + end + let(:expected_attributes) do + { + 'key' => 'email', + 'value' => 'verified', + 'user_id' => user.id, + 'scope' => 'private' + } + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/labels', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'creates a label' do + expect { do_request }.to change { Label.count }.from(0).to(1) + expect(response.status).to eq 201 + expect(Label.first.attributes).to include(expected_attributes) + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + expect { do_request }.to_not change { Label.count } + expect(response.status).to eq 401 + end + + it 'denies when user is not found' do + data[:user_uid] = 'invalid' + expect { do_request }.to_not change { Label.count } + expect(response.status).to eq 404 + end + + context 'when data is blank' do + let(:data) { {} } + + it 'renders errors' do + do_request + expect(response.status).to eq 422 + expect_body.to eq(error: 'user_uid is missing, user_uid is empty, key is missing, key is empty, value is missing, value is empty') + end + end + end + + describe 'update label' do + let!(:label) do + create(:label, key: 'email', value: 'verified', scope: 'private', description: 'initial label', user: user) + end + + let(:data) do + { + user_uid: user.uid, + key: 'email', + value: 'rejected', + description: 'management api label update test' + } + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + put_json '/api/v2/management/labels', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'updates a label value' do + expect { do_request }.to change { label.reload.value }.from('verified').to('rejected') + expect(response.status).to eq 200 + end + + + it 'updates a label value' do + expect { do_request }.to change { label.reload.description }.from('initial label').to('management api label update test') + expect(response.status).to eq 200 + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + expect { do_request }.to_not change { label.reload.value } + expect(response.status).to eq 401 + end + + it 'denies when user is not found' do + data[:user_uid] = 'invalid' + expect { do_request }.to_not change { label.reload.value } + expect(response.status).to eq 404 + end + + context 'when data is blank' do + let(:data) { {} } + + it 'renders errors' do + do_request + expect(response.status).to eq 422 + expect_body.to eq(error: 'user_uid is missing, user_uid is empty, key is missing, key is empty, value is missing, value is empty') + end + end + + context 'when label doesnt exist yet with default replace policy' do + let(:data) do + { + user_uid: user.uid, + key: 'phone', + value: 'verified' + } + end + + it 'creates a label' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 200 + expect(user.labels.find_by(key: 'phone')).not_to eq(nil) + end + + context 'when data is incomplete' do + let(:data) do + { + user_uid: user.uid, + key: 'phone' + } + end + + it 'receive an error' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 422 + expect(response.body).to eq "{\"error\":\"value is missing, value is empty\"}" + expect(user.labels.find_by(key: 'phone')).to eq(nil) + end + end + end + + context 'when label doesnt exist yet with false replace policy' do + let(:data) do + { + user_uid: user.uid, + key: 'phone', + value: 'verified', + replace: false + } + end + + it 'receives an error' do + expect(user.labels.find_by(key: 'phone')).to eq(nil) + + do_request + expect(response.status).to eq 404 + expect(user.labels.find_by(key: 'phone')).to eq(nil) + expect(response.body).to eq "{\"error\":\"label doesnt exist\"}" + end + end + end + + describe 'delete label' do + let!(:label) do + create(:label, key: 'email', value: 'verified', scope: 'private', user: user) + end + + let(:data) do + { + user_uid: user.uid, + key: 'email' + } + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/labels/delete', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'deletes a label' do + expect { do_request }.to change { Label.count }.from(1).to(0) + expect(response.status).to eq 204 + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + expect { do_request }.to_not change { label.reload.value } + expect(response.status).to eq 401 + end + + it 'denies when user is not found' do + data[:user_uid] = 'invalid' + expect { do_request }.to_not change { label.reload.value } + expect(response.status).to eq 404 + end + end + end diff --git a/spec/api/v2/management/otp_spec.rb b/spec/api/v2/management/otp_spec.rb new file mode 100644 index 0000000..6393c03 --- /dev/null +++ b/spec/api/v2/management/otp_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pry' + +describe API::V2::Management::Otp, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + otp_sign: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user, otp: otp_enabled) } + + describe 'POST /otp/sign' do + let(:data) do + { + user_uid: user.uid, + otp_code: valid_otp_code, + jwt: jwt + } + end + let(:signers) { %i[alex jeff] } + let(:valid_otp_code) { '1111' } + let(:invalid_otp_code) { '1234' } + let(:jwt) { applogic_signed_jwt(amount: '1 BTC') } + let(:otp_enabled) { true } + let(:do_request) do + post_json '/api/v2/management/otp/sign', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + let(:valid_payload_keys) do + %i[payload signatures] + end + + before do + allow(TOTPService).to receive(:validate?) + .with(user.uid, valid_otp_code) { true } + allow(TOTPService).to receive(:validate?) + .with(user.uid, invalid_otp_code) { false } + end + + it 'signs a request' do + do_request + expect(json_body.keys).to eq(valid_payload_keys) + expect(response.status).to eq 201 + end + + it 'renders an error when jwt is not a Hash' do + data[:jwt] = 'invalid' + do_request + expect_body.to eq(error: 'jwt is invalid') + expect(response.status).to eq 422 + end + + it 'renders an error when jwt is not RFC' do + data[:jwt] = { data: 'invalid' } + do_request + expect(json_body[:error]).to include('JWT is invalid by the reason') + expect(response.status).to eq 422 + end + + it 'renders an error when otp code is invalid' do + data[:otp_code] = invalid_otp_code + do_request + expect_body.to eq(error: 'OTP code is invalid') + expect(response.status).to eq 422 + end + + it 'renders an error when account is not found' do + data[:user_uid] = 'invalid' + do_request + expect_body.to eq error: 'Record is not found' + expect(response.status).to eq 404 + end + + context 'when data is blank' do + let(:data) { {} } + + it 'renders errors' do + do_request + expect_body.to eq(error: "user_uid is missing, user_uid is empty, otp_code is missing, otp_code is empty, jwt is missing, jwt is empty") + expect(response.status).to eq 422 + end + end + end +end diff --git a/spec/api/v2/management/phones_spec.rb b/spec/api/v2/management/phones_spec.rb new file mode 100644 index 0000000..b79f2b6 --- /dev/null +++ b/spec/api/v2/management/phones_spec.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true + +describe API::V2::Management::Phones, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + write_phones: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + read_phones: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + } + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user) } + + describe 'POST /phones/get' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :read_phones) } + + let(:do_request) do + post_json '/api/v2/management/phones/get', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do + phone_params + end + + context 'valid request' do + context 'user without phones' do + let(:phone_params) do + { + uid: user.uid + } + end + + it 'get list of phones' do + do_request + expect_status_to_eq 201 + expect_body.to eq [] + end + end + + context 'user with phones' do + let!(:phone) { create(:phone, user: user) } + let(:phone_params) do + { + uid: user.uid + } + end + + it 'get list of phones' do + do_request + expect_status_to_eq 201 + expect(json_body.count).to eq 1 + expect(json_body.first[:number]).to eq phone.number + expect(json_body.first[:country]).to eq phone.country + end + end + end + + context 'invalid request' do + let(:phone_params) do + { + uid: 'invalid_uid' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'user.doesnt_exist') + expect_status.to eq 422 + end + end + end + + describe 'POST /phones' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_phones) } + + let(:do_request) do + post_json '/api/v2/management/phones', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do + phone_params + end + + context 'valid request' do + let(:phone_params) do + { + uid: user.uid, + number: '12345678911' + } + end + + it 'creates a phone' do + expect { do_request }.to change { Phone.count }.by(1) + expect_status_to_eq 201 + + result = JSON.parse(response.body) + expect(result['country']).to eq 'US' + expect(result['number']).to eq phone_params[:number] + end + end + + context 'invalid request' do + context 'when phone is invalid' do + let(:phone_params) do + { + uid: user.uid, + number: '123' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.invalid_num') + expect_status.to eq 400 + end + end + + context 'when phone is missing' do + let(:phone_params) do + { + uid: user.uid + } + end + + it 'renders an error' do + do_request + expect_status.to eq 422 + expect_body.to eq(error: 'number is missing, number is empty') + end + end + + context 'when phone is already exists' do + let!(:phone) do + create(:phone, validated_at: validated_at) + end + + let(:phone_params) do + { + uid: user.uid, + number: phone.number + } + end + + context 'when phone verified' do + let(:validated_at) { 1.minutes.ago } + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.number_exist') + expect_status.to eq 400 + end + end + + context 'when phone verified but number is not sanitized' do + let(:validated_at) { 1.minutes.ago } + let(:phone_params) do + { + uid: user.uid, + number: '++#{phone.number}' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.invalid_num') + expect_status.to eq 400 + end + end + end + + context 'when phone exists on international format' do + let(:phone_params) do + { + uid: user.uid, + number: '+44 07418084106' + } + end + let(:international_phone) { '447418084106' } + let!(:phone) do + create(:phone, validated_at: 1.minute.ago, number: international_phone) + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.number_exist') + expect_status.to eq 400 + end + end + + context 'when phone exists for current user' do + let!(:phone) { create(:phone, number: '447418084106', user_id: user.id)} + let(:phone_params) do + { + uid: user.uid, + number: '447418084106' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.exists') + expect_status.to eq 400 + end + end + end + end + + describe 'POST /phone/delete' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_phones) } + + let(:do_request) do + post_json '/api/v2/management/phones/delete', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let(:params) do + phone_params + end + + context 'valid request' do + context 'user with one phone number' do + let!(:phone) { create(:phone, number: '447418084106', user_id: user.id)} + + let(:phone_params) do + { + uid: user.uid, + number: phone.number + } + end + + it 'show deleted phone' do + expect { do_request }.to change { Phone.count }.by(-1) + expect_status.to eq 201 + expect(json_body[:number]).to eq phone.number + expect(json_body[:country]).to eq phone.country + end + end + + context 'user with several phone numbers' do + let!(:phone1) { create(:phone, number: '447418084106', user_id: user.id)} + let!(:phone2) { create(:phone, number: '447418084107', user_id: user.id)} + + let(:phone_params) do + { + uid: user.uid, + number: phone2.number + } + end + + it 'show deleted phone' do + expect { do_request }.to change { Phone.count }.by(-1) + expect_status.to eq 201 + expect(json_body[:number]).to eq phone2.number + expect(json_body[:country]).to eq phone2.country + end + end + end + + context 'invalid request' do + context 'invalid user' do + let(:phone_params) do + { + uid: 'invalid_uid', + number: 'number' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'user.doesnt_exist') + expect_status.to eq 422 + end + end + + context 'invalid phone number' do + let(:phone_params) do + { + uid: user.uid, + number: 'invalid_number' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.doesnt_exists') + expect_status.to eq 422 + end + end + + context 'non existed phone number' do + let(:phone_params) do + { + uid: user.uid, + number: '1111111111' + } + end + + it 'renders an error' do + do_request + expect_body.to eq(error: 'management.phone.doesnt_exists') + expect_status.to eq 422 + end + end + end + end +end diff --git a/spec/api/v2/management/profiles_spec.rb b/spec/api/v2/management/profiles_spec.rb new file mode 100644 index 0000000..66f78c7 --- /dev/null +++ b/spec/api/v2/management/profiles_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe API::V2::Management::Profiles, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + read_users: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + write_users: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_users) } + + let(:do_request) do + post_json '/api/v2/management/profiles', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'when profile params are provided' do + let(:params) do + profile_params + end + + context 'when params are valid' do + let(:user) { create :user } + let(:profile_params) do + { + uid: user.uid, + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + it 'creates a profile' do + expect { do_request }.to change { Profile.count }.by(1) + expect_status_to_eq 201 + + result = JSON.parse(response.body) + expect(result['profiles'][0]['state']).to eq 'drafted' + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + end + end + + context 'when postcode is not provided' do + let(:user) { create :user } + let(:profile_params) do + { + uid: user.uid, + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state + } + end + + it 'create a profile' do + expect { do_request }.to change { Profile.count } + expect_status_to_eq 201 + result = JSON.parse(response.body) + expect(result['profiles'][0]['state']).to eq 'drafted' + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + end + end + + context 'when profile state is provided' do + let(:user) { create :user } + let(:profile_params) do + { + uid: user.uid, + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + state: 'rejected' + } + end + + it 'create a profile' do + expect { do_request }.to change { Profile.count } + expect_status_to_eq 201 + result = JSON.parse(response.body) + expect(result['profiles'][0]['state']).to eq 'rejected' + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + end + end + + context 'when params are invalid' do + let(:user) { create :user } + let(:profile_params) do + { + uid: user.uid, + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: 'a', + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + it 'renders an error' do + expect { do_request }.to_not change { Profile.count } + expect_status_to_eq 422 + expect(json_body[:error]).to eq ['Country must have alpha2 or alpha3 format'] + end + end + end +end diff --git a/spec/api/v2/management/service_accounts_spec.rb b/spec/api/v2/management/service_accounts_spec.rb new file mode 100644 index 0000000..e5b9bb4 --- /dev/null +++ b/spec/api/v2/management/service_accounts_spec.rb @@ -0,0 +1,373 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pry' + +describe API::V2::Management::ServiceAccounts, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + read_service_accounts: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + write_service_accounts: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:create_service_account_permission) do + create :permission, + role: 'service_account' + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + + let!(:user) { create(:user, role: 'member') } + let!(:service_account) { create(:service_account, user: user) } + + describe 'Show service_account info' do + let(:data) do + { + scope: :read_service_accounts + } + end + + let(:expected_attributes) do + %i[email uid role level state user created_at updated_at] + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/service_accounts/get', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'reads service_account info by uid' do + data[:uid] = service_account.uid + do_request + + expect(response.status).to eq 200 + expect(json_body.keys).to eq expected_attributes + end + + it 'reads service_account info by email' do + data[:email] = service_account.email + do_request + + expect(response.status).to eq 200 + expect(json_body.keys).to eq expected_attributes + end + + it 'denies when uid is not found' do + data[:uid] = 'invalid' + do_request + expect(response.status).to eq 404 + end + + it 'denies when email is not found' do + data[:email] = 'invalid' + do_request + expect(response.status).to eq 404 + end + end + + describe 'Returns array of service accounts as collection' do + let(:data) do + { + scope: :read_service_accounts + } + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/service_accounts/list', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'service_accounts' do + let!(:test_service_account) { create(:service_account, email: 'testa@gmail.com', user: user) } + let!(:second_service_account) { create(:service_account, email: 'testb@gmail.com', user: user) } + let!(:third_service_account) { create(:service_account, email: 'testd@gmail.com', user: user) } + let!(:fourth_service_account) { create(:service_account, email: 'testc@gmail.com', user: user) } + + include_context 'bearer authentication' + + let(:do_service_accounts_request) do + post_json '/api/v2/management/service_accounts/get', + multisig_jwt_management_api_v2({ data: data }), headers: auth_header + end + it 'denies access for user JWT instead of management signature' do + do_service_accounts_request + expect(response.status).to eq 401 + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + do_request + expect(response.status).to eq 401 + end + + it 'returns list of service_accounts' do + do_request + + service_accounts = JSON.parse(response.body) + expect(ServiceAccount.count).to eq service_accounts.count + end + + context 'pagination test' do + let(:service_account_list_params) do + { + scope: :read_users, + limit: 2 + } + end + + it 'returns 1st page as default, limit 2 users per page' do + service_account_list_params[:page] = 1 + post_json '/api/v2/management/service_accounts/list', multisig_jwt_management_api_v2({ data: service_account_list_params }, *signers), headers: auth_header + + expect(response.headers.fetch('Total')).to eq ServiceAccount.all.count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + + it 'returns 2nd page, limit 2 users per page' do + service_account_list_params[:page] = 2 + post_json '/api/v2/management/service_accounts/list', multisig_jwt_management_api_v2({ data: service_account_list_params }, *signers), headers: auth_header + + expect(response.headers.fetch('Total')).to eq ServiceAccount.all.count.to_s + expect(response.headers.fetch('Page')).to eq '2' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + end + end + end + + describe 'Create a service account' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_service_accounts) } + + let(:do_request) do + post_json '/api/v2/management/service_accounts/create', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'when uid is present' do + context 'when email is valid' do + let(:params) do + { + service_account_email: 'valid_email@example.com', + service_account_uid: 'Fai5aesoLEcx', + service_account_role: 'admin', + owner_uid: user.uid + } + end + + it 'creates a service account' do + expect { do_request }.to change { ServiceAccount.count }.by(1) + expect_status_to_eq 201 + end + end + + context 'when params are blank' do + let(:params) { {} } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'service_account_role is missing, service_account_role is empty') + end + end + + context 'when email is bad' do + let(:params) { { service_account_email: 'bad_email', service_account_uid: 'Fai5aesoLEcx', service_account_role: 'member', owner_uid: user.uid } } + + it 'renders an error' do + expect { do_request }.to_not change { ServiceAccount.count } + expect_status_to_eq 422 + expect_body.to eq(error: ['Email is invalid']) + end + end + + context 'when role does not exist' do + let(:params) { { service_account_email: 'valid_email@example.com', service_account_uid: 'Fai5aesoLEcx', service_account_role: 'invalid', owner_uid: user.uid } } + + it 'renders an error' do + expect { do_request }.to_not change { ServiceAccount.count } + expect_status_to_eq 422 + expect_body.to eq(error: ['Role doesnt_exist']) + end + end + + context 'when uid is not uniq, the same as user one' do + let(:params) { { service_account_email: 'valid_email@example.com', service_account_uid: user.uid, service_account_role: 'member', owner_uid: user.uid } } + + it 'renders an error' do + expect { do_request }.to_not change { ServiceAccount.count } + expect_status_to_eq 422 + expect(json_body[:error].first).to include 'Email or uid not_uniq' + end + end + end + end + + describe 'Update a service account' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_service_accounts) } + let!(:service_account) { create(:service_account, :without_user) } + + let(:do_request) do + post_json '/api/v2/management/service_accounts/update', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'when params are blank' do + let(:params) { {} } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'uid is missing, uid is empty') + end + end + + context 'when service account doesnt exists' do + let(:params) do + { + uid: 'example' + } + end + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'Service account doesnt exist') + end + end + + context 'update service account' do + context do + let(:params) do + { + uid: service_account.uid + } + end + + context 'service account without user' do + it 'shouldnt modify service account' do + do_request + expect_status_to_eq 201 + + res = JSON.parse(response.body) + expect(res['state']).to eq 'pending' + expect(res['level']).to eq 0 + expect(res['user']).to eq nil + end + end + + context 'service account with user' do + let!(:service_account) { create(:service_account) } + + it 'shouldnt modify service account' do + owner = service_account.user + do_request + expect_status_to_eq 201 + + res = JSON.parse(response.body) + expect(res['state']).to eq owner.state + expect(res['level']).to eq owner.level + expect(res['user']['uid']).to eq owner.uid + end + end + end + + context do + context 'service account without user' do + let!(:user_with_phone) { create(:user, :with_phone) } + let(:params) do + { + uid: service_account.uid, + owner_uid: user_with_phone.uid + } + end + + it 'should modify service account' do + expect(service_account.level).to eq 0 + expect(service_account.state).to eq 'pending' + expect(service_account.user).to eq nil + + do_request + expect_status_to_eq 201 + + res = JSON.parse(response.body) + expect(res['user']['uid']).to eq user_with_phone.uid + expect(res['level']).to eq user_with_phone.level + expect(res['state']).to eq user_with_phone.state + end + end + + context 'service account with user' do + let!(:user_with_phone) { create(:user, :with_phone) } + let!(:user_with_profile) { create(:user, :with_profile) } + let!(:service_account) { create(:service_account, user: user_with_phone)} + + let(:params) do + { + uid: service_account.uid, + owner_uid: user_with_profile.uid + } + end + + it 'should modify service account' do + expect(service_account.level).to eq user_with_phone.level + expect(service_account.state).to eq user_with_phone.state + expect(service_account.user.uid).to eq user_with_phone.uid + + do_request + expect_status_to_eq 201 + + res = JSON.parse(response.body) + expect(res['user']['uid']).to eq user_with_profile.uid + expect(res['level']).to eq user_with_profile.level + expect(res['state']).to eq user_with_profile.state + end + end + end + end + end + + describe 'Delete a service account' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_service_accounts) } + + let(:do_request) do + post_json '/api/v2/management/service_accounts/delete', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context do + let(:params) { { uid: service_account.uid } } + + it do + do_request + expect_status_to_eq 200 + + service_account = JSON.parse(response.body) + expect(service_account['state']).to eq 'disabled' + end + end + + context 'when params are blank' do + let(:params) { {} } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'uid is missing, uid is empty') + end + end + end +end diff --git a/spec/api/v2/management/users_spec.rb b/spec/api/v2/management/users_spec.rb new file mode 100644 index 0000000..810a9f2 --- /dev/null +++ b/spec/api/v2/management/users_spec.rb @@ -0,0 +1,556 @@ +# frozen_string_literal: true + +require 'rails_helper' +require 'pry' + +describe API::V2::Management::Users, type: :request do + before do + defaults_for_management_api_v2_security_configuration! + management_api_v2_security_configuration.merge! \ + scopes: { + read_users: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] }, + write_users: { permitted_signers: %i[alex jeff], mandatory_signers: %i[alex] } + } + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user, :with_profile) } + + describe 'Show user info' do + let(:data) do + { + scope: :read_users + } + end + let(:expected_attributes) do + %i[email uid role level otp state data profiles referral_uid created_at updated_at] + end + let(:extended_attributes) do + %i[email uid role level otp state data profiles labels phones documents data_storages comments referral_uid created_at updated_at] + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/users/get', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + it 'reads extended user info by uid' do + data[:uid] = user.uid + do_request + + expect(response.status).to eq 201 + expect(json_body.keys).to match_array extended_attributes + expect(json_body).to include(:profiles, :comments, :phones, :documents) + expect(json_body[:profiles][0][:last_name]).to eq user.profiles[0].last_name + expect(json_body[:profiles][0][:dob]).to eq user.profiles[0].dob.to_s + end + + it 'reads full user info by email' do + data[:email] = user.email + + do_request + expect(response.status).to eq 201 + expect(json_body.keys).to match_array extended_attributes + expect(json_body).to include(:profiles, :comments, :phones, :documents) + expect(json_body[:profiles][0][:last_name]).to eq user.profiles[0].last_name + expect(json_body[:profiles][0][:dob]).to eq user.profiles[0].dob.to_s + end + + let!(:phone) do + create(:phone, validated_at: 1.minutes.ago, user_id: user.id) + end + + it 'reads full user info by user phone' do + data[:phone_num] = phone.number + + do_request + expect(response.status).to eq 201 + expect(json_body.keys).to match_array extended_attributes + expect(json_body).to include(:profiles, :comments, :phones, :documents) + expect(json_body[:profiles][0][:last_name]).to eq user.profiles[0].last_name + expect(json_body[:profiles][0][:dob]).to eq user.profiles[0].dob.to_s + end + + it 'allows only one of phone, uid, email' do + data[:phone_num] = phone.number + data[:email] = user.email + data[:uid] = user.uid + do_request + expect(response.status).to eq 422 + expect_body.to eq(error: 'uid, email, phone_num are mutually exclusive') + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + do_request + expect(response.status).to eq 401 + end + + it 'denies when uid is not found' do + data[:uid] = 'invalid' + do_request + expect(response.status).to eq 404 + end + + it 'denies when email is not found' do + data[:email] = 'invalid' + do_request + expect(response.status).to eq 404 + end + + it 'denies when email is not found' do + data[:phone_num] = 'invalid' + do_request + expect(response.status).to eq 404 + end + + context 'when data is blank' do + let(:data) { {} } + + it 'renders errors' do + do_request + expect(response.status).to eq 422 + expect_body.to eq(error: 'uid, email, phone_num are missing, exactly one parameter must be provided') + end + end + end + + describe 'Returns array of users as collection' do + let(:data) do + { + scope: :read_users + } + end + let(:signers) { %i[alex jeff] } + + let(:do_request) do + post_json '/api/v2/management/users/list', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'users' do + let!(:test_user) { create(:user, email: 'testa@gmail.com', role: 'admin') } + let!(:second_user) { create(:user, email: 'testb@gmail.com') } + let!(:third_user) { create(:user, email: 'testd@gmail.com') } + let!(:fourth_user) { create(:user, email: 'testc@gmail.com') } + + def validate_fields(user) + user.attributes.slice('email', 'role', 'level', 'otp', 'state', 'uid', 'data') + end + + include_context 'bearer authentication' + + let(:do_user_request) do + post_json '/api/v2/management/users/get', + multisig_jwt_management_api_v2({ data: data }), headers: auth_header + end + it 'denies access for user JWT instead of management signature' do + do_user_request + expect(response.status).to eq 401 + end + + it 'denies access unless enough signatures are supplied' do + signers.clear.concat %i[james jeff] + do_request + expect(response.status).to eq 401 + end + + it 'returns list of users' do + do_request + + users = JSON.parse(response.body) + expect(User.count).to eq users.count + expect(validate_fields(User.first)).to eq users.first.except('referral_uid') + expect(validate_fields(User.second)).to eq users.second.except('referral_uid') + expect(validate_fields(User.third)).to eq users.third.except('referral_uid') + expect(validate_fields(User.last)).to eq users.last.except('referral_uid') + end + + context 'pagination test' do + let(:users_list_params) do + { + scope: :read_users, + limit: 2 + } + end + + it 'returns 1st page as default, limit 2 users per page' do + users_list_params[:page] = 1 + post_json '/api/v2/management/users/list', multisig_jwt_management_api_v2({ data: users_list_params }, *signers), headers: auth_header + + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '1' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + + it 'returns 2nd page, limit 2 users per page' do + users_list_params[:page] = 2 + post_json '/api/v2/management/users/list', multisig_jwt_management_api_v2({ data: users_list_params }, *signers), headers: auth_header + + expect(response.headers.fetch('Total')).to eq User.all.count.to_s + expect(response.headers.fetch('Page')).to eq '2' + expect(response.headers.fetch('Per-Page')).to eq '2' + end + end + + context 'filtering test' do + let(:users_list_params) do + { + scope: :read_users + } + end + + it 'returns filtered list of users when only one filter param given created_at and from' do + users_list_params[:range] = 'created' + User.first.update(created_at: 5.hours.ago) + users_list_params[:from] = 8.hours.ago.to_i + users_list_params[:to] = 2.hours.ago.to_i + + post_json '/api/v2/management/users/list', multisig_jwt_management_api_v2({ data: users_list_params }, *signers), headers: auth_header + + expect(response.status).to eq 200 + expect(json_body.count).to eq (1) + end + + it 'returns filtered list of users when only one filter param given updated_at' do + users_list_params[:range] = 'updated' + User.first.update(updated_at: 5.hours.ago) + users_list_params[:from] = 8.hours.ago.to_i + users_list_params[:to] = 2.hours.ago.to_i + + post_json '/api/v2/management/users/list', multisig_jwt_management_api_v2({ data: users_list_params }, *signers), headers: auth_header + + expect(response.status).to eq 200 + expect(json_body.count).to eq (1) + expect(json_body[0]).not_to include(:profiles) + end + + it 'returns filetered list of users with extended param' do + users_list_params[:extended] = true + post_json '/api/v2/management/users/list', multisig_jwt_management_api_v2({ data: users_list_params }, *signers), headers: auth_header + expect(response.status).to eq 200 + expect(json_body[0]).to include(:profiles) + expect(json_body.first[:profiles][0][:last_name]).to eq user.profiles[0].last_name + expect(json_body.first[:profiles][0][:dob]).to eq user.profiles[0].dob.to_s + end + end + end + end + + describe 'Update role and data of existing user' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_users) } + + let(:do_request) do + post_json '/api/v2/management/users/update', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'when data is present' do + let!(:user) { create :user, :with_profile } + let(:params) do + { + uid: user.uid, + data: { nationality: 'us' }.to_json + } + end + it 'updates data field' do + do_request + + expect_status_to_eq 201 + expect(json_body).to include(:profiles) + expect(json_body[:profiles][0][:last_name]).to eq user.profiles[0].last_name + expect(json_body[:profiles][0][:dob]).to eq user.profiles[0].dob.to_s + expect(user.reload.data).to eq({ nationality: 'us' }.to_json) + end + end + + context 'when role is present' do + let(:user) { create :user } + let(:params) do + { + uid: user.uid, + role: 'admin' + } + end + it 'updates role field' do + do_request + + expect_status_to_eq 201 + expect(user.reload.role).to eq('admin') + end + end + + context 'when data and role are present' do + let(:user) { create :user } + let(:params) do + { + uid: user.uid, + data: { nationality: 'us' }.to_json, + role: 'admin' + } + end + it 'updates data and role fields' do + do_request + + expect_status_to_eq 201 + expect(user.reload.data).to eq({ nationality: 'us' }.to_json) + expect(user.reload.role).to eq('admin') + end + end + + context 'when none of parameters are present' do + let(:user) { create :user } + let(:params) do + { + uid: user.uid, + } + end + it 'renders error' do + do_request + + expect_status_to_eq 422 + expect_body.to eq(error: "role, data are missing, at least one parameter must be provided") + end + end + end + + describe 'Create an user' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_users) } + + let(:do_request) do + post_json '/api/v2/management/users', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + context 'when password is present' do + context 'when email and password are valid' do + let(:params) do + { + email: 'valid_email@example.com', + password: 'Fai5aesoLEcx' + } + end + it 'creates an user' do + expect { do_request }.to change { User.count }.by(1) + expect_status_to_eq 201 + end + end + + context 'when params are blank' do + let(:params) { {} } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'email is missing, email is empty, password is missing, password is empty') + end + end + + context 'when email is bad' do + let(:params) { { email: 'bad_email', password: 'Password1' } } + + it 'renders an error' do + expect { do_request }.to_not change { User.count } + expect_status_to_eq 422 + expect_body.to eq(error: ['Email is invalid','Password weak']) + end + end + + context 'creates an account with referral uid' do + let(:params) { { email: 'test@test.com', password: 'Fai5aesoLEcx', referral_uid: User.last.uid } } + + it 'renders an error' do + expect { do_request }.to change { User.count }.by(1) + expect_status_to_eq 201 + expect(json_body).to include(:email, :uid, :role, :level, + :state, :otp, :profiles) + end + end + + context 'when password is bad' do + let(:params) { { email: 'valid_email@example.com', password: 'password' } } + + it 'renders an error' do + expect { do_request }.to_not change { User.count } + expect_status_to_eq 422 + expect(json_body[:error].first).to include 'Password requirements' + end + end + end + end + + describe 'Imports an existing user' do + let(:signers) { %i[alex jeff] } + let(:data) { params.merge(scope: :write_users) } + + let(:do_request) do + post_json '/api/v2/management/users/import', + multisig_jwt_management_api_v2({ data: data }, *signers) + end + + let!(:email) { 'valid_email@example.com' } + let!(:password) { 'Fai5aesoLEcx' } + let!(:password_digest) do + User.new(password: password).send(:password_digest) + end + let(:params) do + { email: email, password_digest: password_digest } + end + + context 'when email and password_hash are valid' do + it 'creates an user and signs in with credentials' do + expect { do_request }.to change { User.count }.by(1) + expect_status_to_eq 201 + expect(json_body).to include(:email, :uid, :role, :level, + :state, :otp, :profiles) + + # TODO: Check if imported user is able to login + end + end + + context 'when params are blank' do + let(:params) { {} } + + it 'renders an error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(error: 'email is missing, email is empty, password_digest is missing, password_digest is empty') + end + end + + context 'when phone is provided' do + let(:params) do + { + email: email, + password_digest: password_digest, + phone: phone + } + end + + context 'when phone is valid' do + let(:phone) { build(:phone).number } + + it 'creates a phone' do + expect { do_request }.to change { Phone.count }.by(1) + expect_status_to_eq 201 + end + end + + context 'when phone is invalid' do + let(:phone) { '12345' } + + it 'renders an error' do + expect { do_request }.to_not change { Phone.count } + expect_status_to_eq 422 + expect(json_body[:error]).to eq ['Number is invalid'] + end + end + end + + context 'when profile params are provided' do + let(:params) do + { + email: email, + password_digest: password_digest + }.merge(profile_params) + end + + context 'when params are valid' do + let(:profile_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + it 'creates a profile' do + expect { do_request }.to change { Profile.count }.by(1) + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + expect(result['profiles'][0]['state']).to eq 'drafted' + expect_status_to_eq 201 + end + end + + context 'when postcode is not provided' do + let(:profile_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state + } + end + + it 'create a profile' do + expect { do_request }.to change { Profile.count } + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + expect(result['profiles'][0]['state']).to eq 'drafted' + expect_status_to_eq 201 + end + end + + context 'when profile state is provided' do + let(:profile_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + state: 'rejected' + } + end + + it 'create a profile' do + expect { do_request }.to change { Profile.count } + result = JSON.parse(response.body) + expect(result['profiles'][0]['last_name']).to eq profile_params[:last_name] + expect(result['profiles'][0]['dob']).to eq profile_params[:dob].to_s + expect(result['profiles'][0]['state']).to eq 'rejected' + expect_status_to_eq 201 + end + end + + context 'when params are invalid' do + let(:profile_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + country: 'a', + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + it 'renders an error' do + expect { do_request }.to_not change { Profile.count } + expect_status_to_eq 422 + expect(json_body[:error]).to eq ['Country must have alpha2 or alpha3 format'] + end + end + end + end + end diff --git a/spec/api/v2/public/general_spec.rb b/spec/api/v2/public/general_spec.rb new file mode 100644 index 0000000..289064a --- /dev/null +++ b/spec/api/v2/public/general_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::V2::Public::General do + describe 'GET /api/v2/public/time' do + it 'returns a server status' do + get '/api/v2/public/ping' + expect_status_to_eq(200) + expect(json_body[:ping]).to eq('pong') + end + end + + describe 'GET /api/v2/public/time' do + it 'returns a current UNIX time' do + get '/api/v2/public/time' + expect_status_to_eq(200) + expect(json_body[:time].to_i).to be <= Time.now.to_i + end + end + + describe 'GET /api/v2/public/configs' do + it 'returns some of the configurations' do + get '/api/v2/public/configs' + expect_status_to_eq(200) + expect(json_body[:session_expire_time]).to eq(Barong::App.config.session_expire_time) + expect(json_body[:captcha_type]).to eq(Barong::App.config.captcha) + expect(json_body[:phone_verification_type]).to eq(Barong::App.config.phone_verification) + end + + it 'returns all of the configurations with defaults' do + get '/api/v2/public/configs' + expect_status_to_eq(200) + expect(json_body[:session_expire_time]).to eq(Barong::App.config.session_expire_time) + expect(json_body[:captcha_type]).to eq(Barong::App.config.captcha) + expect(json_body[:phone_verification_type]).to eq(Barong::App.config.phone_verification) + expect(json_body[:password_min_entropy]).to eq(Barong::App.config.password_min_entropy) + expect(json_body[:password_regexp]).to eq(Barong::App.config.password_regexp.to_s) + end + end +end diff --git a/spec/api/v2/queries/activity_filter_spec.rb b/spec/api/v2/queries/activity_filter_spec.rb new file mode 100644 index 0000000..ae84d30 --- /dev/null +++ b/spec/api/v2/queries/activity_filter_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +describe API::V2::Queries::ActivityFilter do + let(:initial_scope) { Activity.all } + let(:params) {{topic: 'session', action: 'login', uid: 'UI12345'}} + subject { described_class.new(initial_scope).call(params) } + + it 'filters by uid and action' do + case ActiveRecord::Base.connection.adapter_name + when 'Mysql2' + expect(subject.to_sql).to include("`topic` = 'session'") + expect(subject.to_sql).to include("`action` = 'login'") + expect(subject.to_sql).to include("`uid` = 'UI12345'") + when 'PostgreSQL' + expect(subject.to_sql).to include("\"topic\" = 'session'") + expect(subject.to_sql).to include("\"action\" = 'login'") + expect(subject.to_sql).to include("\"uid\" = 'UI12345'") + else + raise "Unsupported adapter: #{ActiveRecord::Base.connection.adapter_name}" + end + end +end diff --git a/spec/api/v2/resource/addresses_spec.rb b/spec/api/v2/resource/addresses_spec.rb new file mode 100644 index 0000000..f333e99 --- /dev/null +++ b/spec/api/v2/resource/addresses_spec.rb @@ -0,0 +1,135 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Addresses API test' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:image) { fixture_file_upload('/files/documents_test.jpg', 'image/jpg') } + before do + allow(Barong::App.config).to receive_messages(required_docs_expire: false) + end + + describe 'POST /api/v2/resource/addresses/' do + let(:params) do + { + country: 'UA', + address: 'Yaroslaviv Val 15', + city: 'Kiev', + postcode: '101010', + upload: [ + image + ] + } + end + + let(:last_document) { Document.last } + + it 'saves 10 address documents successfully' do + 10.times do + post '/api/v2/resource/addresses', headers: auth_header, + params: { + country: 'UA', + address: 'Yaroslaviv Val 15', + city: 'Kiev', + postcode: '101010', + upload: [fixture_file_upload('/files/documents_test.jpg', 'image/jpg')] + } + end + + expect(response.status).to eq(201) + end + + it 'uploads 2 files at once' do + post '/api/v2/resource/addresses', headers: auth_header, + params: { + country: 'UA', + address: 'Yaroslaviv Val 15', + city: 'Kiev', + postcode: '101010', + upload: [ + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg') + ] + } + expect(response.status).to eq(201) + expect(test_user.documents.length).to eq(2) + end + + it 'uploads 3 files at once' do + post '/api/v2/resource/addresses', headers: auth_header, + params: { + country: 'UA', + address: 'Yaroslaviv Val 15', + city: 'Kiev', + postcode: '101010', + upload: [ + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg') + ] + } + expect(response.status).to eq(201) + expect(test_user.documents.length).to eq(3) + end + + it 'Checks if params are ok and returns success' do + post '/api/v2/resource/addresses', headers: auth_header, params: params + expect(response.status).to eq(201) + end + + it 'Creates a pending address label' do + expect(test_user.labels.find_by(key: :address)).to eq(nil) + post '/api/v2/resource/addresses', headers: auth_header, params: params + expect(response.status).to eq(201) + expect(test_user.labels.find_by(key: :address)).not_to eq(nil) + expect(test_user.labels.find_by(key: :address).value).to eq('pending') + end + + it 'Update rejected or verified label to pending documents label on new doc' do + expect(test_user.labels.find_by(key: :address)).to eq(nil) + test_user.labels.create(key: :address, value: 'verified') + test_user.reload + + post '/api/v2/resource/addresses', headers: auth_header, params: params + expect(response.status).to eq(201) + expect(test_user.labels.find_by(key: :address)).not_to eq(nil) + expect(test_user.labels.find_by(key: :address).value).to eq('pending') + end + + it 'triggers KYCService' do + expect(KycService).to receive(:address_step) + post '/api/v2/resource/addresses', headers: auth_header, params: params + expect(response.status).to eq(201) + end + end + + context 'event API behavior' do + let!(:url) { '/api/v2/resource/addresses' } + let(:request_params) do + { + country: 'UA', + address: 'Yaroslaviv Val 15', + city: 'Kiev', + postcode: '101010', + upload: [ + image + ] + } + end + + before do + allow(EventAPI).to receive(:notify) + end + + it 'receive model.document.created notify' do + expect(EventAPI).to receive(:notify).ordered.with('model.user.created', hash_including(:record)) + expect(EventAPI).to receive(:notify).ordered.with('model.document.created', hash_including(:record)) + + post url, headers: auth_header, params: request_params + end + end +end diff --git a/spec/api/v2/resource/api_keys_spec.rb b/spec/api/v2/resource/api_keys_spec.rb new file mode 100644 index 0000000..b6ad9d1 --- /dev/null +++ b/spec/api/v2/resource/api_keys_spec.rb @@ -0,0 +1,263 @@ +# frozen_string_literal: true + +describe 'Api::V2::APIKeys' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:test_user) { create(:user, otp: otp_enabled) } + let(:otp_enabled) { true } + let!(:first_api_key) { create :api_key, key_holder_account: test_user } + let!(:second_api_key) { create :api_key, key_holder_account: test_user } + let(:valid_otp_code) { '1357' } + let(:invalid_otp_code) { '1234' } + let(:otp_code) { valid_otp_code } + + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, valid_otp_code) { true } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, invalid_otp_code) { false } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, nil) { false } + end + + describe 'GET /api/v2/resource/api_keys/' do + let(:get_params) do + { + ordering: 'asc', + oreder_by: 'id' + } + end + let(:post_params) do + { + scope: 'trade', + algorithm: 'HS256' + } + end + let(:do_request) do + post '/api/v2/resource/api_keys', + params: post_params.merge(totp_code: otp_code), + headers: auth_header + end + let(:do_get_request) { get "/api/v2/resource/api_keys", params: get_params, headers: auth_header } + let(:expected_fields) do + { + kid: first_api_key.kid, + state: first_api_key.state, + scope: %w[trade] + } + end + let(:totp_code) { valid_otp_code } + + it 'Return api key for current account in ASC order' do + do_get_request + + expect(json_body.count).to eq 2 + expect(response.status).to eq(200) + expect(json_body.first).to include(expected_fields) + end + + it 'Return api key for current account in DESC order' do + get_params.merge!(ordering: 'desc') + do_get_request + + expect(json_body.count).to eq 2 + expect(response.status).to eq(200) + expect(json_body.second).to include(expected_fields) + end + + context 'when invalid ordering' do + it 'renders an error' do + get_params.merge!(ordering: 'resc') + do_get_request + + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_ordering"]) + end + end + + context 'when invalid order_by' do + it 'renders an error' do + get_params.merge!(order_by: 'invalid') + do_get_request + + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_attribute"]) + end + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.api_key.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_totp"]) + end + end + end + + describe 'POST /api/v2/resource/api_keys' do + let(:do_request) do + post '/api/v2/resource/api_keys', + params: params.merge(totp_code: otp_code), + headers: auth_header + end + let(:otp_code) { valid_otp_code } + + context 'when fields are valid' do + let(:params) do + { + scope: 'trade', + algorithm: 'HS256' + } + end + let(:expected_fields) do + { + state: 'active', + scope: params[:scope].split(','), + algorithm: 'HS256' + } + end + + it 'Create an api key' do + expect { do_request }.to change { APIKey.count }.by(1) + expect(response.status).to eq(201) + expect_body.to include(expected_fields) + end + + it 'does not create api key if vault is down' do + allow(Vault::Rails).to receive(:encrypt).and_raise(Vault::VaultError) + expect { do_request }.not_to change { APIKey.count } + expect(response.status).to eq(422) + expect_body.to eq(errors: ["api_key.could_not_save_secret"]) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.api_key.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_totp"]) + end + end + + context 'when algorithm is invalid' do + it 'renders an error' do + params[:algorithm] = 'kek' + do_request + expect_body.to eq(errors: ["kid.blank", "algorithm.inclusion"]) + end + end + end + end + + describe 'PATCH /api/v2/resource/api_keys/:kid' do + let(:do_request) do + patch "/api/v2/resource/api_keys/#{first_api_key.kid}", params: params.merge(totp_code: otp_code), + headers: auth_header + end + let(:otp_code) { valid_otp_code } + context 'when valid fields' do + let(:params) do + { + state: 'inactive', + scope: 'sell', + algorithm: 'HS256' + } + end + + it 'Updates a state' do + expect { do_request }.to change { first_api_key.reload.state } + .from('active').to('inactive') + expect(response.status).to eq(200) + end + + it 'Updates a scope' do + expect { do_request }.to change { first_api_key.reload.scope } + .from(['trade']).to(['sell']) + expect(response.status).to eq(200) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.api_key.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_totp"]) + end + end + end + end + + describe 'DELETE /api/v2/resource/api_keys/:uid' do + let(:do_request) do + delete "/api/v2/resource/api_keys/#{first_api_key.kid}?totp_code=#{otp_code}", + headers: auth_header + end + let(:otp_code) { valid_otp_code } + + it 'Removes an api key' do + do_request + expect(response.status).to eq(204) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.api_key.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_totp"]) + end + end + end +end diff --git a/spec/api/v2/resource/data_storage_spec.rb b/spec/api/v2/resource/data_storage_spec.rb new file mode 100644 index 0000000..e9e1d49 --- /dev/null +++ b/spec/api/v2/resource/data_storage_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +describe 'Api::V2::Resource::DataStorage' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + describe 'POST /api/v2/resource/data_storage' do + let(:url) { '/api/v2/resource/data_storage' } + + context 'successful creation' do + let(:valid_params) do + { + title: 'personal', + data: { wife_last_name: Faker::Name.last_name, wife_first_name: Faker::Name.first_name }.to_json + } + end + + it 'creates data_storage record' do + expect { post url, params: valid_params, headers: auth_header }.to change { DataStorage.count }.by(1) + expect(response.status).to eq(201) + end + + it 'creates a label when recording data_storage' do + expect { post url, params: valid_params, headers: auth_header }.to change { Label.count }.by(1) + expect(response.status).to eq(201) + expect(test_user.labels.find_by(key: valid_params[:title])).not_to be_nil + end + end + + context 'errors' do + let(:non_json_data_params) { { title: 'personal', data: 'My name is John Doe. Hello, World!' } } + let(:non_whitelisted_params) { { title: 'family_info', data: { wife_first_name: Faker::Name.first_name }.to_json } } + let(:blacklisted_params) { { title: 'document', data: { approved: true }.to_json } } + + it 'doesnt accept non-json data' do + expect { post url, params: non_json_data_params, headers: auth_header }.not_to change { DataStorage.count } + + expect(response.status).to eq(422) + expect(json_body).to eq({errors: ['data.invalid_format']}) + end + + it 'doesnt accept non-whitelisted title' do + expect { post url, params: non_whitelisted_params, headers: auth_header }.not_to change { DataStorage.count } + expect(response.status).to eq(422) + expect(json_body).to eq({errors: ['title.inclusion']}) + end + + it 'doesnt accept blacklisted title' do + expect { post url, params: blacklisted_params, headers: auth_header }.not_to change { DataStorage.count } + expect(response.status).to eq(422) + expect(json_body).to eq({errors: ['title.inclusion', 'title.exclusion']}) + end + end + end +end diff --git a/spec/api/v2/resource/documents_spec.rb b/spec/api/v2/resource/documents_spec.rb new file mode 100644 index 0000000..8a1d9b0 --- /dev/null +++ b/spec/api/v2/resource/documents_spec.rb @@ -0,0 +1,288 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Documents API test' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:image) { fixture_file_upload('/files/documents_test.jpg', 'image/jpg') } + + describe 'POST /api/v2/resource/documents/' do + let(:params) do + { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + doc_category: 'front_side', + upload: [ + image + ] + } + end + + let!(:optional_params) do + { + metadata: { country: Faker::Address.country }.to_json + } + end + + let(:last_document) { Document.last } + + it 'saves 10 documents successfully' do + 10.times do + post '/api/v2/resource/documents', headers: auth_header, + params: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [fixture_file_upload('/files/documents_test.jpg', 'image/jpg')] + } + end + + expect(response.status).to eq(201) + end + + it 'renders an error when max documents already reached' do + 11.times do + post '/api/v2/resource/documents', headers: auth_header, + params: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [fixture_file_upload('/files/documents_test.jpg', 'image/jpg')] + } + end + + expect(response.status).to eq(400) + expect_body.to eq(errors: ['resource.documents.limit_will_be_reached']) + end + + it 'uploads 2 files at once' do + post '/api/v2/resource/documents', headers: auth_header, + params: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [ + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg') + ] + } + expect(response.status).to eq(201) + expect(test_user.documents.length).to eq(2) + end + + it 'uploads 3 files at once' do + post '/api/v2/resource/documents', headers: auth_header, + params: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [ + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg') + ] + } + expect(response.status).to eq(201) + expect(test_user.documents.length).to eq(3) + end + + it 'doesn\'t upload more than 10 files at once' do + post '/api/v2/resource/documents', headers: auth_header, + params: { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [ + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg'), + fixture_file_upload('/files/documents_test.jpg', 'image/jpg') + ] + } + expect(response.status).to eq(400) + end + + it 'Checks if params are ok and returns success' do + post '/api/v2/resource/documents', headers: auth_header, params: params + expect(response.status).to eq(201) + end + + it 'Creates a pending documents label' do + expect(test_user.labels.find_by(key: :document)).to eq(nil) + post '/api/v2/resource/documents', headers: auth_header, params: params + expect(response.status).to eq(201) + expect(test_user.labels.find_by(key: :document)).to eq(nil) + post '/api/v2/resource/documents', headers: auth_header, + params: params.merge({ doc_category: 'selfie', + upload: [fixture_file_upload('/files/documents_test.jpg', 'image/jpg')], + identificator: test_user.documents.last.identificator }) + expect(test_user.labels.find_by(key: :document).value).to eq('pending') + end + + it 'Update rejected or verified label to pending documents label on new doc' do + expect(test_user.labels.find_by(key: :document)).to eq(nil) + test_user.labels.create(key: :document, value: 'verified') + test_user.reload + + post '/api/v2/resource/documents', headers: auth_header, params: params + expect(response.status).to eq(201) + expect(test_user.labels.find_by(key: :document)).not_to eq(nil) + expect(test_user.labels.find_by(key: :document).value).to eq('verified') + post '/api/v2/resource/documents', headers: auth_header, + params: params.merge({ doc_category: 'selfie', + upload: [fixture_file_upload('/files/documents_test.jpg', 'image/jpg')], + identificator: test_user.documents.last.identificator }) + expect(test_user.labels.find_by(key: :document).value).to eq('pending') + end + + it 'triggers KYCService' do + expect(KycService).to receive(:document_step) + post '/api/v2/resource/documents', headers: auth_header, params: params + expect(response.status).to eq(201) + end + + it 'Creates document with optional params and returns success' do + post '/api/v2/resource/documents', headers: auth_header, params: params.merge(optional_params) + expect(response.status).to eq(201) + expect(last_document.metadata).to eq(optional_params[:metadata]) + end + + it 'renders an error if metadata is not json' do + post '/api/v2/resource/documents', headers: auth_header, params: params.merge({ metadata: '{ bar: baz }' }) + expect_status_to_eq 422 + expect_body.to eq(errors: ['metadata.invalid_format']) + end + + it 'Checks provided params and returns error, cause some of them are not valid or absent' do + post '/api/v2/resource/documents', params: params.except(:doc_type), headers: auth_header + expect_body.to eq(errors: ['resource.document.missing_doc_type', 'resource.document.empty_doc_type']) + expect(response.status).to eq(422) + + post '/api/v2/resource/documents', params: params.except(:doc_number), headers: auth_header + expect_body.to eq(errors: ['resource.document.missing_doc_number', 'resource.document.empty_doc_number']) + expect(response.status).to eq(422) + + post '/api/v2/resource/documents', params: params.except(:upload), headers: auth_header + expect_body.to eq(errors: ['resource.document.missing_upload']) + expect(response.status).to eq(422) + + post '/api/v2/resource/documents', params: params.except(:doc_expire).merge(doc_expire: 'blah'), headers: auth_header + expect_body.to eq(errors: ['resource.documents.expire_not_a_date']) + expect(response.status).to eq(422) + + params0 = params + params0[:upload] = [Faker::Avatar.image] + post '/api/v2/resource/documents', params: params0, headers: auth_header + expect_body.to eq(errors: ['upload.blank']) + expect(response.status).to eq(422) + end + + it 'Does not return error when docs expire is optional' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(false) + post '/api/v2/resource/documents', params: params.except(:doc_expire), headers: auth_header + expect(response.status).to eq(201) + end + + it 'Returns error when docs expire is not optional' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(true) + post '/api/v2/resource/documents', params: params.except(:doc_expire), headers: auth_header + expect(response.status).to eq(422) + expect_body.to eq({ errors: ['resource.documents.invalid_format'] }) + end + + it 'Returns error when docs expire is not optional and date in past' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(true) + post '/api/v2/resource/documents', params: params.merge({ doc_expire: DateTime.now.to_date - 1 }), headers: auth_header + expect(response.status).to eq(422) + expect_body.to eq({ errors: ['resource.documents.already_expired'] }) + end + + it 'Doesnt return error when docs expire is optional and date in past' do + allow(Barong::App.config).to receive(:required_docs_expire).and_return(false) + post '/api/v2/resource/documents', params: params.merge({ doc_expire: DateTime.now.to_date - 1 }), headers: auth_header + expect(response.status).to eq(201) + end + + it 'Returns user all his documents' do + post '/api/v2/resource/documents', params: params, headers: auth_header + expect(response.status).to eq(201) + + get '/api/v2/resource/documents', headers: auth_header + response_arr = JSON.parse(response.body) + + expect(response_arr.count).to eq(1) + expect(response_arr.last['upload']).to_not be_nil + expect(response_arr.last['doc_type']).to eq('Passport') + expect(response_arr.last['doc_expire']).to eq('3020-01-22') + expect(response_arr.last['doc_number']).to eq('AA****BB') + expect(response.status).to eq(200) + end + + context 'without masking' do + before do + Barong::App.config.stub(:api_data_masking_enabled).and_return(false) + end + + it 'Returns user all his documents' do + post '/api/v2/resource/documents', params: params, headers: auth_header + expect(response.status).to eq(201) + + get '/api/v2/resource/documents', headers: auth_header + response_arr = JSON.parse(response.body) + + expect(response_arr.count).to eq(1) + expect(response_arr.last['upload']).to_not be_nil + expect(response_arr.last['doc_type']).to eq('Passport') + expect(response_arr.last['doc_expire']).to eq('3020-01-22') + expect(response_arr.last['doc_number']).to eq('AA1234BB') + expect(response.status).to eq(200) + end + end + + it 'Returns error without token' do + post '/api/v2/resource/documents', params: params + expect(response.status).to eq(401) + + get '/api/v2/resource/documents', params: params + expect(response.status).to eq(401) + end + after(:all) { User.destroy_all } + end + + context 'event API behavior' do + let!(:url) { '/api/v2/resource/documents' } + let(:request_params) do + { + doc_type: 'Passport', + doc_expire: '3020-01-22', + doc_number: 'AA1234BB', + upload: [ + image + ] + } + end + + before do + allow(EventAPI).to receive(:notify) + end + + it 'receive model.document.created notify' do + expect(EventAPI).to receive(:notify).ordered.with('model.user.created', hash_including(:record)) + expect(EventAPI).to receive(:notify).ordered.with('model.document.created', hash_including(:record)) + + post url, headers: auth_header, params: request_params + end + end +end diff --git a/spec/api/v2/resource/labels_spec.rb b/spec/api/v2/resource/labels_spec.rb new file mode 100644 index 0000000..b44139f --- /dev/null +++ b/spec/api/v2/resource/labels_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Labels API.' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let(:post_params) do + { + key: ::Faker::Internet.slug(glue: '-'), + value: ::Faker::Internet.slug(glue: '-') + } + end + + context 'Happy paths for the current user.' do + let(:test_user) { create(:user, role: "admin") } + let!(:label) { create :label, user: test_user } + let!(:label2) { create :label, user: test_user } + let!(:label3) { create :label, user: test_user } + + it 'Return lables for current user' do + get '/api/v2/resource/labels', headers: auth_header + + expect(response.status).to eq(200) + expect(json_body.size).to eq(3) + expect(json_body.last[:key]).to eq(label3.key) + end + + it 'Return a label by key' do + get "/api/v2/resource/labels/#{label.key}", headers: auth_header + expect(response.status).to eq(200) + expect(json_body[:key]).to eq(label.key) + end + + it 'Create a label' do + post '/api/v2/resource/labels', params: post_params, headers: auth_header + expect(response.status).to eq(201) + persisted = Label.find_by(key: json_body[:key]) + expect(persisted.key).to eq(post_params[:key]) + expect(persisted.value).to eq(post_params[:value]) + expect(persisted.scope).to eq('public') + expect(persisted.user_id).to eq(test_user.id) + end + + it 'Update a label' do + patch "/api/v2/resource/labels/#{label.key}", params: post_params, headers: auth_header + expect(response.status).to eq(200) + persisted = Label.find_by(key: label.key) + expect(persisted.value).to eq(post_params[:value]) + expect(persisted.scope).to eq('public') + end + + it 'Delete a label' do + delete "/api/v2/resource/labels/#{label.key}", headers: auth_header + expect(response.status).to eq(204) + expect(Label.find_by(key: label.key)).to be_nil + end + end + + context 'Errors.' do + let(:test_user) { create(:user, role: "admin") } + let!(:label) { create :label, user: test_user } + + it 'Respond with errors on create if existing key is used for current user' do + post '/api/v2/resource/labels', params: post_params.merge(key: label.key), headers: auth_header + expect(response.status).to eq(422) + expect(response.body).to be_include('key.taken') + end + + it 'Respond with error if attempted to update a private label' do + label.update(scope: 'private') + patch "/api/v2/resource/labels/#{label.key}", params: post_params, headers: auth_header + expect(response.status).to eq(400) + end + + it 'Respond with error if key not found' do + get '/api/v2/resource/labels/blah', headers: auth_header + expect(response.status).to eq(404) + end + end +end diff --git a/spec/api/v2/resource/otp_spec.rb b/spec/api/v2/resource/otp_spec.rb new file mode 100644 index 0000000..4c85ef0 --- /dev/null +++ b/spec/api/v2/resource/otp_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +describe 'Api::V2::Resource::Otp' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:do_request) { post '/api/v2/resource/otp/enable', headers: auth_header } + let(:otp_code) { '111111' } + + context 'valid request' do + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, otp_code) { true } + end + + it 'user enables 2fa successfully' do + post '/api/v2/resource/otp/enable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 201 + expect(test_user.reload.otp).to eq true + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).not_to eq nil + end + + it 'user disables 2fa successfully' do + test_user.update(otp: true) + test_user.labels.create(value: 'verified', key: :otp, scope: :private) + post '/api/v2/resource/otp/disable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 200 + expect(test_user.reload.otp).to eq false + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + it 'user verify otp Code' do + + test_user.update(otp: true) + test_user.labels.create(value: 'verified', key: :otp, scope: :private) + + post '/api/v2/resource/otp/verify', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 201 + expect(test_user.reload.otp).to eq true + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).not_to eq nil + end + end + + context 'incomplete request' do + it 'user receives error in enabling' do + post '/api/v2/resource/otp/enable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 422 + expect(test_user.reload.otp).to eq false + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + it 'user receives error in disabling' do + test_user.update(otp: true) + test_user.labels.create(key: :otp, scope: :private) + post '/api/v2/resource/otp/disable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 422 + expect(test_user.reload.otp).to eq true + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + end + + + context 'already done' do + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, otp_code) { true } + end + + it 'enabling otp' do + test_user.update(otp: true) + test_user.labels.create(value: 'verified', key: :otp, scope: :private) + post '/api/v2/resource/otp/enable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 400 + expect(test_user.reload.otp).to eq true + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).not_to eq nil + end + + it 'disabling otp' do + post '/api/v2/resource/otp/disable', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 400 + expect(test_user.reload.otp).to eq false + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + it 'otp disabled' do + post '/api/v2/resource/otp/verify', headers: auth_header, params: { + code: otp_code + } + + expect(response.status).to eq 400 + expect(test_user.reload.otp).to eq false + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).to eq nil + end + + it 'otp enabled' do + test_user.update(otp: true) + test_user.labels.create(value: 'verified', key: :otp, scope: :private) + post '/api/v2/resource/otp/generate_qrcode', headers: auth_header + + expect(response.status).to eq 400 + expect(test_user.reload.otp).to eq true + expect(test_user.reload.labels.find_by(key: :otp, scope: :private)).not_to eq nil + end + end +end diff --git a/spec/api/v2/resource/phones_spec.rb b/spec/api/v2/resource/phones_spec.rb new file mode 100644 index 0000000..37e5bf4 --- /dev/null +++ b/spec/api/v2/resource/phones_spec.rb @@ -0,0 +1,469 @@ +# frozen_string_literal: true + +describe 'Api::V2::Resources::Phones' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + context 'Twilio Verify Service' do + before do + allow(Barong::App.config).to receive(:twilio_provider).and_return(TwilioVerifyService) + end + + describe 'GET /api/v2/resource/phones' do + let!(:phone1) { create(:phone, user: test_user) } + let!(:phone2) { create(:phone, user: test_user, number: 12345677711 ) } + let(:do_request) do + get '/api/v2/resource/phones', headers: auth_header + end + + it 'returns list of user\'s phones' do + do_request + + expect(json_body.length).to eq 2 + + expect(json_body.first[:number]).to eq phone1.sub_masked_number + expect(json_body.first[:country]).to eq phone1.country + expect(json_body.first[:validated_at]).to eq phone1.validated_at + expect(json_body.second[:number]).to eq phone2.sub_masked_number + expect(json_body.second[:country]).to eq phone2.country + expect(json_body.second[:validated_at]).to eq phone2.validated_at + end + + context 'list of users phone without masking' do + before do + Barong::App.config.stub(:api_data_masking_enabled).and_return(false) + end + + it 'returns list of user\'s phones' do + do_request + + expect(json_body.length).to eq 2 + + expect(json_body.first[:number]).to eq phone1.number + expect(json_body.first[:country]).to eq phone1.country + expect(json_body.first[:validated_at]).to eq phone1.validated_at + expect(json_body.second[:number]).to eq phone2.number + expect(json_body.second[:country]).to eq phone2.country + expect(json_body.second[:validated_at]).to eq phone2.validated_at + end + end + end + + describe 'POST /api/v2/resource/phones' do + let(:do_request) do + post '/api/v2/resource/phones', params: params, headers: auth_header + end + let(:phone_number) { nil } + + let(:params) { { phone_number: phone_number, channel: 'sms' } } + + describe 'errors: ' do + context 'when phone is missing' do + let(:phone_number) { nil } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.empty_phone_number"]) + expect_status.to eq 422 + end + end + + context 'when phone is invalid' do + let(:phone_number) { '123' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.invalid_num"]) + expect_status.to eq 400 + end + end + + context 'when phone is already exists' do + let!(:phone) do + create(:phone, validated_at: validated_at) + end + let(:phone_number) { phone.number } + + context 'when phone verified' do + let(:validated_at) { 1.minutes.ago } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.number_exist"]) + expect_status.to eq 400 + end + end + + context 'when phone is not verified' do + let(:validated_at) { 1.minutes.ago } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.number_exist"]) + expect_status.to eq 400 + end + end + + context 'when phone verified but number is not sanitized' do + let(:validated_at) { 1.minutes.ago } + let(:phone_number) { "++#{phone.number}" } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.number_exist"]) + expect_status.to eq 400 + end + end + end + end + + context 'valid story' do + before do + allow(Barong::App.config.twilio_provider).to receive(:send_code).and_return(false) + end + + context 'when phone is not verified' do + let(:phone_number) { build(:phone).number } + + it 'assigns a phone to account and send sms' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + end + end + + context 'when phone is valid' do + let(:phone_number) { build(:phone).number } + + it 'creates a phone and send code via sms channel' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + end + + it 'creates a phone and send code via call channel' do + params['channel'] = 'call' + + do_request + expect_body.to eq(message: 'Code was sent successfully via call') + expect_status.to eq 201 + end + end + end + + context 'when phone is on national format with zero' do + let(:phone_number) { '+44 07418084106' } + let(:international_phone) { '447418084106' } + + it 'creates a phone and send sms' do + allow(Barong::App.config.twilio_provider).to receive(:send_code).and_return(false) + + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(Phone.last.number).to eq(international_phone) + end + end + + context 'when phone exists on international format' do + let(:phone_number) { '+44 07418084106' } + let(:international_phone) { '447418084106' } + let!(:phone) do + create(:phone, validated_at: 1.minute.ago, number: international_phone) + end + + it 'renders an error' do + do_request + expect_body.to eq errors: ["resource.phone.number_exist"] + expect_status.to eq 400 + end + end + end + + describe 'POST /api/v2/resource/phones/verify' do + let(:do_request) do + post '/api/v2/resource/phones/verify', params: params, headers: auth_header + end + let(:params) do + { + phone_number: phone_number, + verification_code: verification_code + } + end + let(:verification_code) { '12345' } + + context 'when phone is missing and code is missing' do + let(:phone_number) { nil } + let(:verification_code) { '' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.empty_phone_number", "resource.phone.empty_verification_code"]) + expect_status.to eq 422 + end + end + + context 'when phone is invalid' do + let(:phone_number) { '123' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.invalid_num"]) + expect_status.to eq 400 + end + end + + context 'when phone is already exists and verified' do + let!(:phone) do + create(:phone, validated_at: 1.minutes.ago) + end + let(:phone_number) { phone.number } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.number_exist"]) + expect_status.to eq 400 + end + end + + context 'when phone exists in international format' do + let!(:phone) do + create(:phone, validated_at: 1.minutes.ago, number: international_phone) + end + let(:phone_number) { '+44 07418084106' } + let(:international_phone) { '447418084106' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ["resource.phone.number_exist"]) + expect_status.to eq 400 + end + end + + context 'when phone is not found in test_user' do + let!(:phone) { create(:phone) } + let(:phone_number) { phone.number } + + it 'rendens an error' do + allow(Barong::App.config.twilio_provider).to receive(:verify_code?).and_return(false) + + do_request + expect_body.to eq(errors: ["resource.phone.doesnt_exist"]) + expect_status.to eq 404 + end + end + + context 'when phone is found in test_user but code is invalid' do + let!(:phone) { create(:phone, user: test_user) } + let(:phone_number) { phone.number } + + it 'rendens an error' do + allow(Barong::App.config.twilio_provider).to receive(:verify_code?).and_return(false) + + do_request + expect_body.to eq(errors: ["resource.phone.verification_invalid"]) + expect_status.to eq 404 + end + end + + context 'when phone and code is valid' do + let(:phone) { create(:phone, user: test_user) } + let(:phone_number) { phone.number } + + it 'responses with success' do + allow(Barong::App.config.twilio_provider).to receive(:verify_code?).and_return(true) + + set_level(test_user, 1) + do_request + expect_status.to eq 201 + expect(phone.reload.validated_at).to be + test_user.update_level + expect(test_user.reload.level).to eq 2 + end + end + end + end + + context 'With Twilio SMS Sender service' do + let(:phone_number) { phone.number } + let(:do_request) do + post '/api/v2/resource/phones', params: params, headers: auth_header + end + let(:params) { { phone_number: phone_number, channel: 'sms' } } + let(:verification_code) { '12345' } + + context 'valid story sms sender service' do + let(:mock_sms) { Barong::MockSMS } + + before do + allow(Barong::App.config).to receive(:twilio_provider).and_return(MockPhoneVerifyService) + end + + context 'when phone is not verified' do + let(:phone_number) { build(:phone).number } + + it 'assigns a phone to account and send sms' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.to).to eq "+#{phone_number}" + end + + it 'sends a default sms content' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.body).to start_with('Your verification code for Barong: ') + end + + it 'sends a custom message with content before code' do + allow(Barong::App.config).to receive(:sms_content_template).and_return('Please confirm your phone with the following code: {{code}}') + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.body).to start_with('Please confirm your phone with the following code: ') + end + + it 'sends a custom message with content after code' do + allow(Barong::App.config).to receive(:sms_content_template).and_return('{{code}} - this is your confirmation code') + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.body).to end_with(' - this is your confirmation code') + end + + it 'sends a custom message with code in the midle of the content' do + allow(Barong::App.config).to receive(:sms_content_template).and_return('Following code: {{code}} should be used for phone confirmation') + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.body).to start_with('Following code: ') + expect(mock_sms.messages.last.body).to end_with(' should be used for phone confirmation') + end + end + + context 'when phone is valid' do + let(:phone_number) { build(:phone).number } + + it 'creates a phone and send sms' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + expect(mock_sms.messages.last.to).to eq "+#{phone_number}" + end + + it 'doesnt change code in DB on phone initialize' do + do_request + expect_body.to eq(message: 'Code was sent successfully via sms') + expect_status.to eq 201 + code_after_create = Phone.last.code + # Phone.last initilazes phone + code_after_initialize = Phone.last.code + expect(code_after_create).to eq code_after_initialize + end + end + end + + context 'POST /api/v2/resource/phones/verify' do + let(:do_request) do + post '/api/v2/resource/phones/verify', params: params, headers: auth_header + end + let(:params) do + { + phone_number: phone_number, + verification_code: verification_code + } + end + let(:verification_code) { '12345' } + + context 'when phone is missing and code is missing' do + let(:phone_number) { nil } + let(:verification_code) { '' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ['resource.phone.empty_phone_number', 'resource.phone.empty_verification_code']) + expect_status.to eq 422 + end + end + + context 'when phone is invalid' do + let(:phone_number) { '123' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ['resource.phone.invalid_num']) + expect_status.to eq 400 + end + end + + context 'when phone is already exists and verified' do + let!(:phone) do + create(:phone, validated_at: 1.minutes.ago) + end + let(:phone_number) { phone.number } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ['resource.phone.number_exist']) + expect_status.to eq 400 + end + end + + context 'when phone exists in international format' do + let!(:phone) do + create(:phone, validated_at: 1.minutes.ago, number: international_phone) + end + let(:phone_number) { '+44 07418084106' } + let(:international_phone) { '447418084106' } + + it 'renders an error' do + do_request + expect_body.to eq(errors: ['resource.phone.number_exist']) + expect_status.to eq 400 + end + end + + context 'when phone is not found in test_user' do + let!(:phone) { create(:phone) } + let(:phone_number) { phone.number } + + it 'rendens an error' do + do_request + expect_body.to eq(errors: ['resource.phone.doesnt_exist']) + expect_status.to eq 404 + end + end + + context 'when phone is found in test_user but code is invalid still approves because of mock provider' do + let!(:phone) { create(:phone, user: test_user) } + let(:phone_number) { phone.number } + + it 'approves phone' do + do_request + + expect_status.to eq 201 + expect(phone.reload.validated_at).to be + end + end + + context 'when phone and code is valid' do + let(:phone) { create(:phone, user: test_user) } + let(:phone_number) { phone.number } + let(:verification_code) { phone.code } + + it 'responses with success' do + set_level(test_user, 1) + do_request + expect_status.to eq 201 + expect(phone.reload.validated_at).to be + test_user.update_level + expect(test_user.reload.level).to eq 2 + end + end + end + end +end diff --git a/spec/api/v2/resource/profiles_spec.rb b/spec/api/v2/resource/profiles_spec.rb new file mode 100644 index 0000000..9b382e5 --- /dev/null +++ b/spec/api/v2/resource/profiles_spec.rb @@ -0,0 +1,572 @@ +# frozen_string_literal: true + +describe 'API::V2::Resource::Profiles' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'local') } + + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:optional_params) do + { + metadata: { + gender: Faker::Creature::Dog.gender, + place_of_birth: Faker::Address.city + }.to_json + } + end + + describe 'POST /api/v2/resource/profiles' do + describe 'KYC verification' do + let!(:request_params) do + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code, + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + let!(:url) { '/api/v2/resource/profiles' } + + it 'triggers KycService' do + expect(KycService).to receive(:profile_step) + + post url, params: request_params, headers: auth_header + expect(response.status).to eq(201) + end + + context 'Local verification' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'local') } + + it 'adds a label for first verification' do + expect(test_user.labels).to eq([]) + post url, params: request_params, headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('drafted') + end + + it 'doesnt trigger KYCAID worker' do + expect(KycService).to receive(:profile_step) + expect(KYC::Kycaid::ApplicantWorker).not_to receive(:perform_async) + + post url, params: request_params, headers: auth_header + expect(response.status).to eq(201) + end + + it 'updates label on re-submit' do + expect(test_user.labels).to eq([]) + post url, params: request_params, headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('drafted') + + put url, params: request_params.merge(confirm: true), headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('submitted') + end + end + + context 'KYCAID verification' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'kycaid') } + + it 'adds a label for first verification' do + expect(test_user.labels).to eq([]) + post url, params: request_params, headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('drafted') + end + + it 'updates label on re-submit' do + expect(test_user.labels).to eq([]) + post url, params: request_params, headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('drafted') + + put url, params: request_params.merge(confirm: true), headers: auth_header + expect(test_user.reload.labels.count).to eq(1) + expect(test_user.reload.labels.first.key).to eq('profile') + expect(test_user.reload.labels.first.value).to eq('submitted') + end + + it 'triggers KYCAID worker on submitted state' do + expect(KYC::Kycaid::ApplicantWorker).to receive(:perform_async) + post url, params: request_params.merge(confirm: true), headers: auth_header + expect(response.status).to eq(201) + end + + it 'doesnt trigger KYCAID worker on drafted state' do + expect(KYC::Kycaid::ApplicantWorker).not_to receive(:perform_async) + post url, params: request_params, headers: auth_header + expect(response.status).to eq(201) + end + + it 'doesnt trigger KYCAID worker on rejected state' do + post url, params: request_params.merge(confirm: true), headers: auth_header + expect(response.status).to eq(201) + + expect(KYC::Kycaid::ApplicantWorker).not_to receive(:perform_async) + test_user.reload.profiles.last.update(state: 'rejected') + end + + it 'doesnt trigger KYCAID worker on verified state' do + post url, params: request_params.merge(confirm: true), headers: auth_header + expect(response.status).to eq(201) + + expect(KYC::Kycaid::ApplicantWorker).not_to receive(:perform_async) + test_user.reload.profiles.last.update(state: 'verified') + end + end + end + + let!(:url) { '/api/v2/resource/profiles' } + let!(:user_info) { '/api/v2/resource/users/me' } + let!(:request_params) do + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + let!(:asian_params) do + { + last_name: "국", + first_name: "채원", + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: "안산시", + address: "사직로3길 23", + postcode: Faker::Address.zip_code + } + end + + it 'accept / , ; in address' do + request_params[:address] = '28/2 Kevin Brook; Miami, USA' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:address]).to eq request_params[:address] + end + + it 'accept . in address' do + request_params[:address] = 'Larkin Fork.South, New York/AP' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:address]).to eq request_params[:address] + end + + it 'accept # ~ \ : " & ( ) in address' do + request_params[:address] = '28~1/2 \"Kevin & Brook": (Miami, USA)' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:address]).to eq request_params[:address] + end + + it "accept ' in address" do + request_params[:address] = "'Larkin Fork' South New York" + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:address]).to eq request_params[:address] + end + + it "accept – in address" do + request_params[:address] = "'Larkin–Fork' South New York" + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:address]).to eq request_params[:address] + end + + it "doesn't accept @ in address" do + request_params[:address] = "'Larkin–Fork' @South New York" + expect { post url, params: request_params, headers: auth_header } + .not_to change { Profile.count } + expect(response.status).to eq(422) + end + + it 'accept . in city' do + request_params[:city] = 'St. Petersburg' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:city]).to eq request_params[:city] + end + + it 'accept dash in city' do + request_params[:city] = 'Hubli–Dharwad' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:city]).to eq request_params[:city] + end + + it 'accept hyphen in city' do + request_params[:city] = 'Hubli-Dharwad' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:city]).to eq request_params[:city] + end + + it 'accept \' in city' do + request_params[:city] = 'Cava de\' Tirreni' + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + res = json_body + expect(res[:city]).to eq request_params[:city] + end + + it "doesn't accept # in city" do + request_params[:city] = 'Cava de\' #Tirreni' + expect { post url, params: request_params, headers: auth_header } + .not_to change { Profile.count } + expect(response.status).to eq(422) + end + + it "doesn't accept @ in city" do + request_params[:city] = 'Cava de\' @Tirreni' + expect { post url, params: request_params, headers: auth_header } + .not_to change { Profile.count } + expect(response.status).to eq(422) + end + + it 'creates new profile with only required fields' do + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + + res = json_body + expect(res[:last_name]).to eq request_params[:last_name].sub(/(?<=\A.{1})(.*)/) { |match| '*' * match.length } + expect(res[:first_name]).to eq request_params[:first_name] + expect(res[:dob]).to eq request_params[:dob].to_s.sub(/(?<=\A.{8})(.*)/) { |match| '*' * match.length } + expect(res[:country]).to eq request_params[:country] + expect(res[:city]).to eq request_params[:city] + expect(res[:address]).to eq request_params[:address] + expect(res[:postcode]).to eq request_params[:postcode] + expect(res[:metadata]).to be_blank + end + + it 'creates new profile with only required fields without masking' do + Barong::App.config.stub(:api_data_masking_enabled).and_return(false) + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + + res = json_body + expect(res[:last_name]).to eq request_params[:last_name] + expect(res[:first_name]).to eq request_params[:first_name] + expect(res[:dob]).to eq request_params[:dob].to_s + expect(res[:country]).to eq request_params[:country] + expect(res[:city]).to eq request_params[:city] + expect(res[:address]).to eq request_params[:address] + expect(res[:postcode]).to eq request_params[:postcode] + expect(res[:metadata]).to be_blank + end + + it 'creates new profile with corean symbols fields' do + expect { post url, params: asian_params, headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + + res = json_body + + expect(res[:last_name]).to eq asian_params[:last_name] + expect(res[:first_name]).to eq asian_params[:first_name] + expect(res[:dob]).to eq asian_params[:dob].to_s.sub(/(?<=\A.{8})(.*)/) { |match| '*' * match.length } + expect(res[:country]).to eq asian_params[:country] + expect(res[:city]).to eq asian_params[:city] + expect(res[:address]).to eq asian_params[:address] + expect(res[:postcode]).to eq asian_params[:postcode] + expect(res[:metadata]).to be_blank + end + + it 'creates new profile with all metadata fields' do + expect { post url, params: request_params.merge(optional_params), headers: auth_header } + .to change { Profile.count }.by (1) + expect(response.status).to eq(201) + + res = json_body + expect(res[:metadata]).to eq(optional_params[:metadata]) + end + + it 'renders an error if metadata is not json' do + expect { post url, params: request_params.merge({ metadata: '{ bar: baz }' }), headers: auth_header } + .not_to change { Profile.count } + expect_status_to_eq 422 + expect_body.to eq(errors: ["metadata.invalid_format"]) + end + + context 'create another one profile with state drafted' do + before do + post url, params: request_params.merge(optional_params), headers: auth_header + end + + it 'doesn\'t create new profile' do + post url, params: request_params.merge(optional_params), headers: auth_header + + result = json_body + expect(*result[:errors]).to eq('state.exists') + end + end + + context 'create another one profile with state submitted' do + before do + post url, params: request_params.merge(optional_params, confirm: true), headers: auth_header + end + + it 'doesn\'t create new profile' do + post url, params: request_params.merge(optional_params, confirm: true), headers: auth_header + + result = json_body + expect(*result[:errors]).to eq('state.exists') + end + end + + context 'create another one profile with state submitted and drafted' do + before do + post url, params: request_params.merge(optional_params, confirm: true), headers: auth_header + end + + it 'doesn\'t create new profile' do + post url, params: request_params.merge(optional_params), headers: auth_header + + result = json_body + expect(*result[:errors]).to eq('state.exists') + end + end + + context 'partial creating profile' do + context 'empty params' do + + it 'returns profile' do + expect { post url, params: {}, headers: auth_header } + .to change { Profile.count }.by (1) + + expect(response.status).to eq(201) + expect(json_body[:state]).to eq('drafted') + expect(json_body[:metadata].blank?).to be_truthy + Profile::OPTIONAL_PARAMS.each { |p| + expect(json_body[p].blank?).to be_truthy + } + Profile::OPTIONAL_PARAMS.each { |p| + expect(json_body.with_indifferent_access[p].blank?).to be_truthy + } + expect(Profile.last.user.labels.find_by(key: 'profile').value).to eq('drafted') + end + end + + context 'several params' do + let(:params) { { last_name: Faker::Name.last_name, first_name: Faker::Name.first_name } } + + it 'returns profile' do + expect { post url, params: params, headers: auth_header } + .to change { Profile.count }.by (1) + + expect(response.status).to eq(201) + expect(json_body[:first_name].nil?).to be_falsey + expect(json_body[:last_name].nil?).to be_falsey + expect(json_body[:state]).to eq('drafted') + expect(json_body[:metadata].blank?).to be_truthy + (Profile::OPTIONAL_PARAMS - params.stringify_keys.keys).each { |p| + expect(json_body.with_indifferent_access[p].blank?).to be_truthy + } + expect(Profile.last.user.labels.find_by(key: :profile).value).to eq('drafted') + end + end + + context 'several params with profile confirmation' do + let(:params) { { last_name: Faker::Name.last_name, first_name: Faker::Name.first_name } } + + it 'returns profile' do + expect { post url, params: params.merge(confirm: true), headers: auth_header } + .to change { Profile.count }.by (1) + + expect(response.status).to eq(201) + expect(json_body[:first_name].nil?).to be_falsey + expect(json_body[:last_name].nil?).to be_falsey + expect(json_body[:state]).to eq('submitted') + expect(json_body[:metadata].blank?).to be_truthy + (Profile::OPTIONAL_PARAMS - params.stringify_keys.keys).each { |p| + expect(json_body.with_indifferent_access[p].blank?).to be_truthy + } + expect(Profile.last.user.labels.find_by(key: :profile).value).to eq('submitted') + end + end + + context 'full profile params' do + it 'returns profile' do + expect { post url, params: request_params, headers: auth_header } + .to change { Profile.count }.by (1) + + expect(response.status).to eq(201) + expect(json_body[:state]).to eq('drafted') + expect(json_body[:metadata].blank?).to be_truthy + request_params.keys.each { |p| + expect(json_body[p].present?).to be_truthy + } + expect(Profile.last.user.labels.find_by(key: 'profile').value).to eq('drafted') + end + end + end + end + + describe 'GET /api/v2/resource/profiles/me' do + let!(:url) { '/api/v2/resource/profiles/me' } + let!(:request_params) do + { + first_name: Faker::Name.first_name, + last_name: Faker::Name.last_name, + dob: Faker::Date.birthday, + address: Faker::Address.state, + postcode: Faker::Address.zip_code, + city: Faker::Address.city, + country: Faker::Address.country_code_long + } + end + + it 'returns user profile data with metadata' do + post '/api/v2/resource/profiles', params: request_params.merge(optional_params), + headers: auth_header + expect(response.status).to eq(201) + + get url, headers: auth_header + expect(response.status).to eq(200) + last_name = request_params[:last_name].sub(/(?<=\A.{1})(.*)/) { |match| '*' * match.length } + dob = request_params[:dob].to_s.sub(/(?<=\A.{8})(.*)/) { |match| '*' * match.length } + expected_json = request_params.merge(state: 'drafted', last_name: last_name,dob: dob).merge(optional_params).to_json + expect(JSON.parse(response.body)[0].except('created_at', 'updated_at')).to eq(JSON.parse(expected_json)) + end + end + + context 'event API behavior' do + let!(:url) { '/api/v2/resource/profiles' } + let!(:request_params) do + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.state, + postcode: Faker::Address.zip_code + } + end + + before do + allow(EventAPI).to receive(:notify) + end + + it 'receive model.profile.created notify' do + expect(EventAPI).to receive(:notify).ordered.with('model.user.created', hash_including(:record)) + expect(EventAPI).to receive(:notify).ordered.with('model.profile.created', hash_including(:record)) + + post url, params: request_params, headers: auth_header + end + + it 'receive model.profile.created notify with user and profile params' do + expect(EventAPI).to receive(:notify).ordered.with('model.user.created', hash_including(:record)) + expect(EventAPI).to receive(:notify).ordered.with( + 'model.profile.created', + hash_including( + record: { + user: anything, + address: anything, + city: anything, + country: anything, + created_at: anything, + updated_at: anything, + dob: anything, + first_name: anything, + last_name: anything, + postcode: anything + } + ) + ) + + post url, params: request_params, headers: auth_header + end + end + + describe 'PUT /api/v2/resource/profiles' do + let!(:url) { '/api/v2/resource/profiles' } + let!(:request_params) do + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + end + + context 'user without profile' do + it 'renders an error when profile doesnt exist' do + put url, params: request_params, headers: auth_header + + expect_status.to eq(404) + expect_body.to eq(errors: ['resource.profile.doesnt_exist_or_not_editable']) + end + end + + context 'user with profile' do + let!(:profile) { create(:profile, user: test_user)} + + it 'returns submitted profile' do + expect { put url, params: request_params.merge(confirm: true), headers: auth_header } + .not_to change { Profile.count } + + expect(response.status).to eq(200) + expect(json_body[:state]).to eq('submitted') + expect(profile[:metadata]).to be_blank + end + end + + context 'user with partial profile' do + let!(:profile) { create(:profile, user: test_user, last_name: nil, first_name: nil) } + + it 'returns partial profile' do + expect { put url, params: request_params.except(:first_name), headers: auth_header } + .not_to change { Profile.count } + + expect(response.status).to eq(200) + expect(json_body[:state]).to eq('drafted') + expect(json_body[:metadata]).to be_blank + end + + it 'returns completed profile' do + expect { put url, params: request_params, headers: auth_header } + .not_to change { Profile.count } + + expect(response.status).to eq(200) + expect(json_body[:state]).to eq('drafted') + expect(json_body[:metadata]).to be_blank + end + end + end +end diff --git a/spec/api/v2/resource/service_accounts_spec.rb b/spec/api/v2/resource/service_accounts_spec.rb new file mode 100644 index 0000000..da80673 --- /dev/null +++ b/spec/api/v2/resource/service_accounts_spec.rb @@ -0,0 +1,297 @@ +# frozen_string_literal: true + +describe 'Api::V2::Resource::ServiceAccounts' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:create_service_account_permission) do + create :permission, + role: 'service_account' + end + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:test_user) { create(:user, otp: otp_enabled, role: 'member') } + let!(:service_account) { create(:service_account, user: test_user) } + let(:otp_enabled) { true } + let!(:first_api_key) { create :api_key, key_holder_account: service_account } + let!(:second_api_key) { create :api_key, key_holder_account: service_account } + let(:valid_otp_code) { '1357' } + let(:invalid_otp_code) { '1234' } + let(:otp_code) { valid_otp_code } + + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, valid_otp_code) { true } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, invalid_otp_code) { false } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, nil) { false } + end + + describe 'GET /api/v2/resource/service_accounts' do + let(:do_request) do + get '/api/v2/resource/service_accounts', + headers: auth_header + end + + let(:expected_fields) do + { + uid: service_account.uid, + email: service_account.email, + owner_id: test_user.id, + role: service_account.role, + level: service_account.level, + state: service_account.state, + } + end + + it 'Return service accounts for current user' do + do_request + + expect(json_body.count).to eq 1 + expect(response.status).to eq(200) + expect(json_body.first).to include(expected_fields) + end + end + + describe 'GET /api/v2/resource/service_accounts/api_keys/' do + let(:get_params) do + { + service_account_uid: service_account.uid, + ordering: 'asc', + order_by: 'id' + } + end + let(:post_params) do + { + service_account_uid: service_account.uid, + scope: 'trade', + algorithm: 'HS256' + } + end + let(:do_request) do + post '/api/v2/resource/service_accounts/api_keys', + params: post_params.merge(totp_code: otp_code), + headers: auth_header + end + let(:do_get_request) { get "/api/v2/resource/service_accounts/api_keys", params: get_params, headers: auth_header } + let(:expected_fields) do + { + kid: first_api_key.kid, + state: first_api_key.state, + scope: %w[trade] + } + end + let(:totp_code) { valid_otp_code } + + it 'Return api key for current account in ASC order' do + do_get_request + + expect(json_body.count).to eq 2 + expect(response.status).to eq(200) + expect(json_body.first).to include(expected_fields) + end + + it 'Return api key for current account in DESC order' do + get_params.merge!(ordering: 'desc') + do_get_request + + expect(json_body.count).to eq 2 + expect(response.status).to eq(200) + expect(json_body.second).to include(expected_fields) + end + + context 'when invalid ordering' do + it 'renders an error' do + get_params.merge!(ordering: 'resc') + do_get_request + + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.service_accounts.invalid_ordering"]) + end + end + + context 'when invalid order_by' do + it 'renders an error' do + get_params.merge!(order_by: 'invalid') + do_get_request + + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.service_accounts.invalid_attribute"]) + end + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.service_accounts.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.service_accounts.invalid_totp"]) + end + end + end + + describe 'POST /api/v2/resource/api_keys' do + let(:do_request) do + post '/api/v2/resource/api_keys', + params: params.merge(totp_code: otp_code), + headers: auth_header + end + let(:otp_code) { valid_otp_code } + + context 'when fields are valid' do + let(:params) do + { + scope: 'trade', + algorithm: 'HS256' + } + end + let(:expected_fields) do + { + state: 'active', + scope: params[:scope].split(','), + algorithm: 'HS256' + } + end + + it 'Create an api key' do + expect { do_request }.to change { APIKey.count }.by(1) + expect(response.status).to eq(201) + expect_body.to include(expected_fields) + end + + it 'does not create api key if vault is down' do + allow(Vault::Rails).to receive(:encrypt).and_raise(Vault::VaultError) + expect { do_request }.not_to change { APIKey.count } + expect(response.status).to eq(422) + expect_body.to eq(errors: ["api_key.could_not_save_secret"]) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.api_key.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.api_key.invalid_totp"]) + end + end + + context 'when algorithm is invalid' do + it 'renders an error' do + params[:algorithm] = 'kek' + do_request + expect_body.to eq(errors: ["kid.blank", "algorithm.inclusion"]) + end + end + end + end + + describe 'PUT /api/v2/resource/service_accounts/api_keys/:kid' do + let(:do_request) do + put "/api/v2/resource/service_accounts/api_keys/#{first_api_key.kid}", params: params.merge(totp_code: otp_code), + headers: auth_header + end + let(:otp_code) { valid_otp_code } + context 'when valid fields' do + let(:params) do + { + service_account_uid: service_account.uid, + state: 'inactive', + scope: 'sell', + algorithm: 'HS256' + } + end + + it 'Updates a state' do + expect { do_request }.to change { first_api_key.reload.state } + .from('active').to('inactive') + expect(response.status).to eq(200) + end + + it 'Updates a scope' do + expect { do_request }.to change { first_api_key.reload.scope } + .from(['trade']).to(['sell']) + expect(response.status).to eq(200) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.service_accounts.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.service_accounts.invalid_totp"]) + end + end + end + end + + describe 'DELETE /api/v2/resource/service_accounts/api_keys/:uid' do + let(:do_request) do + delete "/api/v2/resource/service_accounts/api_keys/#{first_api_key.kid}?totp_code=#{otp_code}&service_account_uid=#{service_account.uid}", + headers: auth_header + end + let(:otp_code) { valid_otp_code } + + it 'Removes an api key' do + do_request + expect(response.status).to eq(204) + end + + context 'when otp is not enabled' do + let(:otp_enabled) { false } + + it 'renders an error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.service_accounts.2fa_disabled"]) + end + end + + context 'when code is invalid' do + let(:otp_code) { invalid_otp_code } + + it 'renders an error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.service_accounts.invalid_totp"]) + end + end + end +end diff --git a/spec/api/v2/resource/users_spec.rb b/spec/api/v2/resource/users_spec.rb new file mode 100644 index 0000000..608e3ec --- /dev/null +++ b/spec/api/v2/resource/users_spec.rb @@ -0,0 +1,270 @@ +# frozen_string_literal: true + +describe 'Api::V1::Profiles' do + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + describe 'GET /api/v2/resource/users/me' do + it 'should reply permissions denied' do + get '/api/v2/resource/users/me' + expect_body.to eq(errors: ["jwt.decode_and_verify"]) + expect(response.status).to eq(401) + end + + it 'should allow traffic with Authorization' do + get '/api/v2/resource/users/me', headers: auth_header + expect(json_body[:email]).to eq(test_user.email) + expect(response.status).to eq(200) + end + end + + let(:valid_otp_code) { '1357' } + let(:invalid_otp_code) { '1234' } + + before do + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, valid_otp_code) { true } + allow(TOTPService).to receive(:validate?) + .with(test_user.uid, invalid_otp_code) { false } + end + + describe 'PUT /api/v2/resource/users/me' do + let(:correct_data) { { bar: 'baz' }.to_json } + let(:incorrect_data) { 'random string' } + + it 'changes user data field' do + put '/api/v2/resource/users/me', headers: auth_header, params: { + data: correct_data + } + expect(test_user.reload.data).to eq(correct_data) + expect(response.status).to eq 200 + end + + it 'receives invalid data error' do + put '/api/v2/resource/users/me', headers: auth_header, params: { + data: incorrect_data + } + + expect(response.status).to eq 422 + expect_body.to eq errors: ['data.invalid_format'] + end + end + + describe 'DELETE /api/v2/resource/users/me' do + it 'receives invalid password error' do + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'WrongPassword' + } + + expect(response.status).to eq 422 + expect(test_user.reload.state).to eq 'active' + expect_body.to eq errors: ['resource.user.invalid_password'] + end + + it 'receives otp missing error if 2fa is enabled and param is missing' do + test_user.update(otp: true) + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'Tecohvi0' + } + + expect(response.status).to eq 422 + expect(test_user.reload.state).to eq 'active' + expect_body.to eq errors: ['resource.user.missing_otp_code'] + end + + it 'receives otp empty error if 2fa is enabled and param is empty' do + test_user.update(otp: true) + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'Tecohvi0', + otp_code: '' + } + + expect(response.status).to eq 422 + expect(test_user.reload.state).to eq 'active' + expect_body.to eq errors: ['resource.user.empty_otp_code'] + end + + it 'receives invalid otp error with wrong otp code' do + test_user.update(otp: true) + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'Tecohvi0', + otp_code: invalid_otp_code + } + + expect(response.status).to eq 422 + expect(test_user.reload.state).to eq 'active' + expect_body.to eq errors: ['resource.user.invalid_otp'] + end + + it 'marks user as deleted with turned on 2fa and valid otp' do + test_user.update(otp: true) + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'Tecohvi0', + otp_code: valid_otp_code + } + + expect(response.status).to eq 200 + expect(test_user.reload.state).to eq 'deleted' + end + + it 'marks user as deleted with turned off 2fa' do + delete '/api/v2/resource/users/me', headers: auth_header, params: { + password: 'Tecohvi0' + } + + expect(response.status).to eq 200 + expect(test_user.reload.state).to eq 'deleted' + end + end + + describe 'POST /api/v2/resource/otp/disable' do + it 'with dasbled 2fa' do + post '/api/v2/resource/otp/disable', headers: auth_header, params: { code: valid_otp_code } + expect_body.to eq errors: ['resource.otp.not_enabled'] + end + + it 'with invalid code' do + test_user.update(otp: true) + post '/api/v2/resource/otp/disable', headers: auth_header, params: { code: invalid_otp_code } + expect_body.to eq errors: ['resource.otp.invalid'] + end + + it 'disables 2fa' do + test_user.update(otp: true) + expect { + post '/api/v2/resource/otp/disable', headers: auth_header, params: { code: valid_otp_code } + }.to change { test_user.reload.otp }.from(true).to(false) + + expect(response.status).to eq 200 + end + end + + describe 'POST /api/v2/resource/users/activity' do + it 'allows only [password, otp, session, all] as a topic parameter' do + get '/api/v2/resource/users/activity/invalid', headers: auth_header + expect(response.status).to eq(422) + expect_body.to eq(errors: ['resource.user.wrong_topic']) + get '/api/v2/resource/users/activity/session', headers: auth_header + expect(response.status).to eq(422) + expect_body.to eq(errors: ['resource.user.no_activity']) + end + it 'sorts user activities' do + 4.times { create(:activity, user: test_user) } + get '/api/v2/resource/users/activity/all', headers: auth_header + activities = JSON.parse(response.body) + expect(activities.first['id']).to be >= activities.last['id'] + end + it 'sorts user activities by result' do + 4.times { create(:activity, user: test_user, result: 'succeed') } + get '/api/v2/resource/users/activity/all', params: { result: 'succeed' }, headers: auth_header + activities = JSON.parse(response.body) + expect(activities.length).to eq(4) + end + it 'sorts user activities by time range' do + 4.times { create(:activity, user: test_user, result: 'succeed') } + get '/api/v2/resource/users/activity/all', params: { time_from: 5.hours.ago.to_i, time_to: 1.hours.ago.to_i }, headers: auth_header + activities = JSON.parse(response.body) + expect(activities.length).to eq(1) + end + + it 'sorts user activities by time range' do + 4.times { create(:activity, user: test_user, result: 'succeed') } + 4.times { create(:activity, user: test_user, result: 'succeed', created_at: 10.hours.ago) } + + get '/api/v2/resource/users/activity/all', params: { time_from: 5.hours.ago.to_i}, headers: auth_header + activities = JSON.parse(response.body) + expect(activities.length).to eq(4) + end + end + + describe 'POST /api/v2/resource/users/password' do + let(:do_request) do + put '/api/v2/resource/users/password', params: params, headers: auth_header + end + let(:do_confirm_request) do + post '/api/v2/resource/users/confirm_password', params: { code: otp_code }, headers: auth_header + end + let(:params) do + { + old_password: old_password, + new_password: new_password, + confirm_password: confirm_password + } + end + let(:old_password) { '' } + let(:new_password) { '' } + let(:confirm_password) { '' } + context 'when params are blank' do + it 'renders 400 error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.user.empty_old_password", "resource.user.empty_new_password", "resource.user.empty_confirm_password"]) + end + end + context 'when old password is not right' do + let(:old_password) { 'invalid' } + let(:new_password) { 'Gol4aid2' } + let(:confirm_password) { 'Gol4aid2' } + it 'renders 400 error' do + do_request + expect(response.status).to eq(400) + expect_body.to eq(errors: ["resource.password.prev_pass_not_correct"]) + end + end + context 'when new pass and confirmation are different' do + let(:old_password) { 'Tecohvi0' } # 'Tecohvi0' - test_user password + let(:new_password) { 'Gol4aid1' } + let(:confirm_password) { 'Gol4aid2' } + it 'renders 422 error' do + do_request + expect(response.status).to eq(422) + expect_body.to eq(errors: ["resource.password.doesnt_match"]) + end + end + context 'when params are valid, passwords are weak' do + let(:old_password) { 'Tecohvi0' } # 'Tecohvi0' - test_user password + let(:new_password) { 'Simple' } + let(:confirm_password) { 'Simple' } + it 'returns weak password error' do + do_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["temp_password.requirements"]) + end + end + + context 'when params are valid, confirm code not valid' do + let(:old_password) { 'Tecohvi0' } # 'Tecohvi0' - test_user password + let(:new_password) { 'Gol4aid1' } + let(:confirm_password) { 'Gol4aid1' } + let(:otp_code) { invalid_otp_code } + let(:log_in) { post '/api/v2/identity/sessions', params: { email: test_user.email, password: new_password } } + it 'changes a password' do + do_request + do_confirm_request + expect_status_to_eq 422 + expect_body.to eq(errors: ["resource.password.confirm_code_not_match"]) + end + end + + context 'when params are valid' do + let(:old_password) { 'Tecohvi0' } # 'Tecohvi0' - test_user password + let(:new_password) { 'Gol4aid1' } + let(:confirm_password) { 'Gol4aid1' } + let(:otp_code) { valid_otp_code } + let(:log_in) { post '/api/v2/identity/sessions', params: { email: test_user.email, password: new_password } } + it 'changes a password' do + do_request + expect(response.status).to eq(201) + + do_confirm_request + expect(response.status).to eq(201) + + log_in + expect(response.status).to eq(200) + end + end + end +end + diff --git a/spec/factories/activity.rb b/spec/factories/activity.rb new file mode 100644 index 0000000..bb9f85f --- /dev/null +++ b/spec/factories/activity.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :activity do + user { FactoryBot.create(:user) } + user_ip { Faker::Internet.ip_v4_address } + user_agent { Faker::Internet.user_agent } + category { %w[admin user].sample } + topic { %w[session password otp account].sample } + action { %w[otp::enable login logout signup].sample } + result { %w[succeed failed].sample } + data { {data: Faker::Lorem.sentence(word_count: 3, supplemental: true, random_words_to_add: 4)}.to_json } + end +end diff --git a/spec/factories/api_keys.rb b/spec/factories/api_keys.rb new file mode 100644 index 0000000..bb462ff --- /dev/null +++ b/spec/factories/api_keys.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :api_key, class: 'APIKey' do + kid { Faker::Crypto.sha256 } + secret { SecureRandom.hex(16) } + scope { %w[trade] } + algorithm { 'HS256' } + + trait :with_service_account do + key_holder_account { create(:service_account) } + end + + trait :with_user do + key_holder_account { create(:user) } + end + end +end diff --git a/spec/factories/document.rb b/spec/factories/document.rb new file mode 100644 index 0000000..df57d6b --- /dev/null +++ b/spec/factories/document.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :document do + user + + doc_type { 'Passport' } + doc_number { Faker::Code.asin } + doc_expire { Faker::Business.credit_card_expiry_date } + + after(:build) do |doc| + doc.upload.download!(Faker::Company.logo) + end + end +end diff --git a/spec/factories/label.rb b/spec/factories/label.rb new file mode 100644 index 0000000..556d2a5 --- /dev/null +++ b/spec/factories/label.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :label do + key { ::Faker::Internet.slug(glue: '-') } + value { ::Faker::Internet.slug(glue: '-') } + scope { 'public' } + user { create(:user) } + end +end diff --git a/spec/factories/level.rb b/spec/factories/level.rb new file mode 100644 index 0000000..a426c42 --- /dev/null +++ b/spec/factories/level.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :level do + key { Faker::Hacker.noun } + value { Faker::Hacker.adjective } + description { Faker::Hacker.say_something_smart } + end +end diff --git a/spec/factories/permission.rb b/spec/factories/permission.rb new file mode 100644 index 0000000..2a86665 --- /dev/null +++ b/spec/factories/permission.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :permission do + role { %w[member trader broker admin accountant support technical compliance superadmin] } + path { 'api/v2' } + action { 'ACCEPT' } + verb { %w[get post put delete head patch all] } + end +end diff --git a/spec/factories/phones.rb b/spec/factories/phones.rb new file mode 100644 index 0000000..0c7e840 --- /dev/null +++ b/spec/factories/phones.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :phone do + number { '12345678911' } + user + end +end diff --git a/spec/factories/profile.rb b/spec/factories/profile.rb new file mode 100644 index 0000000..2058399 --- /dev/null +++ b/spec/factories/profile.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :profile do + user { FactoryBot.create(:user) } + author { } + first_name { Faker::Name.first_name } + last_name { Faker::Name.last_name } + dob { Faker::Date.birthday } + address { Faker::Address.state } + city { Faker::Address.city } + country { Faker::Address.country_code_long } + postcode { Faker::Address.postcode } + end +end diff --git a/spec/factories/restriction.rb b/spec/factories/restriction.rb new file mode 100644 index 0000000..ae4359a --- /dev/null +++ b/spec/factories/restriction.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :restriction do + scope { 'ip_subnet' } + value { '0.0.0.0/0' } + end +end diff --git a/spec/factories/service_account.rb b/spec/factories/service_account.rb new file mode 100644 index 0000000..2603781 --- /dev/null +++ b/spec/factories/service_account.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :service_account do + user { FactoryBot.create(:user) } + email { Faker::Internet.email } + uid { UIDGenerator.generate("SI") } + + trait :without_user do + user { nil } + end + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 0000000..8974148 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + email { Faker::Internet.email } + password { 'Tecohvi0' } + password_confirmation { 'Tecohvi0' } + state { 'active' } + + trait :with_profile do + after(:create) do |user, _| + create(:profile, user: user) + end + end + + trait :with_document do + after(:create) do |user, _| + create(:document, user: user) + end + end + + trait :with_phone do + after(:create) do |user, _| + create(:phone, user: user) + end + end + + trait :with_document_phone_profile do + after(:create) do |user, _| + create(:phone, user: user) + create(:profile, user: user) + create(:document, user: user) + end + end + end +end diff --git a/spec/fixtures/files/documents_test.jpg b/spec/fixtures/files/documents_test.jpg new file mode 100644 index 0000000..9d448d1 Binary files /dev/null and b/spec/fixtures/files/documents_test.jpg differ diff --git a/spec/initializers/data_storage_titles_spec.rb b/spec/initializers/data_storage_titles_spec.rb new file mode 100644 index 0000000..922fb8d --- /dev/null +++ b/spec/initializers/data_storage_titles_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Data Storage Titles configuraton class' do + context 'testing list function' do + let(:expected_values) { ['personal', 'company'] } + + it 'reads configuration from the file' do + expect(UserStorageTitles.list).to eq(expected_values) + end + + it 'cache preconfig and doesn\'t read from the file on every call' do + expect(UserStorageTitles.list).to eq(expected_values) + + @template = {} + + expect(UserStorageTitles.list).to eq(expected_values) + end + end +end diff --git a/spec/initializers/document_types.spec.rb b/spec/initializers/document_types.spec.rb new file mode 100644 index 0000000..24ee7e6 --- /dev/null +++ b/spec/initializers/document_types.spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe 'Document Types configuraton class' do + context 'testing list function' do + let(:expected_values) { ['Passport', 'Identity card', 'Driver license', 'Utility Bill', 'Residental', 'Institutional'] } + + before do + @template = { 'document_types' => ['Passport', 'Identity card', 'Driver license', 'Utility Bill', 'Residental', 'Institutional'] } + allow(YAML).to receive(:load_file).and_return(@template) + end + + it 'reads configuration from the file' do + expect(DocumentTypes.list).to eq(expected_values) + end + + it 'cache preconfig and doesn\'t read from the file on every call' do + expect(DocumentTypes.list).to eq(expected_values) + + @template = {} + + expect(DocumentTypes.list).to eq(expected_values) + end + end +end diff --git a/spec/lib/barong/jwt_spec.rb b/spec/lib/barong/jwt_spec.rb new file mode 100644 index 0000000..ba9b4e0 --- /dev/null +++ b/spec/lib/barong/jwt_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require_dependency 'barong/jwt' + +describe Barong::JWT do + let(:key) { OpenSSL::PKey::RSA.generate(2048) } + + it 'should encode payload with claims' do + codec = Barong::JWT.new(key: key) + token = codec.encode({hello: 'world'}) + + decoded = ::JWT.decode(token, key.public_key, + true, { algorithm: 'RS256' }) + expect(decoded.first.fetch('hello')).to eq('world') + end +end diff --git a/spec/lib/barong/seed_spec.rb b/spec/lib/barong/seed_spec.rb new file mode 100644 index 0000000..f37e1d1 --- /dev/null +++ b/spec/lib/barong/seed_spec.rb @@ -0,0 +1,404 @@ +# frozen_string_literal: true + +require_dependency 'barong/seed' + +describe "Default seeds.yml template" do + let(:seeds) { Barong::Seed.new.seeds } + + it "Generate seed using environement variables" do + expect(seeds["levels"].size).to eq(3) + # TODO fix 2 below expect + # expect(ENV).to receive(:fetch).with("BARONG_ADMIN_EMAIL", "admin@barong.io").and_call_original + # expect(ENV).to receive(:fetch).with("BARONG_ADMIN_PASSWORD", nil).and_return("123AZErty@") + + end +end + +describe Barong::Seed do + let(:create_permissions) do + create :permission, + role: 'member' + create :permission, + role: 'admin' + end + let(:levels) { + [ + { + "key" => "email", + "value" => "verified", + "description" => "User clicked on the confirmation link" + }, + { + "key" => "phone", + "value" => "verified", + "description" => "User entered a valid code from sms" + } + ] + } + let(:permissions) {[]} + let(:restrictions) {[]} + let(:users) {[]} + let(:seeds) { + { + "users" => users, + "levels" => levels, + "permissions" => permissions, + "restrictions" => restrictions + } + } + + let(:logger) { Logger.new('/dev/null') } + let(:seeder) { Barong::Seed.new } + + before(:each) do + allow(seeder).to receive(:seeds).and_return(seeds) + allow(seeder).to receive(:logger).and_return(logger) + Level.delete_all + User.delete_all + Permission.delete_all + Restriction.delete_all + end + + context "Seed simple and valid levels" do + it "seeds levels in database" do + seeder.seed_levels + expect(Level.count).to be 2 + Level.all.each_with_index do |level, index| + expect(level.id).to eq (index+1) + expect(level.key).to eq levels[index]["key"] + expect(level.value).to eq levels[index]["value"] + expect(level.description).to eq levels[index]["description"] + end + end + + it "skips existing levels" do + seeder.seed_levels + seeder.seed_levels + expect(Level.count).to be 2 + end + end + + context "Seed one admin user" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "role" => "admin", + "state" => "active", + "level" => 2 + } + ] + } + + it "seeds users in database" do + create_permissions + seeder.seed_levels + seeder.seed_users + expect(User.count).to eq 1 + user = User.first + expect(user.email).to eq("admin@peatio.tech") + expect(user.role).to eq("admin") + expect(user.state).to eq("active") + expect(user.level).to eq(2) + end + end + + context "User level doesn't match levels in databases" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "role" => "admin", + "state" => "active", + "level" => 3 + } + ] + } + + it "raises an explicit error" do + create_permissions + seeder.seed_levels + expect { + seeder.seed_users + }.to raise_error(Barong::Seed::ConfigError, "No enough levels found in database to grant the user to level 3") + end + end + + context "User level is not set" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "role" => "admin", + "state" => "active", + } + ] + } + + it "raises an explicit error" do + seeder.seed_levels + expect { + seeder.seed_users + }.to raise_error(Barong::Seed::ConfigError, "Level is missing for user admin@peatio.tech") + end + end + + context "User email is not set" do + let(:users) { + [ + { + "password" => "123aZE@654", + "role" => "admin", + "state" => "active", + "level" => 2 + } + ] + } + + it "raises an explicit error" do + seeder.seed_levels + expect { + seeder.seed_users + }.to raise_error(Barong::Seed::ConfigError, "Email missing in users seed") + end + end + + context "User state is not set" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "role" => "admin", + "level" => 0 + } + ] + } + + it "defaults state to pending" do + create_permissions + seeder.seed_levels + seeder.seed_users + expect(User.count).to eq 1 + user = User.first + expect(user.email).to eq("admin@peatio.tech") + expect(user.role).to eq("admin") + expect(user.state).to eq("pending") + expect(user.level).to eq(0) + end + end + + context "User role is not set" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "state" => "active", + "level" => 2 + } + ] + } + + it "defaults role to member" do + create_permissions + seeder.seed_levels + seeder.seed_users + expect(User.count).to eq 1 + user = User.first + expect(user.email).to eq("admin@peatio.tech") + expect(user.role).to eq("member") + expect(user.state).to eq("active") + expect(user.level).to eq(2) + end + end + + context "Seed one admin and a regular user" do + let(:users) { + [ + { + "email" => "admin@peatio.tech", + "password" => "123aZE@654", + "role" => "admin", + "state" => "active", + "level" => 2 + }, + { + "email" => "user@example.com", + "password" => "123aZE@654", + "role" => "member", + "state" => "active", + "level" => 2 + } + ] + } + + it "seeds users in database" do + create_permissions + seeder.seed_levels + seeder.seed_users + + expect(User.count).to eq 2 + + admin = User.find_by(email: "admin@peatio.tech") + expect(admin.role).to eq("admin") + expect(admin.state).to eq("active") + expect(admin.level).to eq(2) + + admin = User.find_by(email: "user@example.com") + expect(admin.role).to eq("member") + expect(admin.state).to eq("active") + expect(admin.level).to eq(2) + end + end + + context "Seed simple GET /me member permissions" do + let(:permissions) { + [ + { + "role" => "member", + "verb" => "GET", + "action" => "ACCEPT", + "path" => "api/v2/resource/users/me" + } + ] + } + + it "seeds permissions in database" do + seeder.seed_permissions + expect(Permission.count).to eq 1 + permission = Permission.first + expect(permission.role).to eq("member") + expect(permission.verb).to eq("GET") + expect(permission.path).to eq("api/v2/resource/users/me") + end + end + + context "Permission verb is not set" do + let(:permissions) { + [ + { + "role" => "member", + "action" => "ACCEPT", + "path" => "api/v2/resource/users/me" + } + ] + } + + it "raises an explicit error" do + expect { + seeder.seed_permissions + }.to raise_error(Barong::Seed::ConfigError, "Can't create permission: Verb can't be blank") + end + end + + context "Permission path is not set" do + let(:permissions) { + [ + { + "role" => "member", + "action" => "ACCEPT", + "verb" => "GET" + } + ] + } + it "raises an explicit error" do + expect { + seeder.seed_permissions + }.to raise_error(Barong::Seed::ConfigError, "Can't create permission: Path can't be blank") + end + end + + context "Seed one admin GET and one accountant POST permissions" do + let(:permissions) { + [ + { + "role" => "admin", + "verb" => "GET", + "action" => "ACCEPT", + "path" => "api/v2/admin/users/list" + }, + { + "role" => "accountant", + "verb" => "POST", + "action" => "ACCEPT", + "path" => "api/v2/admin/users" + } + ] + } + + it "seeds permissions in database" do + seeder.seed_permissions + expect(Permission.count).to eq 2 + + admin_permission = Permission.find_by_role('admin') + expect(admin_permission.role).to eq("admin") + expect(admin_permission.verb).to eq("GET") + expect(admin_permission.path).to eq("api/v2/admin/users/list") + + accountant_permission = Permission.find_by_role('accountant') + expect(accountant_permission.role).to eq("accountant") + expect(accountant_permission.verb).to eq("POST") + expect(accountant_permission.path).to eq("api/v2/admin/users") + end + end + + context "Seed simple and valid restrictions" do + let(:restrictions) { + [ + { + "category" => "maintenance", + "scope" => "all", + "value" => "all", + "state" => "enabled" + }, + { + "category" => "blacklist", + "scope" => "all", + "value" => "all", + "state" => "enabled" + } + ] + } + + it "seeds restrictions in database" do + seeder.seed_restrictions + expect(Restriction.count).to be 2 + Restriction.all.each_with_index do |restriction, index| + expect(restriction.category).to eq restrictions[index]["category"] + expect(restriction.scope).to eq restrictions[index]["value"] + expect(restriction.value).to eq restrictions[index]["value"] + expect(restriction.state).to eq restrictions[index]["state"] + end + end + end + + context "Empty seed" do + it "returns without error and with kind log info" do + expect { + seeder.seed_restrictions + }.not_to raise_error + end + end + + context "Missing params in restriction seed" do + let(:restrictions) { + [ + { + "category" => "maintenance", + "scope" => "all", + "value" => "all", + } + ] + } + + it "raises error on invalid seed" do + expect { + seeder.seed_restrictions + }.to raise_error(Barong::Seed::ConfigError, "state is missing in restrictions seed") + end + end +end diff --git a/spec/mailers/postmaster_spec.rb b/spec/mailers/postmaster_spec.rb new file mode 100644 index 0000000..1c15a52 --- /dev/null +++ b/spec/mailers/postmaster_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Postmaster, type: :mailer do + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + describe '#process_payload' do + let!(:user) { create(:user, email: 'test1@gmail.com') } + let(:record) { OpenStruct.new(domain: 'barong.com', token: 'blah-blah' ) } + let(:payload) do + { + user: user, + changes: nil, + record: record, + subject: 'Test Email', + template_name: 'email_confirmation.en.html.erb', + logo: 'https://storage.googleapis.com/public_peatio/logo.png' + } + end + let(:mail) { Postmaster.process_payload(payload) } + + it 'renders the headers' do + expect(mail.subject).to eq('Test Email') + expect(mail.to).to eq(['test1@gmail.com']) + expect(mail.from).to eq(['noreply@barong.io']) + end + + it 'renders the body' do + expect(mail.body.encoded).to match('Use this unique link to confirm your email test1@gmail.com') + end + end +end diff --git a/spec/mailers/previews/mailer_preview.rb b/spec/mailers/previews/mailer_preview.rb new file mode 100644 index 0000000..522f157 --- /dev/null +++ b/spec/mailers/previews/mailer_preview.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Preview all emails at http://localhost:3000/rails/mailers/mailer +class MailerPreview < ActionMailer::Preview + +end diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb new file mode 100644 index 0000000..240c229 --- /dev/null +++ b/spec/models/ability_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +describe Ability do + + before do + allow(Ability).to receive(:admin_permissions).and_return({ + "superadmin"=>{"manage"=>["User", "Activity", "Ability", "APIKey", "Profile", "Permission", "Label", "Restriction", "Level"]}, + "admin"=>{"read"=>["Level", "APIKey", "Permission"], "manage"=>["User", "Activity", "Profile", "Label"]}, + "compliance"=>{"read"=>["Level", "User", "Activity"], "manage"=>["Label"], "update"=>["Profile"]}, + "support"=>{"read"=>["User", "Activity", "APIKey", "Profile", "Label", "Level"]} + }) + end + + let!(:create_permissions) do + create(:permission, role: 'superadmin', action: 'accept', verb: 'get') + create(:permission, role: 'admin', action: 'accept', verb: 'get') + create(:permission, role: 'compliance', action: 'accept', verb: 'get') + create(:permission, role: 'support', action: 'accept', verb: 'get') + end + + context 'abilities for superadmin' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'superadmin') } + subject(:ability) { AdminAbility.new(test_user) } + + it { is_expected.to be_able_to(:manage, User.new) } + it { is_expected.to be_able_to(:manage, Activity.new) } + it { is_expected.to be_able_to(:manage, Profile.new) } + it { is_expected.to be_able_to(:manage, Permission.new) } + it { is_expected.to be_able_to(:manage, Label.new) } + it { is_expected.to be_able_to(:manage, Restriction.new) } + it { is_expected.to be_able_to(:manage, Level.new) } + end + + context 'abilities for admin' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'admin') } + subject(:ability) { AdminAbility.new(test_user) } + + it { is_expected.to be_able_to(:read, Level.new) } + it { is_expected.to be_able_to(:read, APIKey.new) } + it { is_expected.to be_able_to(:read, Permission.new) } + it { is_expected.to be_able_to(:manage, User.new) } + it { is_expected.to be_able_to(:manage, Activity.new) } + it { is_expected.to be_able_to(:manage, Profile.new) } + it { is_expected.to be_able_to(:manage, Label.new) } + end + + context 'abilities for compliance' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'compliance') } + subject(:ability) { AdminAbility.new(test_user) } + + + it { is_expected.to be_able_to(:read, Level.new) } + it { is_expected.to be_able_to(:read, User.new) } + it { is_expected.to be_able_to(:read, Activity.new) } + it { is_expected.to be_able_to(:manage, Label.new) } + it { is_expected.to be_able_to(:update, Profile.new) } + end + + context 'abilities for support' do + let(:test_user) { create(:user, email: 'example@gmail.com', role: 'support') } + subject(:ability) { AdminAbility.new(test_user) } + + it { is_expected.to be_able_to(:read, User.new) } + it { is_expected.to be_able_to(:read, Activity.new) } + it { is_expected.to be_able_to(:read, APIKey.new) } + it { is_expected.to be_able_to(:read, Profile.new) } + it { is_expected.to be_able_to(:read, Label.new) } + it { is_expected.to be_able_to(:read, Level.new) } + end +end diff --git a/spec/models/activity_spec.rb b/spec/models/activity_spec.rb new file mode 100644 index 0000000..d5e7352 --- /dev/null +++ b/spec/models/activity_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +RSpec.describe Activity, type: :model do + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:activity) do + create :activity, topic: 'session', + action: 'login', + result: 'succeed', + user_agent: 'Magic Pony Browser' + end + + context 'General' do + it { should belong_to(:user) } + it { + expect { + activity.update_attribute(:topic, 'otp') + }.to raise_error(ActiveRecord::ActiveRecordError, 'Activity is marked as readonly') + } + end + + describe 'Validations' do + context 'Correct fields values' do + it { should allow_value('session').for(:topic) } + it { should allow_value('otp').for(:topic) } + it { should allow_value('password').for(:topic) } + it { should allow_value('succeed').for(:result) } + it { should allow_value('failed').for(:result) } + it { expect(activity.browser.ua).to eq 'Magic Pony Browser'} + end + + context 'Banned actions and values' do + it { should_not allow_value('passed').for(:result) } + it { should_not allow_value('').for(:user_ip) } + it { expect(activity.browser.known?).to eq false } + it { expect(JSON.parse(activity.data)['note']).to eq 'Detected suspicious browser' } + end + end + + describe 'Browser functionality' do + let(:valid_user_agent){'Android SDK 1.5r3: Mozilla/5.0 (Linux; U; Android 1.5; de-; sdk Build/CUPCAKE) + AppleWebkit/528.5+ (KHTML, like Gecko) Version/3.1.2 Mobile Safari/525.20.1'} + let!(:activity) do + create :activity, topic: 'session', + action: 'login', + result: 'succeed', + user_agent: valid_user_agent + end + + context 'Detects browser information' do + it { expect(activity.browser.name).to eq 'Safari' } + it { expect(activity.browser.platform.android?).to eq true } + it { expect(activity.browser.webkit?).to eq true } + it { expect(activity.browser.full_version).to eq '3.1.2' } + it { expect(activity.browser.version).to eq '3' } + end + end + +end diff --git a/spec/models/api_key_spec.rb b/spec/models/api_key_spec.rb new file mode 100644 index 0000000..4b55a72 --- /dev/null +++ b/spec/models/api_key_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +RSpec.describe APIKey, type: :model do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:create_service_account_permission) do + create :permission, + role: 'service_account' + end + + describe 'Validations' do + subject(:api_key) { build(:api_key, :with_user) } + + it { should validate_presence_of(:secret).with_message(/can't be blank/) } + it { should validate_uniqueness_of(:kid).with_message(/has already been taken/) } + + context 'key holder state' do + context 'valid response' do + context 'user with active state' do + let!(:user) { create(:user, state: 'active') } + + it 'creates api key' do + subject.key_holder_account = user + expect(subject.save).to eq true + expect(subject.errors.full_messages).to eq [] + end + end + + context 'service account with active state' do + let!(:user) { create(:user, state: 'active') } + let!(:service_account) { create(:service_account, owner_id: user.id) } + + it 'creates api key' do + subject.key_holder_account = user + expect(subject.save).to eq true + expect(subject.errors.full_messages).to eq [] + end + end + end + + context 'invalid response' do + subject { build(:api_key) } + + context 'user with non active state' do + let!(:user) { create(:user, state: 'pending') } + + it 'render error message' do + subject.key_holder_account = user + expect(subject.save).to eq false + expect(subject.errors.full_messages).to include(/non active state for key holder account/) + end + end + + context 'service account with non active state' do + let!(:user) { create(:user, state: 'pending') } + let!(:service_account) { create(:service_account, owner_id: user.id) } + + it 'render error message' do + subject.key_holder_account = service_account + expect(subject.save).to eq false + expect(subject.errors.full_messages).to include(/non active state for key holder account/) + end + end + end + end + + context 'api key state on update' do + context 'valid response' do + context 'user with active state' do + let!(:user) { create(:user, state: 'active') } + let!(:api_key) { create(:api_key, key_holder_account: user, state: 'disabled') } + + it 'updates api key state' do + api_key.state = 'active' + expect(api_key.save).to eq true + expect(api_key.errors.full_messages).to eq [] + end + end + + context 'service account with non active state' do + let!(:user) { create(:user, state: 'active') } + let!(:service_account) { create(:service_account, owner_id: user.id) } + let!(:api_key) { create(:api_key, key_holder_account: service_account, state: 'disabled') } + + it 'render error message' do + api_key.state = 'active' + expect(api_key.save).to eq true + expect(api_key.errors.full_messages).to eq [] + end + end + end + + context 'invalid response' do + context 'user with non active state' do + let!(:user) { create(:user, state: 'active') } + let!(:api_key) { create(:api_key, key_holder_account: user) } + + it 'render error message' do + user.update(state: 'banned') + api_key.state = 'active' + expect(api_key.save).to eq false + expect(api_key.errors.full_messages).to include(/cant activate api key with disabled key holder account/) + end + end + + context 'service account with non active state' do + let!(:user) { create(:user, state: 'active') } + let!(:service_account) { create(:service_account, owner_id: user.id) } + let!(:api_key) { create(:api_key, key_holder_account: service_account) } + + it 'render error message' do + service_account.update(state: 'disabled') + api_key.state = 'active' + expect(api_key.save).to eq false + expect(api_key.errors.full_messages).to include(/cant activate api key with disabled key holder account/) + end + end + end + end + end +end diff --git a/spec/models/document_spec.rb b/spec/models/document_spec.rb new file mode 100644 index 0000000..ebe5607 --- /dev/null +++ b/spec/models/document_spec.rb @@ -0,0 +1,108 @@ +# frozen_string_literal: true + +RSpec.describe Document, type: :model do + ## Test of relationships + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + it { should belong_to(:user) } + + describe 'validation' do + let!(:document) { build :document, doc_expire: doc_expire } + subject do + document.valid? + document.errors.messages + end + let(:doc_expire) { Date.current.to_s } + it { is_expected.to be_blank } + + context 'when doc_expire is expired' do + let(:doc_expire) { 1.day.ago.to_s } + + it { is_expected.to eq(doc_expire: ['is invalid']) } + end + end + + context 'Document creation' do + let!(:current_user) { create(:user) } + let(:create_document) { create :document, user: current_user, doc_type: 'Passport', doc_category: 'front_side' } + let(:create_document_second) { create :document, user: current_user, doc_type: 'Passport', doc_category: 'Selfie' } + let(:create_without_label) { create :document, user: current_user, update_labels: false } + let(:document_label) { current_user.labels.first } + + context 'when it is first document' do + it 'adds new document label' do + create_document + expect { create_document_second }.to change { current_user.reload.labels.count }.from(0).to(1) + end + + it 'new document label is document: pending' do + create_document + create_document_second + expect(document_label.key).to eq 'document' + expect(document_label.value).to eq 'pending' + end + end + + context 'when user has label document: rejected' do + let!(:document_label) do + create :label, + scope: 'private', + key: 'document', + value: 'rejected', + user: current_user + end + + it 'does not add new label' do + create_document + expect { create_document_second }.to_not change { Label.count } + end + + it 'changes label value to pending' do + create_document + create_document_second + expect(current_user.labels.first.value).to eq 'pending' + end + end + + context 'when user has label document: verified' do + let!(:document_label) do + create :label, + scope: 'private', + key: 'document', + value: 'verified', + user: current_user + end + + it 'does not add new label' do + expect { create_document }.to_not change { Label.count } + end + + it 'remains value verified' do + expect { create_document }.to_not change { document_label } + end + end + end + + context 'submasked fields' do + let!(:current_user) { create(:user) } + let(:document) { create :document, user: current_user, doc_type: 'Passport', doc_number: 'M0993353' } + let(:document_without_doc_number) { create :document, user: current_user, doc_type: 'Passport', doc_number: nil } + + context 'number' do + it { expect(document.sub_masked_doc_number).to eq 'M0****53' } + it { expect(document_without_doc_number.sub_masked_doc_number).to eq nil } + + it 'should mask first 2 letters and last 2 digits' do + document.update(doc_type: 'Driver license', doc_number: 'BO231283013DSAS23') + expect(document.sub_masked_doc_number).to eq 'BO*************23' + end + end + end +end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb new file mode 100644 index 0000000..17cd513 --- /dev/null +++ b/spec/models/label_spec.rb @@ -0,0 +1,252 @@ +# frozen_string_literal: true + +RSpec.describe Label, type: :model do + let!(:create_admin_permission) do + create :permission, + role: 'admin' + end + let!(:create_member_permission) do + create :permission, + role: 'member' + end + it { should belong_to(:user) } + + describe 'update user level if label defined as level', order: :defined do + let!(:email_verified_level) { Level.find(1) } + let!(:phone_verified_level) { Level.find(2) } + let!(:identity_verified_level) { Level.find(3) } + let!(:document_verified_level) { Level.find(4) } + + context 'when user has no labels' do + let!(:user) { create(:user) } + it { expect(user.reload.level).to eq 0 } + + it 'does not change level if valid label has public scope' do + expect do + create_label_with_level(user, email_verified_level, scope: 'public') + end.to_not change { user.reload.level } + end + + it 'when checks labels-levels mappings' do + expect do + create_label_with_level(user, email_verified_level) + end.to change { user.reload.level }.from(0).to(1) + + expect do + create_label_with_level(user, phone_verified_level) + end.to change { user.reload.level }.from(1).to(2) + + expect do + create_label_with_level(user, identity_verified_level) + end.to change { user.reload.level }.from(2).to(3) + + expect do + create_label_with_level(user, document_verified_level) + end.to change { user.reload.level }.from(3).to(4) + end + end + + context 'when user has verified email and phone' do + let!(:user) { create(:user) } + + before do + create_label_with_level(user, email_verified_level) + create_label_with_level(user, phone_verified_level) + end + + it 'changes to level 2 when identity verified label applied' do + expect(user.reload.level).to eq(2) + end + + it 'changes to level 3 when identity verified label applied' do + expect do + create_label_with_level(user, identity_verified_level) + end.to change { user.reload.level }.to(3) + end + + it 'downgrades level to 1 when phone state changes to rejected' do + Label.find_by(user: user, key: phone_verified_level.key).update(value: 'rejected') + user.update_level + expect(user.reload.level).to eq 1 + end + + it 'does not change level if user has document verified label' do + expect do + create_label_with_level(user, document_verified_level) + end.to_not change { user.reload.level } + end + end + + context 'when user has all label required for level 4' do + let!(:user) { create(:user) } + before do + create_label_with_level(user, email_verified_level) + create_label_with_level(user, phone_verified_level) + create_label_with_level(user, identity_verified_level) + create_label_with_level(user, document_verified_level) + end + + it { expect(user.reload.level).to eq 4 } + + it 'downgrades level to 0 when email verified label changes' do + Label.find_by(user: user, key: email_verified_level.key).update(value: 'rejected') + user.update_level + expect(user.reload.level).to eq 0 + end + + it 'downgrades level to 0 when email verified label changes' do + Label.find_by(user: user, key: email_verified_level.key).destroy + user.update_level + expect(user.reload.level).to eq 0 + end + end + + context 'document label changes' do + let!(:user) { create(:user) } + let(:last_mailer_delivery) { ActionMailer::Base.deliveries.last } + let!(:document_label) do + create( + :label, + user: user, + key: 'document', + value: 'pending', + scope: 'private' + ) + end + end + + context 'when label scope is changed from private to public and reverse' do + let!(:user) { create(:user) } + + before do + create_label_with_level(user, email_verified_level) + create_label_with_level(user, phone_verified_level) + create_label_with_level(user, identity_verified_level) + create_label_with_level(user, document_verified_level) + end + + it 'updates level' do + expect do + user.labels.last.update(scope: :public) + end.to change { user.reload.level }.from(4).to(3) + + expect do + user.labels.last.update(scope: :private) + end.to change { user.reload.level }.from(3).to(4) + end + end + + context '2 labels with same keys' do + let!(:user) { create(:user) } + let!(:label_public) do + create :label, + user: user, + key: email_verified_level.key, + value: email_verified_level.value, + scope: 'public' + end + + let!(:label_private) do + create :label, + user: user, + key: email_verified_level.key, + value: email_verified_level.value, + scope: 'private' + end + + context 'can be created with different scopes' do + it 'user has both labels' do + expect(user.labels).to include(label_private, label_public) + end + + it 'user has level 1' do + expect(user.level).to eq 1 + end + + context 'when private label changes' do + it 'user level downgrades when value changed' do + expect { label_private.update(value: 'rejected') }.to change { user.reload.level }.to 0 + end + + it 'user level downgrades when key changed' do + expect { label_private.update(key: 'email0') }.to change { user.reload.level }.to 0 + end + end + + context 'when public label changes' do + it 'user level does not change' do + expect { label_public.update(value: 'rejected') }.to_not change { user.reload.level } + end + end + end + end + + context 'when user has all required labels for level 4 in wrong scope' do + let!(:user) { create(:user) } + let!(:label_email_public) do + create :label, + user: user, + key: email_verified_level.key, + value: email_verified_level.value, + scope: 'public' + end + let!(:label_phone_public) do + create :label, + user: user, + key: phone_verified_level.key, + value: phone_verified_level.value, + scope: 'public' + end + let!(:label_identity_public) do + create :label, + user: user, + key: identity_verified_level.key, + value: identity_verified_level.value, + scope: 'public' + end + let!(:label_document_public) do + create :label, + user: user, + key: document_verified_level.key, + value: document_verified_level.value, + scope: 'public' + end + + it { expect(user.reload.level).to eq 0 } + end + end + + context 'downcase fields' do + let(:user) { create(:user) } + let(:label) { build(:label, user: user, key: 'PhoNe', value: 'VeRifiEd') } + + it 'downcases key and value before save' do + label.save + expect(label.reload.key).to eq 'phone' + expect(label.reload.value).to eq 'verified' + end + end + + context 'event api behaviour' do + let!(:user) { create(:user, state: 'pending') } + let(:label) { create(:label, user_id: user.id) } + + before do + allow(EventAPI).to receive(:notify) + end + + it 'receives event with label create' do + label + + expect(EventAPI).to have_received(:notify).with('model.label.created', + hash_including( + record: hash_including( + id: label.id, + key: label.key, + value: label.value, + user: hash_including(uid: user.uid, level: 0, email: user.email) + ) + )) + end + end +end diff --git a/spec/models/level_spec.rb b/spec/models/level_spec.rb new file mode 100644 index 0000000..726288e --- /dev/null +++ b/spec/models/level_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +RSpec.describe Level, type: :model do + it { should validate_presence_of(:key) } + it { should validate_presence_of(:value) } + it { should validate_presence_of(:description) } +end diff --git a/spec/models/phone_spec.rb b/spec/models/phone_spec.rb new file mode 100644 index 0000000..029c61f --- /dev/null +++ b/spec/models/phone_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.describe Phone, type: :model do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + context 'submasked fields' do + let!(:phone) { create(:phone) } + + context 'number' do + it 'should mask country code and last 4 digits' do + phone.update(number: '79225551234') + expect(phone.sub_masked_number).to eq '7******1234' + end + + it 'should mask country code and last 4 digits' do + phone.update(number: '+201112341923') + expect(phone.sub_masked_number).to eq '20******1923' + end + + it 'should mask country code and last 4 digits' do + phone.update(number: '+380971232322') + expect(phone.sub_masked_number).to eq '380*****2322' + end + + it 'should return empty phone number' do + phone.update(number: '') + expect(phone.sub_masked_number).to eq '' + end + + context 'default country code' do + it 'should mask country code' do + phone.update_attribute(:number, '1112222') + expect(phone.sub_masked_number).to eq '11*2222' + end + end + end + end +end diff --git a/spec/models/profile_spec.rb b/spec/models/profile_spec.rb new file mode 100644 index 0000000..4382b18 --- /dev/null +++ b/spec/models/profile_spec.rb @@ -0,0 +1,385 @@ +# frozen_string_literal: true + +RSpec.describe Profile, type: :model do + describe 'squish_spaces' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:profile) do + create :profile, first_name: ' First Name ', + last_name: ' Last Name ', + city: ' New York ', + postcode: ' AB 135-144 ' + end + + it 'squishes spaces' do + profile.reload + expect(profile.first_name).to eq 'First Name' + expect(profile.last_name).to eq 'Last Name' + expect(profile.city).to eq 'New York' + expect(profile.postcode).to eq 'AB 135-144' + end + end + + describe 'creating partial priofile' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let!(:user) { create(:user) } + + subject { Profile.create(params.merge(user: user)) } + + context 'empty params' do + let!(:params) { {} } + + it { expect(subject).to be_valid } + it { expect(subject.first_name.nil?).to be_truthy } + it { expect(subject.last_name.nil?).to be_truthy } + it { expect(subject.dob.nil?).to be_truthy } + it { expect(subject.address.nil?).to be_truthy } + it { expect(subject.postcode.nil?).to be_truthy } + it { expect(subject.city.nil?).to be_truthy } + it { expect(subject.country.nil?).to be_truthy } + it { expect(subject.metadata.nil?).to be_truthy } + it { expect(subject.state).to eq('drafted') } + it { expect(subject.user.labels.find_by(key: 'profile').value).to eq('drafted') } + + context 'add empty params' do + + let!(:params) { + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + } + + before do + subject.update(params) + end + + it { expect(subject).to be_valid } + it { expect(subject.first_name.present?).to be_truthy } + it { expect(subject.last_name.present?).to be_truthy } + it { expect(subject.dob.present?).to be_truthy } + it { expect(subject.address.present?).to be_truthy } + it { expect(subject.postcode.present?).to be_truthy } + it { expect(subject.city.present?).to be_truthy } + it { expect(subject.country.present?).to be_truthy } + it { expect(subject.metadata.nil?).to be_truthy } + it { expect(subject.state).to eq('drafted') } + it { expect(subject.user.labels.find_by(key: 'profile').value).to eq('drafted') } + end + end + + context 'all profile params' do + let!(:params) { + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + } + + it { expect(subject).to be_valid } + it { expect(subject.first_name.present?).to be_truthy } + it { expect(subject.last_name.present?).to be_truthy } + it { expect(subject.dob.present?).to be_truthy } + it { expect(subject.address.present?).to be_truthy } + it { expect(subject.postcode.present?).to be_truthy } + it { expect(subject.city.present?).to be_truthy } + it { expect(subject.country.present?).to be_truthy } + it { expect(subject.metadata.present?).to be_falsey } + it { expect(subject.state).to eq('drafted') } + it { expect(subject.user.labels.find_by(key: 'profile').value).to eq('drafted') } + end + end + + context 'profile_state!' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:user) { create(:user) } + let!(:verified_profile) { create(:profile, user_id: user.id, state: 'verified') } + let!(:rejected_profile) { create(:profile, user_id: user.id, state: 'rejected') } + let!(:drafted_profile) { create(:profile, user_id: user.id, state: 'drafted') } + let!(:profile_params) { + { + user_id: user.id, + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + } + + it 'profile with state drafted' do + profile = Profile.new(profile_params) + profile.save + profile.valid? + + expect(profile.errors[:state]).to eq(['already exists']) + end + + it 'profile with state submitted' do + profile = Profile.new(profile_params.merge(state: 'submitted')) + profile.save + profile.valid? + + expect(profile.errors[:state]).to eq(['already exists']) + end + + it 'profile with state verified' do + profile = Profile.new(profile_params.merge(state: 'verified')) + profile.save + expect(profile.valid?).to eq true + end + + it 'profile with state rejected' do + profile = Profile.new(profile_params.merge(state: 'rejected')) + profile.save + expect(profile.valid?).to eq true + end + end + + context 'create_or_update_profile_label' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:user) { create(:user) } + let!(:profile_params) { + { + user_id: user.id, + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + } + + it 'creating profile label' do + expect(user.labels.count).to eq 0 + + Profile.create(profile_params) + expect(user.labels.count).to eq 1 + expect(user.labels.first.key).to eq 'profile' + expect(user.labels.first.value).to eq 'drafted' + end + + it 'updating profile label' do + profile = Profile.create(profile_params.merge(state: 'submitted')) + expect(user.labels.count).to eq 1 + expect(user.labels.first.key).to eq 'profile' + expect(user.labels.first.value).to eq 'submitted' + + expect(user.labels.count).to eq 1 + + profile.update(state: 'verified') + expect(user.labels.count).to eq 1 + expect(user.labels.first.key).to eq 'profile' + expect(user.labels.first.value).to eq 'verified' + end + + context 'dob param' do + let!(:profile) { create(:profile, user_id: user.id, state: 'verified') } + + it 'should be valid dob' do + profile.update(dob: Time.now) + expect(profile.valid?).to eq true + end + + it 'should be invalid dob format' do + profile.update(dob: '') + expect(profile.valid?).to eq false + expect(profile.errors[:dob]).to eq ["invalid date format"] + end + + it 'should be invalid dob format' do + profile.update(dob: Time.now + 3.days) + expect(profile.valid?).to eq false + expect(profile.errors[:dob]).to eq ["cant be in future"] + end + end + end + + context 'update_document_label' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:user) { create(:user) } + let!(:user_without_document) { create(:user) } + let!(:document_verified_level) { Level.find(4) } + let!(:document_verified_label) { create_label_with_level(user, document_verified_level) } + + let!(:profile_params) { + { + last_name: Faker::Name.last_name, + first_name: Faker::Name.first_name, + dob: Faker::Date.birthday, + country: Faker::Address.country_code_long, + city: Faker::Address.city, + address: Faker::Address.street_address, + postcode: Faker::Address.zip_code + } + } + + it 'change document state when new profile created' do + expect(user.labels.find_by(key: :document).value).to eq 'verified' + + Profile.create(profile_params.merge(user_id: user.id)) + expect(user.labels.count).to eq 2 + expect(user.labels.find_by(key: :document).value).to eq 'verified' + end + + it 'do not change document state when document doesnt exist' do + expect(user_without_document.labels.find_by(key: :document)).to eq nil + + Profile.create(profile_params.merge(user_id: user_without_document.id)) + expect(user_without_document.labels.count).to eq 1 + expect(user_without_document.labels.find_by(key: :document)).to eq nil + end + end + + context 'event api behaviour' do + let!(:permission) { create(:permission, role: 'member') } + let!(:user) { create(:user, state: 'pending', role: 'member') } + let!(:profile) { create(:profile, user_id: user.id, first_name: old_name) } + let!(:old_name) { Faker::Name.first_name } + let!(:new_name) { Faker::Name.first_name } + + let(:profile_update) { profile.update(first_name: new_name ) } + + before do + allow(EventAPI).to receive(:notify) + end + + it 'receives event with label create' do + profile_update + + expect(EventAPI).to have_received(:notify).with('model.profile.updated', + hash_including( + changes: { first_name: old_name }, + record: hash_including(first_name: new_name) + )) + end + end + + context 'validation' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + context 'encrypted fields length' do + let!(:profile) { create(:profile) } + + it 'encrypted first name should be valid with 255 characters' do + profile.update(first_name: Faker::Alphanumeric.alphanumeric(number: 255)) + + expect(profile.valid?).to eq true + end + + it 'encrypted last name should be valid with 255 characters' do + profile.update(last_name: Faker::Alphanumeric.alphanumeric(number: 255)) + + expect(profile.valid?).to eq true + end + + it 'encrypted address should be valid with 255 characters' do + profile.update(address: Faker::Alphanumeric.alphanumeric(number: 255)) + + expect(profile.valid?).to eq true + end + end + end + + context 'submasked fields' do + let!(:create_member_permission) do + create :permission, + role: 'member' + end + + let!(:user) { create(:user) } + + let!(:profile) do + create :profile, first_name: 'Oleksandr', + last_name: 'Berezniy', + city: 'New York', + postcode: 'AB 135-144', + dob: Date.new(2007, 5, 12) + end + + let!(:profile_without_last_name) do + create :profile, first_name: 'Oleksandr', + last_name: nil, + city: 'New York', + postcode: 'AB 135-144' + end + + let!(:profile_without_dob) do + create :profile, first_name: 'Oleksandr', + last_name: nil, + city: 'New York', + postcode: 'AB 135-144', + dob: nil + end + + context 'last_name' do + it { expect(profile.sub_masked_last_name).to eq 'B*******' } + + it 'should mask all letters except first one' do + profile.update(last_name: 'Teylor_1') + expect(profile.sub_masked_last_name).to eq 'T*******' + end + + it 'should mask all letters except first one' do + profile.update(last_name: ' Last name ') + expect(profile.sub_masked_last_name).to eq 'L********' + end + + it 'should mask all letters except first one' do + profile.update(last_name: '') + expect(profile.sub_masked_last_name).to eq '' + end + + it 'should mask all letters except first one' do + expect(profile_without_last_name.sub_masked_last_name).to eq nil + end + end + + context 'dob' do + it { expect(profile.sub_masked_dob).to eq '2007-05-**' } + + it 'should mask only date numbers' do + profile.update(dob: Date.new(2020, 12, 12)) + expect(profile.sub_masked_dob).to eq '2020-12-**' + end + + it 'should mask only date numbers' do + expect(profile_without_dob.sub_masked_dob).to eq nil + end + end + end +end diff --git a/spec/models/restrictions_spec.rb b/spec/models/restrictions_spec.rb new file mode 100644 index 0000000..dee0474 --- /dev/null +++ b/spec/models/restrictions_spec.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +RSpec.describe Restriction, type: :model do + context 'create' do + it { should_not allow_value('planet').for(:scope) } + + it 'valid address with ip scope' do + expect(Restriction.new(scope: 'ip', value: '127.0.0.1', category: 'blacklist').valid?).to be_truthy + end + + it 'invalid address with ip scope' do + expect(Restriction.new(scope: 'ip', value: 'abc.0.0.1', category: 'blacklist').valid?).to be_falsey + end + + it 'ip subnet with ip scope' do + expect(Restriction.new(scope: 'ip', value: '127.0.0.1/24', category: 'blacklist').valid?).to be_falsey + end + + it 'valid subnet with ip_subnet scope' do + expect(Restriction.new(scope: 'ip_subnet', value: '127.0.0.1/24', category: 'blacklist').valid?).to be_truthy + end + + it 'invalid subnet with ip_subnet scope' do + expect(Restriction.new(scope: 'ip_subnet', value: '127.0.0.1/one', category: 'blacklist').valid?).to be_falsey + end + + it 'ip address with ip_subnet scope' do + expect(Restriction.new(scope: 'ip_subnet', value: '127.0.0.1', category: 'blacklist').valid?).to be_falsey + end + end + + context 'session destroy' do + let!(:permission) { create :permission, role: 'member'} + let(:user) { create(:user) } + + before do + allow(Rails.cache).to receive(:delete_matched).and_return(nil) + end + + context 'blocklogin category' do + context 'after create' do + it do + expect(Rails.cache).to receive(:delete_matched) + Restriction.create!(scope: 'ip', value: '127.0.0.1', category: 'blocklogin', state: "enabled") + end + + it do + expect(Rails.cache).to_not receive(:delete_matched) + Restriction.create!(scope: 'ip', value: '127.0.0.2', category: 'blocklogin', state: "disabled") + end + end + + context 'after update' do + let!(:enabled_restriction) {create(:restriction, scope: 'ip', value: '127.0.0.3', category: 'blocklogin', state: "enabled")} + let!(:disabled_restriction) {create( :restriction, scope: 'ip', value: '127.0.0.4', category: 'blocklogin', state: "disabled")} + + it do + expect(Rails.cache).to receive(:delete_matched) + disabled_restriction.update(state: "enabled") + end + + it do + expect(Rails.cache).to_not receive(:delete_matched) + enabled_restriction.update(state: "enabled") + end + + it do + expect(Rails.cache).to_not receive(:delete_matched) + disabled_restriction.update(state: "disabled") + end + + it do + expect(Rails.cache).to_not receive(:delete_matched) + enabled_restriction.update(state: "disabled") + end + end + end + + context 'another category' do + it 'it should not destroy sessions' do + expect(Rails.cache).to_not receive(:delete_matched) + Restriction.create!(scope: 'ip', value: '127.0.0.1', category: 'blacklist', state: "enabled") + end + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb new file mode 100644 index 0000000..61ce410 --- /dev/null +++ b/spec/models/user_spec.rb @@ -0,0 +1,409 @@ +# frozen_string_literal: true + +RSpec.describe User, type: :model do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'local') } + + let!(:create_member_permission) do + create :permission, + role: 'member' + end + context 'User model basic syntax' do + ## Test of data length + it { should validate_length_of(:data) } + + # 1100 char string + let(:big_str) { "string bar"*11 } + + it { should_not allow_value(big_str).for(:data)} + + ## Test of validations + it { should validate_presence_of(:email) } + it { should have_many(:documents).dependent(:destroy) } + + ## Test of relationships + it { should have_many(:profiles).dependent(:destroy) } + + ## Test of UID creation + it 'creates default uid with prefix ID' do + default_user = create(:user) + expect(default_user.uid).to start_with(Barong::App.config.uid_prefix) + end + + it 'uid prefix can be changed by ENV' do + allow(Barong::App.config).to receive(:uid_prefix).and_return('GG') + + default_user = create(:user) + expect(default_user.uid).to start_with('GG') + end + + it 'uid_prefix doesnt case sensitive and always converts to big letters' do + allow(Barong::App.config).to receive(:uid_prefix).and_return('aa') + + default_user = create(:user) + expect(default_user.uid).to start_with('AA') + end + + it do + usr = create(:user) + payload = usr.as_payload + expect(payload['email']).to eq(usr.email) + end + + describe '#referral' do + let!(:user1) { create(:user) } + let!(:user2) { create(:user, referral_id: user1.id) } + + it 'return error when referral doesnt exist' do + record = User.new(uid: 'ID122312323', email: 'test@barong.io', password: 'Oo213Wqw') + record.referral_id = 0 + record.valid? + + expect(record.errors[:referral_id]).to eq(['doesnt_exist']) + end + + it 'return refferal uid' do + expect(user2.referral_uid).to eq user1.uid + end + end + end + + describe '#submitted_profile' do + let!(:user_with_drafted_profile) { create(:user) } + let!(:user_with_submitted_profiles) { create(:user) } + let!(:user_without_profile) { create(:user) } + let!(:drafted_profile) { create(:profile, user: user_with_drafted_profile) } + let!(:submitted_profile) { create(:profile, user: user_with_submitted_profiles, state: 'submitted') } + + it { expect(user_with_drafted_profile.submitted_profile).to eq nil } + it { expect(user_without_profile.submitted_profile).to eq nil } + it { expect(user_with_submitted_profiles.submitted_profile).to eq submitted_profile } + end + + describe '#drafted_profile' do + let!(:user_with_drafted_profile) { create(:user) } + let!(:user_without_profile) { create(:user) } + let!(:drafted_profile) { create(:profile, user: user_with_drafted_profile) } + + it { expect(user_with_drafted_profile.drafted_profile).to eq drafted_profile } + it { expect(user_without_profile.drafted_profile).to eq nil } + end + + describe '#password' do + it { should_not allow_value('Password1').for(:password)} + it { should_not allow_value('Password1123').for(:password)} + it { should_not allow_value('password').for(:password)} + it { should_not allow_value('password1').for(:password)} + it { should_not allow_value('Qq123123').for(:password)} + it { should_not allow_value('QqQq123123').for (:password)} + it { should_not allow_value('X2qL32').for(:password)} + it { should_not allow_value('eoV0qu').for(:password)} + it { should allow_value('Iequ4geiEWQw').for(:password)} + it { should allow_value('Xwqe213PZCXwe').for(:password)} + it { should allow_value('Kal31ewwqXrew').for(:password)} + end + + let(:uploaded_file) { fixture_file_upload('/files/documents_test.jpg', 'image/jpg') } + + context 'User with 2 or more documents' do + it do + user = User.create!(email: 'test@gmail.com', password: 'KeeKi7zoWExzc') + expect(User.count).to eq 1 + document1 = user.documents.create!(upload: uploaded_file, + doc_type: 'Passport', + doc_number: 'MyString', + doc_expire: '01-01-3020') + document2 = user.documents.create!(upload: uploaded_file, + doc_type: 'Passport', + doc_number: 'MyString', + doc_expire: '01-02-3020') + expect(user.reload.documents).to eq([document1, document2]) + end + + after(:all) { User.destroy_all } + end + + describe 'Iso8601TimeFormat' do + let!(:user) { create(:user) } + around(:each) { |example| Time.use_zone('Pacific/Midway') { example.run } } + + it 'parses time in utc and iso8601' do + expect(user.format_iso8601_time(user.created_at)).to \ + eq user.created_at.utc.iso8601 + end + + it 'skips nil' do + expect(user.format_iso8601_time(nil)).to eq nil + end + + it 'parses date to iso8601' do + expect(user.format_iso8601_time(user.created_at.to_date)).to \ + eq user.created_at.to_date.iso8601 + end + end + + describe 'States and labels dependency' do + let(:reqs_list) { {} } + let!(:create_permissions) do + create :permission, role: 'member' + end + + before do + allow(BarongConfig).to receive(:list) { reqs_list } + end + + context 'function testing' do + let!(:user) { create(:user) } + let!(:user_with_no_labels) { create(:user) } + let!(:user_with_labels) do + create(:label, user_id: user.id, key: 'email', value: 'verified', scope: 'private') + create(:label, user_id: user.id, key: 'phone', value: 'verified', scope: 'private') + end + let(:reqs_list) { + { + "activation_requirements" => { + "phone" => "verified", + "documents" => "verified" + }, + "state_triggers" => { + "active_one_of_1_label" => ['email'], + "active_one_of_3_labels" => ['first', 'second', 'third'] + } + } + } + + context 'labels_include?' do + it { expect(user.labels_include?({ 'email' => 'verified' })).to be_truthy } + + it { expect(user.labels_include?({ 'email' => 'pending' })).to be_falsey } + + it { expect(user.labels_include?({ 'email' => 'verified', 'phone' => 'verified' })).to be_truthy } + + it { expect(user.labels_include?({ 'email' => 'verified', 'phone' => 'pending' })).to be_falsey } + end + + context 'private_labels_to_hash' do + it { expect(user.private_labels_to_hash).to eq({ 'email' => 'verified', 'phone' => 'verified' }) } + + it { expect(user_with_no_labels.private_labels_to_hash).to eq({}) } + end + end + + describe 'testing workability with ALL mapping type' do + let!(:user) { create(:user, state: 'pending') } + let(:reqs_list) { + { + "activation_requirements" => { + "phone" => "verified", + "documents" => "verified" + }, + "state_triggers" => { + "active_one_of_1_label" => ['email'], + "active_one_of_3_labels" => ['first', 'second', 'third'] + } + } + } + + context 'changing state on adding label' do + context 'sucessfully' do + it 'changes state when only one label required' do + # email required + expect(user.state).to eq('pending') + + user.labels.create(key: 'email', value: 'verified', scope: 'private') + expect(user.state).to eq('active_one_of_1_label') + end + + it 'rollback from active to pending only in case of deleted one of activation reqs' do + # [phone documents] required + expect(user.state).to eq('pending') + + user.labels.create(key: 'phone', value: 'verified', scope: 'private') + expect(user.state).to eq('pending') + + user.labels.create(key: 'documents', value: 'verified', scope: 'private') + expect(user.state).to eq('active') + + user.labels.create(key: 'random', value: 'verified', scope: 'private') + expect(user.state).to eq('active') + + user.labels.last.destroy + expect(user.state).to eq('active') + + user.labels.find_by_key('phone').destroy + expect(user.state).to eq('pending') + end + + it 'changes state when 2 label required' do + # [phone documents] required + expect(user.state).to eq('pending') + + user.labels.create(key: 'phone', value: 'verified', scope: 'private') + expect(user.state).to eq('pending') + + user.labels.create(key: 'documents', value: 'verified', scope: 'private') + expect(user.state).to eq('active') + end + + it 'changes state when one out of 3 label required' do + # first or second third required + expect(user.state).to eq('pending') + + user.labels.create(key: 'first', value: 'verified', scope: 'private') + expect(user.state).to eq('active_one_of_3_labels') + + user.labels.find_by_key('first').destroy + expect(user.state).to eq('pending') + + user.labels.create(key: 'third', value: 'verified', scope: 'private') + expect(user.state).to eq('active_one_of_3_labels') + + user.labels.find_by_key('third').destroy + expect(user.state).to eq('pending') + + user.labels.create(key: 'second', value: 'verified', scope: 'private') + expect(user.state).to eq('active_one_of_3_labels') + end + end + + context 'not enough labels' do + it 'doesnt change state if provided 1 out of 2 labels only' do + # [phone documents] required + expect(user.state).to eq('pending') + + user.labels.create(key: 'documents', value: 'verified', scope: 'private') + expect(user.state).to eq('pending') + end + end + end + + context 'changing state on deleting' do + context 'ALL policy' do + let!(:give_user_active_state) do + user.labels.create(key: 'phone', value: 'verified', scope: 'private') + user.labels.create(key: 'documents', value: 'verified', scope: 'private') + end + + it 'recalculates from active to when remove all active labels' do + expect(user.state).to eq('active') + Label.find_by(key: 'documents', user: user).destroy + Label.find_by(key: 'phone', user: user).destroy + + user.reload + + expect(user.state).to eq('pending') + end + + it 'recalculates from active_2_labels to active when pending one of active labels on ALL policy' do + expect(user.state).to eq('active') + Label.find_by(key: 'documents', user: user).destroy + user.reload + + expect(user.state).to eq('pending') + end + end + end + end + + describe 'testing workability if config is missing' do + let!(:user) { create(:user, state: 'pending') } + let(:reqs_list) { + { + "activation_requirements" => { + "email" => "verified" + }, + } + } + + context 'not changing state on adding label' do + it 'doesnt changes state if state_triggers config is missing' do + expect(user.state).to eq('pending') + + user.labels.create(key: 'trade', value: 'suspicious', scope: 'private') + expect(user.state).to eq('pending') + end + end + end + + describe 'testing workability with ANY mapping type' do + let!(:user) { create(:user, state: 'pending') } + let(:reqs_list) { + { + "activation_requirements" => { + "email" => "verified" + }, + "state_triggers" => { + "locked" => ['trade', 'withdraw'] + } + } + } + + context 'changing state on adding label' do + context 'sucessfully' do + it 'changes state when only when 1 label out of 2 matches' do + expect(user.state).to eq('pending') + + user.labels.create(key: 'trade', value: 'suspicious', scope: 'private') + expect(user.state).to eq('locked') + end + end + + context 'not enough labels' do + it 'doesnt change state when no label matches' do + expect(user.state).to eq('pending') + user.labels.create(key: 'random', value: 'suspicious', scope: 'private') + + expect(user.state).to eq('pending') + end + end + end + + context 'changing state on deleting' do + context 'ANY policy' do + let!(:give_user_locked_state) do + user.labels.create(key: 'trade', value: 'suspicious', scope: 'private') + user.labels.create(key: 'withdraw', value: 'suspicious', scope: 'private') + end + + it 'doesnt recalculate state if remove one of ANY and another one of ANY still here with ANY policy' do + expect(user.state).to eq('locked') + + user.labels.find_by(key: 'trade').destroy + user.reload + + expect(user.state).to eq('locked') + end + + it 'recalculates if removed all of ANY and there is no more match' do + expect(user.state).to eq('locked') + + user.labels.find_by(key: 'trade').destroy + user.labels.find_by(key: 'withdraw').destroy + + user.reload + expect(user.state).to eq('pending') + end + end + end + end + end + + describe 'event api behaviour' do + let!(:user) { create(:user) } + + context 'user updated' do + before do + allow(EventAPI).to receive(:notify) + end + + it 'receives event with change info' do + user.update(level: 2) + + expect(EventAPI).to have_received(:notify).with('model.user.updated', + hash_including( + changes: { level: 0 }, + record: hash_including(uid: user.uid, level: 2, email: user.email) + )) + end + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..09fc43b --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,74 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' + +ENV['RAILS_ENV'] ||= 'test' + +require File.expand_path('../../config/environment', __FILE__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +require 'cancan/matchers' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end + +Shoulda::Matchers.configure do |config| + config.integrate do |with| + with.test_framework :rspec + with.library :rails + end +end + + +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, :type => :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") + +end diff --git a/spec/services/encryption_service_spec.rb b/spec/services/encryption_service_spec.rb new file mode 100644 index 0000000..2d781a3 --- /dev/null +++ b/spec/services/encryption_service_spec.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +describe EncryptionService do + context 'current_salt' do + before(:each) do + EncryptionService.instance_variable_set(:@cache, {}) + end + + it { expect(EncryptionService.current_salt).to eq Time.now.strftime('%Y%W') } + + it do + allow(Time).to receive(:now).and_return(1.week.ago) + expect(EncryptionService.current_salt).to eq Time.now.strftime('%Y%W') + end + end + + context 'pack' do + before(:each) do + EncryptionService.instance_variable_set(:@cache, {}) + end + + it { expect(EncryptionService.pack('salt', 'value')).to eq 'salt.value' } + it { expect(EncryptionService.pack('salt', nil)).to eq 'salt.' } + it { expect(EncryptionService.pack('salt', '')).to eq 'salt.' } + end + + context 'unpack' do + before(:each) do + EncryptionService.instance_variable_set(:@cache, {}) + end + + it { expect(EncryptionService.unpack('salt.value')).to eq ['salt', 'value'] } + it { expect(EncryptionService.unpack('salt.value.value')).to eq ['salt', 'value.value'] } + it { expect(EncryptionService.unpack(' salt.value')).to eq ['salt', 'value'] } + it { expect(EncryptionService.unpack(' salt_underscore.value')).to eq ['salt_underscore', 'value'] } + it { expect(EncryptionService.unpack('salt.')).to eq ['salt', ''] } + it { expect { EncryptionService.unpack('salt') }.to raise_error('Invalid encrypted value: salt') } + end + + context 'encrypt' do + before(:each) do + EncryptionService.instance_variable_set(:@cache, {}) + end + + let(:time) { Time.now.strftime('%Y%W') } + it { expect(EncryptionService.encrypt('value')).to match(/#{time}/) } + end + + context 'decrypt' do + before(:each) do + EncryptionService.instance_variable_set(:@cache, {}) + end + + let(:encrypted_key) { EncryptionService.encrypt('value') } + it { expect(EncryptionService.decrypt(encrypted_key)).to eq 'value' } + end + + context 'private methods' do + context 'delete_expired_keys' do + context 'ivalid cases' do + context 'cache as number' do + it 'shouldnt delete expired hash when hash is invalid' do + EncryptionService.instance_variable_set(:@cache, 5) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache)).to eq 5 + end + end + + context 'empty hash' do + it 'shouldnt delete expired hash when hash is invalid' do + EncryptionService.instance_variable_set(:@cache, {}) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache)).to eq ({}) + end + end + + context 'invalid hash' do + it 'shouldnt delete expired hash when hash is invalid' do + EncryptionService.instance_variable_set(:@cache, {'test': 'value'}) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache)).to eq ({'test': 'value'}) + end + end + + context 'hash without expire date' do + it 'shouldnt delete expired hash when hash is invalid' do + EncryptionService.instance_variable_set(:@cache, {'test': {'key': 'generated_key'}}) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache)).to eq ({'test': {'key': 'generated_key'}}) + end + end + end + + context 'valid cases' do + context 'should not delete keys' do + it 'shouldnt delete hash keys' do + EncryptionService.instance_variable_set(:@cache, + { + 'test1': {key: 'generated_key', expire_date: Time.now + 3.hours }, + 'test2': {key: 'generated_key', expire_date: Time.now + 2.hours }, + 'test3': {key: 'generated_key', expire_date: Time.now + 1.hours }, + }.with_indifferent_access + ) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache).keys).to match_array %w[test1 test2 test3] + end + end + + context 'should delete keys' do + it 'should delete hash keys' do + EncryptionService.instance_variable_set(:@cache, + { + 'test1': {key: 'generated_key', expire_date: Time.now - 3.hours }, + 'test2': {key: 'generated_key', expire_date: Time.now - 2.hours }, + 'test3': {key: 'generated_key', expire_date: Time.now - 1.hours }, + }.with_indifferent_access + ) + EncryptionService.send(:delete_expired_keys) + expect(EncryptionService.instance_variable_get(:@cache)).to eq({}) + end + end + end + end + + context 'get_master_key' do + context 'empty cache' do + it 'should generate key by salt' do + EncryptionService.instance_variable_set(:@cache, {}) + key = EncryptionService.send(:get_master_key, 'salt') + expect(EncryptionService.instance_variable_get(:@cache)['salt']['key']).to eq key + expect(EncryptionService.instance_variable_get(:@cache)['salt']['expire_date'].to_date).to eq 1.week.from_now.to_date + end + end + + context 'non empty hash' do + let(:values) { + { + "salt":{ + "key": "\xD5\xD8e\\N{1}\xEB\x9DP\x033)\x10{X\x91\xA1V\xB7\xCC\xE1L2\xDF\xC5,\f\x1Fh\xC6", + "expire_date": 1.week.from_now + } + } + } + + it 'should get key from hash' do + EncryptionService.instance_variable_set(:@cache, values) + allow(Time).to receive(:now).and_return(1.week.ago) + key = EncryptionService.send(:get_master_key, 'salt') + expect(key).to eq values['salt']['key'] + end + end + end + end +end diff --git a/spec/services/event_mailer_spec.rb b/spec/services/event_mailer_spec.rb new file mode 100644 index 0000000..64690b7 --- /dev/null +++ b/spec/services/event_mailer_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +describe EventMailer do + let(:event_mailer) { EventMailer.new('', '', '')} + let(:event) { + {:record=> + {:user=> + {:uid=>"ID8434CD6E8E", + :email=>"admin@barong.io", + :role=>"admin", + :level=>1, + :otp=>false, + :state=>"active", + :referral_uid=>nil, + :created_at=>"2020-05-26T07:01:04Z", + :updated_at=>"2020-05-26T08:30:54Z"}, + :user_ip=>"::1", + :user_agent=>"PostmanRuntime/7.25.0"}, + :name=>"system.session.create", + :state=>"sdasd" + } + } + + describe "#nested_hash_value" do + it 'return event value' do + expect(event_mailer.send(:safe_dig, event, %i[name])).to eq event[:name] + expect(event_mailer.send(:safe_dig, event, %i[record user state])).to eq event[:record][:user][:state] + expect(event_mailer.send(:safe_dig, event, %i[record user uid])).to eq event[:record][:user][:uid] + expect(event_mailer.send(:safe_dig, event, %i[record user_agent])).to eq event[:record][:user_agent] + expect(event_mailer.send(:safe_dig, event, %i[record user_ip])).to eq event[:record][:user_ip] + expect(event_mailer.send(:safe_dig, event, %i[record name])).to eq nil + end + end + + describe "#skip_event" do + context 'AND expression' do + let(:expression) { { + :and=> + {:"record.user_ip"=>"::1", :"record.user.role"=>"member"} + } + } + + it 'should skip event' do + expect(event_mailer.send(:skip_event, event, expression)).to eq true + end + + it 'shouldnt skip event' do + expression[:and][:"record.user.role"] = 'admin' + expect(event_mailer.send(:skip_event, event, expression)).to eq false + end + end + + context 'OR expression' do + let(:expression) { { + :or=> + {:"record.user_ip"=>"::1", :"record.user.role"=>"member"} + } + } + + it 'shouldnt skip event' do + expect(event_mailer.send(:skip_event, event, expression)).to eq false + end + + it 'should skip event' do + expression[:or][:"record.user_ip"] = 'test' + expect(event_mailer.send(:skip_event, event, expression)).to eq true + end + end + + context 'NOT expression' do + let(:expression) { { + :not=> + {:"record.user_ip"=>"::1"} + } + } + + it 'shoul skip event' do + expect(event_mailer.send(:skip_event, event, expression)).to eq true + end + + it 'shouldnt skip event' do + expression[:not][:"record.user_ip"] = 'test' + expect(event_mailer.send(:skip_event, event, expression)).to eq false + end + + it 'should skip event' do + expression[:not][:'record.user.email'] = 'admin@barong.io' + expect(event_mailer.send(:skip_event, event, expression)).to eq true + end + + it 'shouldnt skip event' do + expression[:not][:'record.user.email'] = 'admin1@barong.io' + expect(event_mailer.send(:skip_event, event, expression)).to eq false + end + end + end +end diff --git a/spec/services/vault_totp_spec.rb b/spec/services/vault_totp_spec.rb new file mode 100644 index 0000000..1ae477f --- /dev/null +++ b/spec/services/vault_totp_spec.rb @@ -0,0 +1,148 @@ +# frozen_string_literal: true + +describe TOTPService do + let(:uid) { 'uid' } + let(:email) { 'email' } + let(:period) { '60' } + + describe '.with_human_error' do + it 'renders human error when vault is down' do + expect do + described_class.with_human_error do + raise Vault::VaultError, 'Message connection refused message' + end + end.to raise_error(described_class::Error, '2FA server is under maintenance') + end + + it 'renders human error when code was used twice' do + expect do + described_class.with_human_error do + raise Vault::VaultError, 'Message code already used message' + end + end.to raise_error(described_class::Error, + 'This code was already used. Wait until the next time period') + end + + it 'renders error when called without block' do + expect do + described_class.with_human_error + end.to raise_error(ArgumentError, 'Block is required') + end + end + + describe '.server_available?' do + subject { described_class.server_available? } + + context 'when server is available' do + before { expect(described_class).to receive(:read_data) { ['data'] } } + it { is_expected.to eq true } + end + + context 'when server is not available' do + before { expect(described_class).to receive(:read_data) { [] } } + it { is_expected.to eq false } + end + + context 'when exception raised' do + before do + expect(described_class).to receive(:read_data).and_raise(StandardError, 'vault error') + end + + it { is_expected.to eq false } + end + end + + describe '.otp_secret' do + let(:otp_url) { 'otpauth://totp/Alvand:admin@barong.io?secret=code' } + let(:otp) { double(data: { url: otp_url }) } + it { expect(described_class.otp_secret(otp)).to eq 'code' } + end + + describe '.safe_create' do + it 'does not create secret when it exists' do + expect(described_class).to receive(:exist?).with(uid) { true } + expect(described_class).to_not receive(:create) + described_class.safe_create(uid, email, period) + end + + it 'creates secret when it does not exist' do + expect(described_class).to receive(:exist?).with(uid) { false } + expect(described_class).to receive(:create).with(uid, email, period) + described_class.safe_create(uid, email, period) + end + end + + describe '.create' do + let(:create_params) do + { + generate: true, + issuer: 'Alvand', + account_name: 'email', + period: period, + qr_size: 300 + } + end + + it 'creates secret' do + expect(described_class).to receive(:write_data) + .with('totp/keys/alvand_uid', create_params) + described_class.create(uid, email, period) + end + end + + describe '.exist?' do + it 'creates secret' do + expect(described_class).to receive(:read_data) + .with('totp/keys/alvand_uid') { ['data'] } + described_class.exist?(uid) + end + end + + describe '.validate?' do + before do + allow(described_class).to receive(:write_data) { double(data: data) } + allow(described_class).to receive(:read_data) { double(data: data) } + end + let(:data) { { valid: true } } + + subject { described_class.validate?(uid, 'code') } + + context 'when not exist' do + before { expect(described_class).to receive(:exist?) { false } } + it { is_expected.to eq false } + end + + context 'when valid' do + before { expect(described_class).to receive(:exist?) { true } } + it { is_expected.to eq true } + end + + context 'when invalid' do + before { expect(described_class).to receive(:exist?) { true } } + let(:data) { { valid: false } } + it { is_expected.to eq false } + end + end + + describe '.delete' do + before { expect(described_class).to receive(:delete_data) } + it { expect(described_class.delete(uid)) } + end + + describe 'private methods' do + let(:fake_vault) { double(read: 'read', write: 'writed', delete: 'deleted') } + before { stub_const('Vault', double(logical: fake_vault)) } + + it 'read_data reads from vault storage' do + expect(described_class.send(:read_data, 'key')).to eq 'read' + end + + it 'write_data writes to vault storage' do + expect(described_class.send(:write_data, 'key', {})).to eq 'writed' + end + + it 'delete_data deletes from vault storage' do + expect(described_class.send(:delete_data, 'key')).to eq 'deleted' + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..031a1d6 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +require 'rails_helper' + +require_dependency 'barong/jwt' + +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +Dir[Rails.root.join('spec/support/**/*.rb')].sort.each { |f| require f } +RSpec.configure do |config| + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = false + + # See https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example + config.before(:suite) do + FileUtils.rm_rf(File.join(__dir__, 'tmp', 'cache')) + DatabaseCleaner.clean_with(:truncation) + end + + config.before(:each) do + DatabaseCleaner.strategy = :transaction + end + + config.before(:each, clean_database_with_truncation: true) do + FileUtils.rm_rf(File.join(__dir__, 'tmp', 'cache')) + DatabaseCleaner.clean_with :truncation + end + + config.append_after(:each) do + DatabaseCleaner.clean + end + + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.before(:each) do + DatabaseCleaner.start + allow(Ability).to receive(:abilities).and_return( + 'roles' => %w[admin manager accountant superadmin technical compliance support], + 'admin_permissions' => { + 'superadmin' => { 'manage' => %w[User Activity Ability APIKey Profile Permission Label Restriction Level] }, + 'admin' => { 'read' => %w[Level APIKey Permission], 'manage' => %w[User Activity Profile Label] }, + 'compliance' => { 'read' => %w[User Activity Level], 'manage' => %w[Label], 'update' => %w[Profile] }, + 'support' => { 'read' => %w[User Activity APIKey Profile Label Level] } + } + ) + allow_any_instance_of(Barong::Authorize).to receive(:validate_csrf!).and_return(true) + allow(UserStorageTitles).to receive(:list).and_return(%w[personal company]) + %w[email phone identity document].each_with_index do |key, index| + FactoryBot.create(:level, id: index + 1, key: key, value: 'verified') + end + end + + config.after(:each) do + DatabaseCleaner.clean + end + + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # The settings below are suggested to provide a good initial experience + # with RSpec, but feel free to customize to your heart's content. + # # This allows you to limit a spec run to individual examples or groups + # # you care about by tagging them with `:focus` metadata. When nothing + # # is tagged with `:focus`, all examples get run. RSpec also provides + # # aliases for `it`, `describe`, and `context` that include `:focus` + # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + # + # # Allows RSpec to persist some state between runs in order to support + # # the `--only-failures` and `--next-failure` CLI options. We recommend + # # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + # + # # Limits the available syntax to the non-monkey patched syntax that is + # # recommended. For more details, see: + # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + # + # # Many RSpec users commonly either run the entire suite or an individual + # # file, and it's useful to allow more verbose output when running an + # # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + # + # # Print the 10 slowest examples and example groups at the + # # end of the spec run, to help surface which specs are running + # # particularly slow. + # config.profile_examples = 10 + # + # # Seed global randomization in this process using the `--seed` CLI option. + # # Setting this allows you to use `--seed` to deterministically reproduce + # # test failures related to randomization by passing the same `--seed` value + # # as the one that triggered the failure. + # Kernel.srand config.seed +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb new file mode 100644 index 0000000..4ee204c --- /dev/null +++ b/spec/support/api_helpers.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +module APIHelpers + extend Memoist + + def json_body + JSON.parse(response.body, symbolize_names: true) + end + + def expect_status_to_eq(status) + expect_status.to eq status + end + + def expect_status + expect(response.status) + end + + def expect_body + expect(json_body) + end + + def post_json(destination, body, headers = {}) + post destination, + params: build_body(body), + headers: headers.reverse_merge('Content-Type' => 'application/json') + end + + def delete_json(destination, body, headers = {}) + delete destination, + params: build_body(body), + headers: headers.reverse_merge('Content-Type' => 'application/json') + end + + def put_json(destination, body, headers = {}) + put destination, + params: build_body(body), + headers: headers.reverse_merge('Content-Type' => 'application/json') + end + + def build_body(body) + body.is_a?(String) ? body : body.to_json + end + + def create_label_with_level(user, level, scope: 'private') + create(:label, user: user, + key: level.key, + value: level.value, + scope: scope) + end + + def set_level(user, level) + raise "level doesn't exist" if Level.last.id < level + levels = Level.where(id: 1..level) + levels.each do |lvl| + Label.find_or_create_by(user: user, key: lvl.key, value: lvl.value, scope: 'private') + end + end + + def applogic_signed_jwt(payload) + multisig_jwt(payload, management_api_v2_keychain, :james, management_api_v2_algorithms) + end + + def multisig_jwt(payload, keychain, signers, algorithms) + JWT::Multisig.generate_jwt(payload, keychain.slice(*signers), algorithms) + end + + def multisig_jwt_management_api_v2(payload, *signers) + multisig_jwt(payload, management_api_v2_keychain, signers, management_api_v2_algorithms) + end + + def management_api_v2_keychain + require 'openssl' + { james: OpenSSL::PKey::RSA.generate(2048), + john: OpenSSL::PKey::RSA.generate(2048), + david: OpenSSL::PKey::RSA.generate(2048), + robert: OpenSSL::PKey::RSA.generate(2048), + alex: OpenSSL::PKey::RSA.generate(2048), + jeff: OpenSSL::PKey::RSA.generate(2048) } + end + memoize :management_api_v2_keychain + + def management_api_v2_algorithms + management_api_v2_keychain.each_with_object({}) { |(k, _v), memo| memo[k] = 'RS256' } + end + memoize :management_api_v2_algorithms + + def management_api_v2_security_configuration + API::V2::Management::JWTAuthenticationMiddleware.security_configuration + end + + def defaults_for_management_api_v2_security_configuration! + config = { jwt: {} } + config[:keychain] = management_api_v2_keychain.each_with_object({}) do |(signer, key), memo| + memo[signer] = { algorithm: management_api_v2_algorithms.fetch(signer), value: key.public_key } + end + API::V2::Management::JWTAuthenticationMiddleware.security_configuration = config + end + + def codec + @_codec ||= Barong::JWT.new(key: Barong::App.config.keystore.private_key) + end + +end + +RSpec.configure { |config| config.include APIHelpers } diff --git a/spec/support/bearer_context.rb b/spec/support/bearer_context.rb new file mode 100644 index 0000000..c45d11a --- /dev/null +++ b/spec/support/bearer_context.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +shared_context 'bearer authentication' do + + let(:test_user) { create(:user) } + + let(:jwt_token) do + pkey = Rails.application.config.x.keystore.private_key + codec = Barong::JWT.new(key: pkey) + codec.encode(test_user.as_payload) + end + + let(:auth_header) { { 'Authorization' => "Bearer #{jwt_token}" } } +end diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..2e7665c --- /dev/null +++ b/spec/support/factory_bot.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +RSpec.configure do |config| + config.include FactoryBot::Syntax::Methods +end diff --git a/spec/support/geoip_context.rb b/spec/support/geoip_context.rb new file mode 100644 index 0000000..665797d --- /dev/null +++ b/spec/support/geoip_context.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +shared_context 'geoip mock' do + let(:london_ip) { '196.245.163.202' } + let(:tokyo_ip) { '140.227.60.114' } + + before do + class DummyReader + def get(ip) + case ip + when '196.245.163.202' + { + 'country' => { 'names' => { 'en' => 'United Kingdom' } }, + 'continent' => { 'names' => { 'en' => 'Europe' } } + } + when '140.227.60.114' + { + 'country' => { 'names' => { 'en' => 'Japan' } }, + 'continent' => { 'names' => { 'en' => 'Asia' } } + } + else + {} + end + end + end + + reader = DummyReader.new + + allow(Barong::GeoIP).to receive(:reader).and_return(reader) + end +end diff --git a/spec/support/helper.rb b/spec/support/helper.rb new file mode 100644 index 0000000..3766146 --- /dev/null +++ b/spec/support/helper.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Helper + def to_readable(field) + field.to_s.humanize.downcase + end +end + + +RSpec.configure { |config| config.include Helper } diff --git a/spec/workers/address_worker_spec.rb b/spec/workers/address_worker_spec.rb new file mode 100644 index 0000000..34ef021 --- /dev/null +++ b/spec/workers/address_worker_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +describe 'KYC::Kycaid::AddressWorker' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'kycaid') } + + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:user) { create(:user) } + let!(:profile) { create(:profile, country: 'UA', state: 'submitted', applicant_id: '84e51f8a0677a646fd185fc741717ad9a8b3', user_id: user.id) } + let!(:document) { create(:document, identificator: '84e51f8a06', user_id: user.id, doc_type: 'Address') } + let!(:label) { create(:label, key: 'address', user_id: user.id, value: 'pending', scope: 'private') } + let(:address_params) { { "identificator": '84e51f8a06', 'user_id': document.user.id, "country": 'UA', "city": 'Kiev', "postcode": '101010' } } + + describe 'successful verification' do + let(:successful_docs_response) { OpenStruct.new(address_id: '84e51f8a0677a646fd185fc741717ad9a8b3') } + let(:successful_verification_response) { OpenStruct.new(verification_id: '84e51f8a0677a646fd185fc741717ad9a8b3') } + + before { allow(KYCAID::Address).to receive(:create).and_return(successful_docs_response) } + before { allow(KYCAID::Verification).to receive(:create).and_return(successful_verification_response) } + + before { allow_any_instance_of(KYC::Kycaid::AddressWorker).to receive(:address_params).and_return({}) } + + context 'perform' do + before { allow(KYCAID::Address).to receive(:create).and_return(successful_docs_response) } + before { allow(KYCAID::Verification).to receive(:create).and_return(successful_verification_response) } + + it 'creates a document record' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Address).to receive(:create) + + KYC::Kycaid::AddressWorker.new.perform(address_params) + expect(document.reload.metadata).not_to eq(nil) + end + + it 'creates a verification request' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Address).to receive(:create) + expect(KYCAID::Verification).to receive(:create) + + KYC::Kycaid::AddressWorker.new.perform(address_params) + expect(document.reload.metadata).not_to eq(nil) + end + end + end + + describe 'failed verification' do + before { allow_any_instance_of(KYC::Kycaid::AddressWorker).to receive(:address_params).and_return({}) } + let(:unauthorized_response) { OpenStruct.new(error: { "type": 'unauthorized' }) } + let(:unsuccessful_response) do + OpenStruct.new(type: 'validation', errors: [{ "parameter": 'front_file', "message": 'Image is blured' }]) + end + context 'unathorized' do + before { allow(KYCAID::Address).to receive(:create).and_return(unauthorized_response) } + + it 'does not create a address because of error' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Address).to receive(:create) + expect(KYCAID::Verification).not_to receive(:create) + + KYC::Kycaid::AddressWorker.new.perform(address_params) + expect(document.reload.metadata).to eq(nil) + end + end + + context 'validation failed' do + before { allow(KYCAID::Address).to receive(:create).and_return(unsuccessful_response) } + + it 'does not create a address' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Address).to receive(:create) + + KYC::Kycaid::AddressWorker.new.perform(address_params) + expect(document.metadata).to eq(nil) + end + + it 'gets a rejected label' do + expect(document.metadata).to eq(nil) + expect(document.user.labels.count).to eq(2) + expect(document.user.labels.find_by(key: :address).value).to eq('pending') + + expect(KYCAID::Address).to receive(:create) + + KYC::Kycaid::AddressWorker.new.perform(address_params) + expect(document.metadata).to eq(nil) + expect(document.user.labels.count).to eq(2) + expect(document.user.labels.find_by(key: :address).value).to eq('rejected') + end + end + end +end diff --git a/spec/workers/applicant_worker_spec.rb b/spec/workers/applicant_worker_spec.rb new file mode 100644 index 0000000..32b1f41 --- /dev/null +++ b/spec/workers/applicant_worker_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +describe 'KYC::ApplicantWorker' do + include_context 'bearer authentication' + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'kycaid') } + + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:profile) { create(:profile, country: "UA", state: 'submitted') } + + describe 'successful verification' do + let(:successful_response) { OpenStruct.new(applicant_id: "84e51f8a0677a646fd185fc741717ad9a8b3") } + let(:params) {{ type: 'PERSON', first_name: profile.first_name, last_name: profile.last_name, dob: profile.dob, + residence_country: profile.country, email: profile.user.email, phone: profile.user.phones&.last&.number }} + + before { allow(KYCAID::Applicant).to receive(:create).with(params).and_return(successful_response)} + + context 'perform' do + it 'creates an applicant' do + expect(profile.applicant_id).to eq(nil) + expect(KYCAID::Applicant).to receive(:create) + + KYC::Kycaid::ApplicantWorker.new.perform(profile.id) + expect(profile.reload.applicant_id).not_to eq(nil) + end + + it 'changes a label to verified' do + expect(profile.applicant_id).to eq(nil) + expect(profile.user.labels.count).to eq(1) + expect(profile.user.labels.first.key).to eq('profile') + expect(profile.user.labels.first.value).to eq('submitted') + + expect(KYCAID::Applicant).to receive(:create) + KYC::Kycaid::ApplicantWorker.new.perform(profile.id) + + expect(profile.reload.applicant_id).not_to eq(nil) + expect(profile.user.labels.count).to eq(1) + expect(profile.user.labels.first.key).to eq('profile') + expect(profile.user.labels.first.value).to eq('verified') + end + end + end + + describe 'failed verification' do + let(:unauthorized_response) { OpenStruct.new(error: { "type": "unauthorized"}) } + let(:unsuccessful_response) { + OpenStruct.new(type: "validation", errors: [{"parameter": "residence_country", "message": "Country of residence is not valid"}]) + } + + let(:params) {{ type: 'PERSON', first_name: profile.first_name, last_name: profile.last_name, dob: profile.dob, + residence_country: profile.country, email: profile.user.email, phone: profile.user.phones&.last&.number }} + + context 'unathorized' do + before { allow(KYCAID::Applicant).to receive(:create).with(params).and_return(unauthorized_response)} + + it 'does not create an applicant' do + expect(profile.applicant_id).to eq(nil) + expect(KYCAID::Applicant).to receive(:create) + + KYC::Kycaid::ApplicantWorker.new.perform(profile.id) + expect(profile.reload.applicant_id).to eq(nil) + end + end + + context 'validation failed' do + before { allow(KYCAID::Applicant).to receive(:create).with(params).and_return(unsuccessful_response)} + + it 'does not create an applicant' do + expect(profile.applicant_id).to eq(nil) + expect(KYCAID::Applicant).to receive(:create) + + KYC::Kycaid::ApplicantWorker.new.perform(profile.id) + expect(profile.reload.applicant_id).to eq(nil) + end + + it 'gets a rejected label' do + expect(profile.applicant_id).to eq(nil) + expect(KYCAID::Applicant).to receive(:create) + + KYC::Kycaid::ApplicantWorker.new.perform(profile.id) + expect(profile.reload.applicant_id).to eq(nil) + expect(profile.user.labels.count).to eq(1) + expect(profile.user.labels.first.key).to eq('profile') + expect(profile.user.labels.first.value).to eq('rejected') + end + end + end +end diff --git a/spec/workers/document_worker_spec.rb b/spec/workers/document_worker_spec.rb new file mode 100644 index 0000000..c0dda6e --- /dev/null +++ b/spec/workers/document_worker_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +describe 'KYC::Kycaid::DocumentWorker' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'kycaid') } + + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:user) { create(:user) } + let!(:profile) { create(:profile, country: 'UA', state: 'submitted', applicant_id: '84e51f8a0677a646fd185fc741717ad9a8b3', user_id: user.id) } + let!(:document) { create(:document, identificator: '84e51f8a06', doc_type: 'Passport', doc_category: 'front_side', user_id: user.id) } + let!(:document_second) { create(:document, identificator: '84e51f8a06', doc_type: 'Passport', doc_category: 'selfie', user_id: user.id) } + + describe 'successful verification' do + let(:successful_docs_response) { OpenStruct.new(document_id: '84e51f8a0677a646fd185fc741717ad9a8b3') } + let(:successful_verification_response) { OpenStruct.new(verification_id: '84e51f8a0677a646fd185fc741717ad9a8b3') } + + before { allow(KYCAID::Document).to receive(:create).and_return(successful_docs_response) } + before { allow(KYCAID::Verification).to receive(:create).and_return(successful_verification_response) } + + before { allow_any_instance_of(KYC::Kycaid::DocumentWorker).to receive(:document_params).and_return({}) } + before { allow_any_instance_of(KYC::Kycaid::DocumentWorker).to receive(:selfie_image_params).and_return({}) } + + context 'perform' do + it 'creates a document record' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Document).to receive(:create) + + KYC::Kycaid::DocumentWorker.new.perform(document.user.id, '84e51f8a06') + expect(document.reload.metadata).not_to eq(nil) + end + + it 'creates a verification request' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Document).to receive(:create) + expect(KYCAID::Verification).to receive(:create) + + KYC::Kycaid::DocumentWorker.new.perform(document.user.id, '84e51f8a06') + expect(document.reload.metadata).not_to eq(nil) + end + end + end + + describe 'failed verification' do + before { allow_any_instance_of(KYC::Kycaid::DocumentWorker).to receive(:document_params).and_return({}) } + before { allow_any_instance_of(KYC::Kycaid::DocumentWorker).to receive(:selfie_image_params).and_return({}) } + + let(:unauthorized_response) { OpenStruct.new(error: { "type": 'unauthorized' }) } + let(:unsuccessful_response) do + OpenStruct.new(type: 'validation', errors: [{ "parameter": 'front_file', "message": 'Image is blured' }]) + end + + context 'unathorized' do + before { allow(KYCAID::Document).to receive(:create).and_return(unauthorized_response) } + + it 'does not create a document because of error' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Document).to receive(:create) + expect(KYCAID::Verification).not_to receive(:create) + + KYC::Kycaid::DocumentWorker.new.perform(document.user.id, '84e51f8a06') + expect(document.reload.metadata).to eq(nil) + end + end + + context 'validation failed' do + before { allow(KYCAID::Document).to receive(:create).and_return(unsuccessful_response) } + + it 'does not create a document' do + expect(document.metadata).to eq(nil) + expect(KYCAID::Document).to receive(:create) + + KYC::Kycaid::DocumentWorker.new.perform(document.user.id, '84e51f8a06') + expect(document.metadata).to eq(nil) + end + + it 'gets a rejected label' do + expect(document.metadata).to eq(nil) + expect(document.user.labels.count).to eq(2) + expect(document.user.labels.find_by(key: :document).value).to eq('pending') + + expect(KYCAID::Document).to receive(:create) + + KYC::Kycaid::DocumentWorker.new.perform(document.user.id, '84e51f8a06') + expect(document.metadata).to eq(nil) + expect(document.user.labels.count).to eq(2) + expect(document.user.labels.find_by(key: :document).value).to eq('rejected') + end + end + end +end diff --git a/spec/workers/verification_worker_spec.rb b/spec/workers/verification_worker_spec.rb new file mode 100644 index 0000000..9ffa5a6 --- /dev/null +++ b/spec/workers/verification_worker_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +describe 'KYC::Kycaid::VerificationWorker' do + before { allow(Barong::App.config).to receive_messages(kyc_provider: 'kycaid') } + + include_context 'bearer authentication' + let!(:create_member_permission) do + create :permission, + role: 'member' + end + let(:user) { create(:user) } + let!(:profile) { create(:profile, country: 'UA', state: 'submitted', applicant_id: '84e51f8a0677a646fd185fc741717ad9a8b3', user_id: user.id) } + let!(:document) { create(:document, identificator: '84e51f8a06', doc_type: 'Passport', doc_category: 'front_side', user_id: user.id) } + let!(:document_second) { create(:document, identificator: '84e51f8a06', doc_type: 'Passport', doc_category: 'selfie', user_id: user.id) } + + describe 'successful verifications' do + let(:successful_verification_response) { OpenStruct.new(status: 'completed', verifications: { document: { verified: true } }) } + before { allow(KYCAID::Verification).to receive(:fetch).and_return(successful_verification_response) } + + context 'perform' do + it 'fetches verification data' do + expect(user.labels.count).to eq(2) + expect(user.labels.find_by(key: :document, scope: :private).value).to eq('pending') + + expect(KYCAID::Verification).to receive(:fetch) + + KYC::Kycaid::VerificationsWorker.new.perform({ applicant_id: profile.applicant_id, verification_id: '84e51f8a' }) + end + + it 'changes label accordingly to verification' do + expect(user.labels.count).to eq(2) + expect(user.labels.find_by(key: :document, scope: :private).value).to eq('pending') + + expect(KYCAID::Verification).to receive(:fetch) + + KYC::Kycaid::VerificationsWorker.new.perform({ applicant_id: profile.applicant_id, verification_id: '84e51f8a' }) + + expect(user.labels.count).to eq(2) + expect(user.labels.reload.find_by(key: :document, scope: :private).value).to eq('verified') + end + end + end + + describe 'failed verification' do + let(:unsuccessful_response) { OpenStruct.new(status: 'completed', verifications: { document: { verified: false } }) } + before { allow(KYCAID::Verification).to receive(:fetch).and_return(unsuccessful_response) } + + let(:unauthorized_response) { OpenStruct.new(error: { "type": 'unauthorized' }) } + + context 'unathorized' do + before { allow(KYCAID::Verification).to receive(:fetch).and_return(unauthorized_response) } + + it 'does not create a document because of error' do + expect(user.labels.count).to eq(2) + expect(user.labels.find_by(key: :document, scope: :private).value).to eq('pending') + + expect(KYCAID::Verification).to receive(:fetch) + + KYC::Kycaid::VerificationsWorker.new.perform({ applicant_id: profile.applicant_id, verification_id: '84e51f8a' }) + expect(user.labels.reload.find_by(key: :document, scope: :private).value).to eq('pending') + end + end + + context 'verification declined' do + it 'fetches verification data' do + expect(user.labels.count).to eq(2) + expect(user.labels.find_by(key: :document, scope: :private).value).to eq('pending') + + expect(KYCAID::Verification).to receive(:fetch) + + KYC::Kycaid::VerificationsWorker.new.perform({ applicant_id: profile.applicant_id, verification_id: '84e51f8a' }) + end + + it 'changes label accordingly to verification' do + expect(user.labels.count).to eq(2) + expect(user.labels.find_by(key: :document, scope: :private).value).to eq('pending') + + expect(KYCAID::Verification).to receive(:fetch) + + KYC::Kycaid::VerificationsWorker.new.perform({ applicant_id: profile.applicant_id, verification_id: '84e51f8a' }) + + expect(user.labels.count).to eq(2) + expect(user.labels.reload.find_by(key: :document, scope: :private).value).to eq('rejected') + end + end + end +end diff --git a/storage/.keep b/storage/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test.rb b/test.rb new file mode 100644 index 0000000..95f207e --- /dev/null +++ b/test.rb @@ -0,0 +1,24 @@ +module Mo + MAPPING = { a: 'a', b: 'b' } + @mapping = 'ayza2' + class << self + @mapping_singleton = 'ayza' + def test + a = :a + p MAPPING.dig(a) + p @mapping + end + end + + def test2 + p MAPPING + end +end + + +Mo.test + +# Mo::test2 + +p Mo.singleton_class.instance_variables +p Mo.instance_variables diff --git a/test/application_system_test_case.rb b/test/application_system_test_case.rb new file mode 100644 index 0000000..d19212a --- /dev/null +++ b/test/application_system_test_case.rb @@ -0,0 +1,5 @@ +require "test_helper" + +class ApplicationSystemTestCase < ActionDispatch::SystemTestCase + driven_by :selenium, using: :chrome, screen_size: [1400, 1400] +end diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/.keep b/test/fixtures/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/fixtures/files/.keep b/test/fixtures/files/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/system/.keep b/test/system/.keep new file mode 100644 index 0000000..e69de29 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..3ab84e3 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,10 @@ +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +require 'rails/test_help' + +class ActiveSupport::TestCase + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + # Add more helper methods to be used by all tests here... +end diff --git a/tmp/.keep b/tmp/.keep new file mode 100644 index 0000000..e69de29 diff --git a/vendor/.keep b/vendor/.keep new file mode 100644 index 0000000..e69de29