eventush 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +28 -0
  3. data/Rakefile +22 -0
  4. data/app/consumers/barong/model/label_updated_consumer.rb +25 -0
  5. data/app/consumers/peatio/model/deposit_created_consumer.rb +23 -0
  6. data/app/controllers/concerns/exception_handlers.rb +19 -0
  7. data/app/controllers/concerns/jwt_payload.rb +19 -0
  8. data/app/controllers/concerns/response.rb +25 -0
  9. data/app/controllers/event/api/v2/admin/base_controller.rb +22 -0
  10. data/app/controllers/event/api/v2/admin/events_controller.rb +59 -0
  11. data/app/controllers/event/application_controller.rb +9 -0
  12. data/app/helpers/event/application_helper.rb +4 -0
  13. data/app/helpers/event/event_helper.rb +4 -0
  14. data/app/models/concerns/currency.rb +26 -0
  15. data/app/models/event/application_record.rb +5 -0
  16. data/app/models/event/event.rb +82 -0
  17. data/app/models/event/participant.rb +83 -0
  18. data/app/services/barong/management_api_v2/client.rb +33 -0
  19. data/app/services/event_api_listener.rb +120 -0
  20. data/app/services/management_api_v2/client.rb +73 -0
  21. data/app/services/management_api_v2/exception.rb +25 -0
  22. data/app/services/peatio/management_api_v2/client.rb +27 -0
  23. data/config/initializers/active_model.rb +13 -0
  24. data/config/initializers/api_pagination.rb +33 -0
  25. data/config/initializers/inflections.rb +18 -0
  26. data/config/routes.rb +12 -0
  27. data/db/migrate/20200825103859_create_events.rb +24 -0
  28. data/lib/event.rb +10 -0
  29. data/lib/event/engine.rb +17 -0
  30. data/lib/event/version.rb +3 -0
  31. data/lib/tasks/auto_annotate_models.rake +59 -0
  32. data/lib/tasks/event_api.rake +9 -0
  33. data/lib/tasks/event_tasks.rake +4 -0
  34. data/spec/controllers/event/api/v2/admin/metadata_controller_spec.rb +72 -0
  35. data/spec/dummy/Rakefile +6 -0
  36. data/spec/dummy/app/assets/config/manifest.js +3 -0
  37. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  38. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  39. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  40. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  41. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  42. data/spec/dummy/app/javascript/packs/application.js +15 -0
  43. data/spec/dummy/app/jobs/application_job.rb +7 -0
  44. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  45. data/spec/dummy/app/models/application_record.rb +3 -0
  46. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  47. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  48. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  49. data/spec/dummy/bin/rails +4 -0
  50. data/spec/dummy/bin/rake +4 -0
  51. data/spec/dummy/bin/setup +33 -0
  52. data/spec/dummy/config.ru +5 -0
  53. data/spec/dummy/config/application.rb +30 -0
  54. data/spec/dummy/config/application.yml +42 -0
  55. data/spec/dummy/config/boot.rb +5 -0
  56. data/spec/dummy/config/cable.yml +10 -0
  57. data/spec/dummy/config/database.yml +17 -0
  58. data/spec/dummy/config/environment.rb +5 -0
  59. data/spec/dummy/config/environments/development.rb +62 -0
  60. data/spec/dummy/config/environments/production.rb +112 -0
  61. data/spec/dummy/config/environments/test.rb +48 -0
  62. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  63. data/spec/dummy/config/initializers/assets.rb +12 -0
  64. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  65. data/spec/dummy/config/initializers/content_security_policy.rb +28 -0
  66. data/spec/dummy/config/initializers/cookies_serializer.rb +5 -0
  67. data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  68. data/spec/dummy/config/initializers/inflections.rb +18 -0
  69. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  70. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  71. data/spec/dummy/config/locales/en.yml +33 -0
  72. data/spec/dummy/config/puma.rb +38 -0
  73. data/spec/dummy/config/routes.rb +3 -0
  74. data/spec/dummy/config/spring.rb +6 -0
  75. data/spec/dummy/config/storage.yml +34 -0
  76. data/spec/dummy/db/schema.rb +37 -0
  77. data/spec/dummy/public/404.html +67 -0
  78. data/spec/dummy/public/422.html +67 -0
  79. data/spec/dummy/public/500.html +66 -0
  80. data/spec/dummy/public/apple-touch-icon-precomposed.png +0 -0
  81. data/spec/dummy/public/apple-touch-icon.png +0 -0
  82. data/spec/dummy/public/favicon.ico +0 -0
  83. data/spec/dummy/tmp/development_secret.txt +1 -0
  84. data/spec/factories/event/events.rb +22 -0
  85. data/spec/factories/event/participants.rb +21 -0
  86. data/spec/factories/event/sequences.rb +26 -0
  87. data/spec/models/event/event_spec.rb +24 -0
  88. data/spec/models/event/participant_spec.rb +23 -0
  89. data/spec/rails_helper.rb +72 -0
  90. data/spec/routing/metadastore/metadata_routing_spec.rb +29 -0
  91. data/spec/spec_helper.rb +96 -0
  92. data/spec/support/api_helper.rb +46 -0
  93. data/spec/support/auth_helper.rb +15 -0
  94. data/spec/support/rspec_matchers.rb +17 -0
  95. metadata +352 -0
