eventush 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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