236 lines
6.7 KiB
Ruby
236 lines
6.7 KiB
Ruby
# 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
|