@@ -0,0 +1,33 @@
1
+ module Barong
2
+ module ManagementAPIV2
3
+ class Client < ::ManagementAPIV2::Client
4
+ def initialize(*)
5
+ super ENV.fetch('BARONG_URL'), Rails.configuration.x.barong_management_api_v2_configuration
6
+ end
7
+
8
+ def otp_sign(request_params = {})
9
+ self.action = :otp_sign
10
+ params = request_params.slice(:user_uid, :otp_code, :jwt)
11
+ request(:post, 'otp/sign', params)
12
+ end
13
+
14
+ def get_user_info(request_params={})
15
+ self.action = :read_users
16
+ params = request_params.slice(:uid, :extended, :jwt)
17
+ request(:post, "users/get", params)
18
+ end
19
+
20
+ def update_label(request_params = {})
21
+ self.action = :write_labels
22
+ params = request_params.slice(:user_uid, :key, :value, :jwt, :replace)
23
+ request(:put, 'labels', params)
24
+ end
25
+
26
+ def create_label(request_params = {})
27
+ self.action = :write_labels
28
+ params = request_params.slice(:user_uid, :key, :value, :jwt)
29
+ request(:post, 'labels', params)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bunny'
4
+
5
+ class EventAPIListener
6
+ def initialize(application, event_category, event_name)
7
+ @application = application
8
+ @event_category = event_category
9
+ @event_name = event_name
10
+ Kernel.at_exit { unlisten }
11
+ end
12
+
13
+ def call
14
+ consumer # Eager load consumer to early detect errors.
15
+ listen
16
+ end
17
+
18
+ private
19
+
20
+ def event_api_jwt_env(suffix)
21
+ ENV["#{@application.upcase}_EVENT_API_JWT_#{suffix}"]
22
+ end
23
+
24
+ def event_api_jwt_env_to_int(suffix)
25
+ event_api_jwt_env(suffix).to_s.squish.yield_self { |n| n.to_i if n.present? }
26
+ end
27
+
28
+ def listen
29
+ unlisten
30
+ @bunny_session = Bunny::Session.new(rabbitmq_credentials).tap(&:start)
31
+ @bunny_channel = @bunny_session.channel
32
+ exchange_name = [@application, 'events', @event_category].join('.')
33
+ exchange = @bunny_channel.direct(exchange_name)
34
+ queue = @bunny_channel.queue('', auto_delete: true, durable: true)
35
+ .bind(exchange, routing_key: @event_name)
36
+ Rails.logger.info { "Listening for #{exchange_name}.#{@event_name}." }
37
+ queue.subscribe(block: true, &method(:handle_message))
38
+ end
39
+
40
+ def unlisten
41
+ Rails.logger.info { 'No longer listening for events.' } if @bunny_session || @bunny_channel
42
+ @bunny_channel&.work_pool&.kill
43
+ @bunny_session&.stop
44
+ ensure
45
+ @bunny_channel = nil
46
+ @bunny_session = nil
47
+ end
48
+
49
+ def jwt_public_key
50
+ pem = Base64.urlsafe_decode64(event_api_jwt_env('PUBLIC_KEY'))
51
+ OpenSSL::PKey.read(pem)
52
+ end
53
+
54
+ def token_verification_options
55
+ # We set option only if it is not blank.
56
+ { verify_jti: true,
57
+ iss: event_api_jwt_env('ISSUER').to_s.squish.presence,
58
+ verify_iss: event_api_jwt_env('ISSUER').present?,
59
+ aud: event_api_jwt_env('AUDIENCE').to_s.squish.presence,
60
+ verify_aud: event_api_jwt_env('AUDIENCE').present?,
61
+ sub: event_api_jwt_env('SUBJECT'),
62
+ verify_sub: event_api_jwt_env('SUBJECT').present? }
63
+ end
64
+
65
+ def algorithm_verification_options
66
+ { algorithms: [ENV.fetch("#{@application.upcase}_EVENT_API_JWT_ALGORITHM")] }
67
+ end
68
+
69
+ def timing_verification_options
70
+ { verify_expiration: true, verify_not_before: true, verify_iat: true,
71
+ leeway: event_api_jwt_env_to_int('DEFAULT_LEEWAY'),
72
+ iat_leeway: event_api_jwt_env_to_int('ISSUED_AT_LEEWAY'),
73
+ exp_leeway: event_api_jwt_env_to_int('EXPIRATION_LEEWAY'),
74
+ nbf_leeway: event_api_jwt_env_to_int('NOT_BEFORE_LEEWAY') }
75
+ end
76
+
77
+ def rabbitmq_credentials
78
+ @application.upcase.yield_self do |_app|
79
+ if ENV['EVENT_API_RABBITMQ_URL'].present?
80
+ ENV['EVENT_API_RABBITMQ_URL']
81
+ else
82
+ { host: ENV.fetch('EVENT_API_RABBITMQ_HOST'),
83
+ port: ENV.fetch('EVENT_API_RABBITMQ_PORT'),
84
+ username: ENV.fetch('EVENT_API_RABBITMQ_USERNAME'),
85
+ password: ENV.fetch('EVENT_API_RABBITMQ_PASSWORD') }
86
+ end
87
+ end
88
+ end
89
+
90
+ def handle_message(_delivery_info, _metadata, payload)
91
+ result = verify_jwt(payload)
92
+
93
+ raise "Failed to verify signature from #{@application}." \
94
+ unless result[:verified].include?(@application.to_sym)
95
+ consumer.call(result[:payload].fetch(:event))
96
+ rescue StandardError => e
97
+ Rails.logger.error { e.inspect }
98
+ end
99
+
100
+ def consumer
101
+ [@application, @event_category, @event_name.tr('.', '_') + '_consumer']
102
+ .map { |x| x.tr('.', '_') }
103
+ .join('/')
104
+ .camelize
105
+ .constantize
106
+ end
107
+
108
+ def verify_jwt(payload)
109
+ options = token_verification_options.merge(timing_verification_options)
110
+ .merge(algorithm_verification_options)
111
+ JWT::Multisig.verify_jwt JSON.parse(payload), { @application.to_sym => jwt_public_key },
112
+ options.compact
113
+ end
114
+
115
+ class << self
116
+ def call(*args)
117
+ new(*args).call
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module ManagementAPIV2
6
+ class Client
7
+
8
+ attr_reader :action
9
+
10
+ def initialize(root_url, security_configuration)
11
+ @root_api_url = root_url
12
+ @security_configuration = security_configuration
13
+ end
14
+
15
+ def request(request_method, request_path, request_parameters, options = {})
16
+ options = { jwt: false }.merge(options)
17
+ raise ArgumentError, "Request method is not supported: #{request_method.inspect}." unless request_method.in?(%i[post put])
18
+
19
+ request_parameters = generate_jwt(payload(request_parameters)) unless options[:jwt]
20
+
21
+ begin
22
+ http_client
23
+ .public_send(request_method, build_path(request_path), request_parameters)
24
+ .tap { |response| raise ManagementAPIV2::Exception.new(response) unless response.success? }
25
+ .body
26
+ .symbolize_keys
27
+
28
+ rescue Faraday::Error => e
29
+ raise ManagementAPIV2::Exception.new
30
+ end
31
+ end
32
+
33
+ def build_path(path)
34
+ "api/v2/management/#{path}"
35
+ end
36
+
37
+ def http_client
38
+ Faraday.new(url: @root_api_url) do |conn|
39
+ conn.request :json
40
+ conn.response :json
41
+ conn.adapter Faraday.default_adapter
42
+ end
43
+ end
44
+
45
+ def keychain(field)
46
+ {}.tap do |h|
47
+ @security_configuration[:keychain].each do |id, key|
48
+ next unless action
49
+ next unless id.in?(action[:required_signatures])
50
+ h[id] = key[field]
51
+ end
52
+ end
53
+ end
54
+
55
+ def payload(data = {})
56
+ {
57
+ data: data,
58
+ iat: Time.now.to_i,
59
+ exp: Time.now.to_i + ENV.fetch('JWT_EXPIRE_DATE', 60).to_i,
60
+ jti: SecureRandom.hex(12),
61
+ iss: 'applogic'
62
+ }
63
+ end
64
+
65
+ def generate_jwt(payload)
66
+ JWT::Multisig.generate_jwt(payload, keychain(:value), keychain(:algorithm))
67
+ end
68
+
69
+ def action=(value)
70
+ @action = @security_configuration[:actions].fetch(value)
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ManagementAPIV2
4
+ class Exception < StandardError
5
+ attr_accessor :status
6
+
7
+ def initialize(response_or_ex="External services error")
8
+ @status = 503
9
+ if response_or_ex.respond_to?(:body)
10
+ @status = 422
11
+ body = response_or_ex.body || {}
12
+
13
+ if body.fetch("error", false)
14
+ super body.fetch("error")
15
+ elsif body.fetch("errors", false)
16
+ super Array(body.fetch("errors")).first
17
+ else
18
+ super response_or_ex.body
19
+ end
20
+ else
21
+ super response_or_ex
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ module Peatio
2
+ module ManagementAPIV2
3
+ class Client < ::ManagementAPIV2::Client
4
+ def initialize(*)
5
+ super ENV.fetch('PEATIO_URL'), Rails.configuration.x.peatio_management_api_v2_configuration
6
+ end
7
+
8
+ def create_transfer(request_params={})
9
+ self.action = :write_transfers
10
+ params = request_params.slice(:key, :category, :description, :operations)
11
+ request(:post, "transfers/new", params, {})
12
+ end
13
+
14
+ def balance(request_params={})
15
+ self.action = :read_accounts
16
+ params = request_params.slice(:uid, :currency)
17
+ request(:post, "/accounts/balance", params, {})
18
+ end
19
+
20
+ def currency(request_params={})
21
+ self.action = :read_currencies
22
+ params = request_params.slice(:code)
23
+ request(:post, "/currencies/#{params[:code]}", {})
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Module.new do
4
+ def api_messages
5
+ map do |attr, err|
6
+ if err.start_with?("_")
7
+ [attr, err]
8
+ else
9
+ [err, attr]
10
+ end.join
11
+ end
12
+ end
13
+ end.tap { |m| ActiveSupport.on_load(:active_record) { ActiveModel::Errors.include(m) } }
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "api-pagination"
4
+ require "pagy/countless"
5
+ require "pagy/extras/overflow"
6
+
7
+ ApiPagination.configure do |config|
8
+ # If you have more than one gem included, you can choose a paginator.
9
+ config.paginator = :pagy # or :will_ApiPagination.paginate
10
+
11
+ # By default, this is set to 'Total'
12
+ # config.total_header = 'X-Total'
13
+
14
+ # By default, this is set to 'Per-Page'
15
+ # config.per_page_header = 'X-Per-Page'
16
+
17
+ # Optional: set this to add a header with the current page number.
18
+ config.page_header = "Page"
19
+
20
+ # Optional: set this to add other response format. Useful with tools that define :jsonapi format
21
+ # config.response_formats = [:json, :xml, :jsonapi]
22
+
23
+ # Optional: what parameter should be used to set the page option
24
+ config.page_param = :page
25
+
26
+ # Optional: what parameter should be used to set the per page option
27
+ config.per_page_param = :limit
28
+
29
+ # Optional: Include the total and last_page link header
30
+ # By default, this is set to true
31
+ # Note: When using kaminari, this prevents the count call to the database
32
+ # config.include_total = false
33
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+ # Be sure to restart your server when you modify this file.
3
+
4
+ # Add new inflection rules using the following format. Inflections
5
+ # are locale specific, and you may define rules for as many different
6
+ # locales as you wish. All of these examples are active by default:
7
+ # ActiveSupport::Inflector.inflections(:en) do |inflect|
8
+ # inflect.plural /^(ox)$/i, '\1en'
9
+ # inflect.singular /^(ox)en/i, '\1'
10
+ # inflect.irregular 'person', 'people'
11
+ # inflect.uncountable %w( fish sheep )
12
+ # end
13
+
14
+ # These inflection rules are supported but not enabled by default:
15
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
16
+ inflect.acronym "API"
17
+ inflect.acronym "JWT"
18
+ end
data/config/routes.rb ADDED
@@ -0,0 +1,12 @@
1
+ Rails.application.routes.draw do
2
+ scope module: 'event' do
3
+ namespace :api do
4
+ namespace :v2 do
5
+ namespace :admin do
6
+ get '/events/history', to: 'events#history'
7
+ resources :events, only: %i[index show create update]
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,24 @@
1
+ class CreateEvents < ActiveRecord::Migration[6.0]
2
+ def change
3
+ create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
4
+ t.string :name, null: false
5
+ t.string :trigger_key, null: false
6
+ t.string :trigger_value, null: false
7
+ t.decimal :amount, null: false
8
+ t.string :currency_id, null: false
9
+ t.integer :state, null: false
10
+ t.string :creator_uid, null: false
11
+ t.string :description
12
+
13
+ t.timestamps
14
+ end
15
+
16
+ create_table "event_participants", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
17
+ t.references :event, null: false
18
+ t.string :uid, null: false
19
+ t.string :transfer_keys, limit: 1000
20
+
21
+ t.timestamps
22
+ end
23
+ end
24
+ end
data/lib/event.rb ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "event/engine"
4
+
5
+ require "peatio"
6
+ require "ransack"
7
+ require "rails/pagination"
8
+ require "jwt-multisig"
9
+
10
+ module Event end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Event
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Event
6
+
7
+ config.generators do |generators|
8
+ generators.test_framework :rspec
9
+ generators.fixture_replacement :factory_bot
10
+ generators.factory_bot dir: "spec/factories"
11
+ end
12
+
13
+ initializer "metadata.factories", after: "factory_bot.set_factory_paths" do
14
+ FactoryBot.definition_file_paths << File.expand_path("../../spec/factories", __dir__) if defined?(FactoryBot)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,3 @@
1
+ module Event
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,59 @@
1
+ # NOTE: only doing this in development as some production environments (Heroku)
2
+ # NOTE: are sensitive to local FS writes, and besides -- it's just not proper
3
+ # NOTE: to have a dev-mode tool do its thing in production.
4
+ if Rails.env.development?
5
+ require 'annotate'
6
+ task :set_annotation_options do
7
+ # You can override any of these by setting an environment variable of the
8
+ # same name.
9
+ Annotate.set_defaults(
10
+ 'active_admin' => 'false',
11
+ 'additional_file_patterns' => [],
12
+ 'routes' => 'false',
13
+ 'models' => 'true',
14
+ 'position_in_routes' => 'after',
15
+ 'position_in_class' => 'after',
16
+ 'position_in_test' => 'after',
17
+ 'position_in_fixture' => 'after',
18
+ 'position_in_factory' => 'after',
19
+ 'position_in_serializer' => 'after',
20
+ 'show_foreign_keys' => 'true',
21
+ 'show_complete_foreign_keys' => 'false',
22
+ 'show_indexes' => 'true',
23
+ 'simple_indexes' => 'false',
24
+ 'model_dir' => 'app/models',
25
+ 'root_dir' => '',
26
+ 'include_version' => 'false',
27
+ 'require' => '',
28
+ 'exclude_tests' => 'false',
29
+ 'exclude_fixtures' => 'false',
30
+ 'exclude_factories' => 'false',
31
+ 'exclude_serializers' => 'false',
32
+ 'exclude_scaffolds' => 'true',
33
+ 'exclude_controllers' => 'true',
34
+ 'exclude_helpers' => 'true',
35
+ 'exclude_sti_subclasses' => 'false',
36
+ 'ignore_model_sub_dir' => 'false',
37
+ 'ignore_columns' => nil,
38
+ 'ignore_routes' => nil,
39
+ 'ignore_unknown_models' => 'false',
40
+ 'hide_limit_column_types' => 'integer,bigint,boolean',
41
+ 'hide_default_column_types' => 'json,jsonb,hstore',
42
+ 'skip_on_db_migrate' => 'false',
43
+ 'format_bare' => 'true',
44
+ 'format_rdoc' => 'false',
45
+ 'format_yard' => 'false',
46
+ 'format_markdown' => 'false',
47
+ 'sort' => 'false',
48
+ 'force' => 'false',
49
+ 'frozen' => 'false',
50
+ 'classified_sort' => 'true',
51
+ 'trace' => 'false',
52
+ 'wrapper_open' => nil,
53
+ 'wrapper_close' => nil,
54
+ 'with_comment' => 'true'
55
+ )
56
+ end
57
+
58
+ Annotate.load_tasks
59
+ end