web47core 0.0.9 → 0.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +16 -4
- data/README.md +37 -33
- data/config/locales/en.yml +11 -0
- data/lib/app/models/concerns/app47_logger.rb +1 -1
- data/lib/app/models/concerns/core_system_configuration.rb +5 -1
- data/lib/app/models/concerns/email_able.rb +84 -0
- data/lib/app/models/concerns/search_able.rb +100 -0
- data/lib/app/models/concerns/standard_model.rb +2 -2
- data/lib/app/models/concerns/time_zone_able.rb +49 -0
- data/lib/app/models/notification.rb +3 -3
- data/lib/app/models/redis_configuration.rb +137 -0
- data/lib/app/models/slack_notification.rb +14 -30
- data/lib/app/models/sms_notification.rb +5 -7
- data/lib/app/models/smtp_configuration.rb +14 -89
- data/lib/web47core.rb +4 -0
- data/test/fixtures/redis/host.yml +5 -0
- data/test/fixtures/redis/options.yml +8 -0
- data/test/fixtures/redis/sentinel.yml +8 -0
- data/test/fixtures/redis/url.yml +2 -0
- data/test/models/concerns/email_able_test.rb +145 -0
- data/test/models/concerns/search_able_test.rb +80 -0
- data/test/models/concerns/standard_model_test.rb +62 -1
- data/test/models/concerns/time_zone_able_test.rb +77 -0
- data/test/models/redis_configuration_test.rb +86 -0
- data/test/models/slack_notification_test.rb +41 -0
- data/test/models/sms_notification_test.rb +69 -0
- data/test/models/smtp_configuration_test.rb +66 -0
- data/test/test_helper.rb +5 -1
- data/web47core.gemspec +7 -4
- metadata +87 -14
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Objects that can localize time to it, kicking out safe uses of a date format.
|
5
|
+
#
|
6
|
+
module TimeZoneAble
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
def self.included(base)
|
10
|
+
base.class_eval do
|
11
|
+
#
|
12
|
+
# Fields
|
13
|
+
#
|
14
|
+
field :time_zone, type: String
|
15
|
+
#
|
16
|
+
# Validations
|
17
|
+
#
|
18
|
+
validates :time_zone, inclusion: TZInfo::Timezone.all_identifiers, presence: true
|
19
|
+
#
|
20
|
+
# Callbacks
|
21
|
+
#
|
22
|
+
before_validation :default_time_zone
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# Return the given time in the localized time for this object
|
28
|
+
#
|
29
|
+
def local_time(time, format = :medium, default = 'N/A')
|
30
|
+
tz = TZInfo::Timezone.get(time_zone.presence || SystemConfiguration.default_time_zone)
|
31
|
+
time.present? ? I18n.l(time.in_time_zone(tz), format: format) : default
|
32
|
+
rescue StandardError
|
33
|
+
default
|
34
|
+
end
|
35
|
+
|
36
|
+
#
|
37
|
+
# Return the updated_at if found, otherwise return the created_at. If neither are there
|
38
|
+
# then return unknown
|
39
|
+
#
|
40
|
+
def local_updated_time(obj, format = :medium, default = 'N/A')
|
41
|
+
obj.updated_at.present? ? local_time(obj.updated_at, format, default) : local_time(created_at, format, default)
|
42
|
+
rescue StandardError
|
43
|
+
default
|
44
|
+
end
|
45
|
+
|
46
|
+
def default_time_zone
|
47
|
+
self.time_zone ||= SystemConfiguration.default_time_zone
|
48
|
+
end
|
49
|
+
end
|
@@ -97,9 +97,9 @@ class Notification
|
|
97
97
|
#
|
98
98
|
# Finish processing the notification successfully
|
99
99
|
#
|
100
|
-
def finish_processing(
|
101
|
-
if
|
102
|
-
set state: STATE_INVALID, error_message:
|
100
|
+
def finish_processing(processing_message = nil)
|
101
|
+
if processing_message.present?
|
102
|
+
set state: STATE_INVALID, error_message: processing_message
|
103
103
|
else
|
104
104
|
set state: STATE_PROCESSED, error_message: ''
|
105
105
|
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
# Easily fetch the redis configuration from redis.yml in the config directory.
|
5
|
+
# There are several formats to support
|
6
|
+
#
|
7
|
+
# URL with single server
|
8
|
+
# development:
|
9
|
+
# url: 'redis://localhost:6379/0'
|
10
|
+
#
|
11
|
+
# Host/port combination for single server
|
12
|
+
# development:
|
13
|
+
# host: localhost
|
14
|
+
# port: 6379
|
15
|
+
# db: 0
|
16
|
+
#
|
17
|
+
# Sentinel
|
18
|
+
# development:
|
19
|
+
# master: production
|
20
|
+
# sentinels:
|
21
|
+
# - host1:6379
|
22
|
+
# - host2:6379
|
23
|
+
# - host3:6379
|
24
|
+
# role: master
|
25
|
+
#
|
26
|
+
# Available for all options
|
27
|
+
# connect_timeout: 0.2
|
28
|
+
# read_timeout: 0.2
|
29
|
+
# write_timeout: 0.2
|
30
|
+
# timeout: 1
|
31
|
+
#
|
32
|
+
class RedisConfiguration
|
33
|
+
#
|
34
|
+
# Methods are static
|
35
|
+
#
|
36
|
+
class << self
|
37
|
+
#
|
38
|
+
# Load the configuration using the given DB
|
39
|
+
#
|
40
|
+
# Trying first the URL, then host, then sentinel, then default method
|
41
|
+
#
|
42
|
+
def load(database = nil)
|
43
|
+
load_url(database) || load_host(database) || load_sentinel(database) || load_default(database)
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
#
|
49
|
+
# Load the base set of parameters from the config file
|
50
|
+
#
|
51
|
+
def load_base(raw_config)
|
52
|
+
base = {}
|
53
|
+
%i[connect_timeout namespace read_timeout write_timeout timeout].each do |key|
|
54
|
+
base[key] = raw_config[key.to_s] unless raw_config[key.to_s].nil?
|
55
|
+
end
|
56
|
+
base
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Load a default configuration with the given database
|
61
|
+
#
|
62
|
+
def load_default(database = 0)
|
63
|
+
{ host: '127.0.0.1', port: 6_379, db: (database || 0) }
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Load the url
|
68
|
+
#
|
69
|
+
def load_url(database = 0)
|
70
|
+
raw_config = load_file
|
71
|
+
return if raw_config.nil? || raw_config['url'].nil?
|
72
|
+
|
73
|
+
config = load_base(raw_config)
|
74
|
+
config[:url] = raw_config['url']
|
75
|
+
config[:db] = (database || 0)
|
76
|
+
config
|
77
|
+
end
|
78
|
+
|
79
|
+
#
|
80
|
+
# Load the given host
|
81
|
+
#
|
82
|
+
def load_host(database = nil)
|
83
|
+
raw_config = load_file
|
84
|
+
return if raw_config.nil? || raw_config['host'].nil?
|
85
|
+
|
86
|
+
config = load_base(raw_config)
|
87
|
+
config[:host] = raw_config['host']
|
88
|
+
config[:port] = raw_config['port'] || 6_379
|
89
|
+
config[:db] = database || raw_config['db'] || 0
|
90
|
+
config
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Load the sentinel configuration
|
95
|
+
#
|
96
|
+
def load_sentinel(database = 0)
|
97
|
+
raw_config = load_file
|
98
|
+
return if raw_config.nil? || raw_config['master'].nil?
|
99
|
+
|
100
|
+
config = load_base(raw_config)
|
101
|
+
config[:url] = "redis://#{raw_config['master']}"
|
102
|
+
config[:sentinels] = raw_config['sentinels'].collect do |sentinel|
|
103
|
+
parts = sentinel.split(':')
|
104
|
+
host = parts.first
|
105
|
+
port = parts.count.eql?(2) ? parts.last : 26_379
|
106
|
+
{ host: host, port: port }
|
107
|
+
end
|
108
|
+
config[:db] = database || 0
|
109
|
+
config[:role] = raw_config['role'] || 'master'
|
110
|
+
config
|
111
|
+
end
|
112
|
+
|
113
|
+
#
|
114
|
+
# Return the config file
|
115
|
+
#
|
116
|
+
def config_file_path
|
117
|
+
File.expand_path('config/redis.yml')
|
118
|
+
end
|
119
|
+
|
120
|
+
#
|
121
|
+
# Load the config/redis.yml and return the environment name
|
122
|
+
#
|
123
|
+
def load_file
|
124
|
+
config = nil
|
125
|
+
file_path = config_file_path
|
126
|
+
if File.exist? file_path
|
127
|
+
yaml_data = YAML.load_file(file_path)
|
128
|
+
config = yaml_data[Rails.env] if yaml_data
|
129
|
+
end
|
130
|
+
|
131
|
+
config
|
132
|
+
rescue StandardError => error
|
133
|
+
puts "Error loading #{config_file_path} file", error
|
134
|
+
nil # return nothing if there is an error
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -31,38 +31,24 @@ class SlackNotification < Notification
|
|
31
31
|
# The slack username to use
|
32
32
|
field :from, type: String
|
33
33
|
|
34
|
-
def deliver_message
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
payload[:username] = from.presence || Rails.env
|
48
|
-
# Setup the delivery method for this message only.
|
49
|
-
RestClient.post(SystemConfiguration.slack_api_url, payload.to_json)
|
50
|
-
finish_processing
|
34
|
+
def deliver_message
|
35
|
+
if SystemConfiguration.slack_configured?
|
36
|
+
start_processing
|
37
|
+
payload = { text: message }
|
38
|
+
payload[:channel] = to.presence || SystemConfiguration.slack_support_channel
|
39
|
+
# Use the environment as the default, otherwise set it as the from
|
40
|
+
payload[:username] = from.presence || Rails.env
|
41
|
+
# Setup the delivery method for this message only.
|
42
|
+
RestClient.post(SystemConfiguration.slack_api_url, payload.to_json)
|
43
|
+
finish_processing
|
44
|
+
else
|
45
|
+
finish_processing 'Slack is not configured'
|
46
|
+
end
|
51
47
|
rescue StandardError => error
|
52
|
-
|
53
|
-
App47Logger.log_debug error.message
|
54
|
-
App47Logger.log_debug error.backtrace
|
55
|
-
App47Logger.log_debug '!!! Error sending SLACK notification !!!'
|
48
|
+
log_warn '!!! Error sending SLACK notification !!!', error
|
56
49
|
finish_processing error.message
|
57
50
|
end
|
58
51
|
|
59
|
-
#
|
60
|
-
# Limit the number of times the slack notification is retried
|
61
|
-
#
|
62
|
-
def max_retries
|
63
|
-
1
|
64
|
-
end
|
65
|
-
|
66
52
|
#
|
67
53
|
# Default delivery channel is email, override for sms, SMTP or other channels
|
68
54
|
#
|
@@ -74,8 +60,6 @@ class SlackNotification < Notification
|
|
74
60
|
# Convenience method to say something when we see something
|
75
61
|
#
|
76
62
|
def self.say(message, to: nil, from: nil, template: nil)
|
77
|
-
return unless SystemConfiguration.slack_configured?
|
78
|
-
|
79
63
|
notification = SlackNotification.new(to: to, from: from)
|
80
64
|
notification.message = if template.present?
|
81
65
|
notification.message_from_template template, message
|
@@ -26,9 +26,7 @@ class SmsNotification < Notification
|
|
26
26
|
#
|
27
27
|
field :sid, type: String
|
28
28
|
|
29
|
-
|
30
|
-
validates_presence_of :to
|
31
|
-
validates_presence_of :account_id
|
29
|
+
validates :to, presence: true, format: { with: /\A\+[1-9]{1}[0-9]{3,14}\z/ }
|
32
30
|
|
33
31
|
def deliver_message!
|
34
32
|
return unless SystemConfiguration.twilio_configured?
|
@@ -38,13 +36,13 @@ class SmsNotification < Notification
|
|
38
36
|
auth_token = config.twilio_auth_token
|
39
37
|
client = Twilio::REST::Client.new account_sid, auth_token
|
40
38
|
|
41
|
-
|
42
|
-
body:
|
43
|
-
to:
|
39
|
+
twilio_message = client.account.messages.create(
|
40
|
+
body: message,
|
41
|
+
to: to,
|
44
42
|
from: config.twilio_phone_number
|
45
43
|
)
|
46
44
|
# We are saved in the calling class, no reason to save again
|
47
|
-
set sid:
|
45
|
+
set sid: twilio_message.sid
|
48
46
|
end
|
49
47
|
|
50
48
|
#
|
@@ -28,7 +28,6 @@ class SmtpConfiguration
|
|
28
28
|
# Callbacks
|
29
29
|
#
|
30
30
|
before_save :update_token
|
31
|
-
#after_save :send_smtp_verification
|
32
31
|
|
33
32
|
#
|
34
33
|
# If we can use this SMTP configuration or not, it must be confirmed and
|
@@ -38,111 +37,37 @@ class SmtpConfiguration
|
|
38
37
|
confirmed? && active?
|
39
38
|
end
|
40
39
|
|
40
|
+
#
|
41
|
+
# Which fields to protect
|
42
|
+
#
|
41
43
|
def secure_fields
|
42
44
|
super + %i[password]
|
43
45
|
end
|
44
46
|
|
47
|
+
#
|
48
|
+
# Validate the token, returning true of false if valid
|
49
|
+
#
|
45
50
|
def validate_token(token)
|
46
|
-
valid =
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
end
|
53
|
-
end
|
51
|
+
valid = if confirmation_token.present? && confirmation_token.eql?(token)
|
52
|
+
unset(:confirmation_token, :verification_message)
|
53
|
+
true
|
54
|
+
else
|
55
|
+
false
|
56
|
+
end
|
54
57
|
set(confirmed: valid)
|
55
58
|
valid
|
56
59
|
end
|
57
60
|
|
58
61
|
#
|
59
62
|
# Update the token on save if we are active and the token is not already set.
|
63
|
+
#
|
60
64
|
def update_token
|
61
65
|
if active?
|
62
|
-
set(confirmation_token:
|
66
|
+
set(confirmation_token: Digest::SHA256.hexdigest("#{id}_#{account.id}")) if confirmation_token.nil?
|
63
67
|
set(confirmed: false)
|
64
68
|
set(verification_message: 'Sending SMTP verification email(s) to SMTP admins.')
|
65
69
|
else
|
66
70
|
set(verification_message: 'SMTP Configuration is not active, no verification email will be sent.')
|
67
71
|
end
|
68
72
|
end
|
69
|
-
|
70
|
-
def send_smtp_verification
|
71
|
-
if self.active?
|
72
|
-
sent_to= []
|
73
|
-
self.account.smtp_admins.each do |admin|
|
74
|
-
sent_to << admin.email if self.send_confirmation(admin)
|
75
|
-
end
|
76
|
-
if sent_to.empty?
|
77
|
-
set(verification_message: "No confirmations emails sent: (#{verification_message}).")
|
78
|
-
else
|
79
|
-
set(verification_message: "Confirmation emails sent to SMTP admins: #{sent_to.join(", ")}")
|
80
|
-
end
|
81
|
-
end
|
82
|
-
end
|
83
|
-
|
84
|
-
def send_confirmation(member)
|
85
|
-
sent = false
|
86
|
-
begin
|
87
|
-
mail = Mail.new
|
88
|
-
# Set the from line
|
89
|
-
mail.from = email_address
|
90
|
-
|
91
|
-
# Set the to address
|
92
|
-
mail.to = member.email
|
93
|
-
|
94
|
-
# Set the subject line
|
95
|
-
mail.subject = 'Verify SMTP configuration change'
|
96
|
-
|
97
|
-
# set the message body and send
|
98
|
-
html_message = build_message
|
99
|
-
mail.html_part do
|
100
|
-
content_type 'text/html; charset=UTF-8'
|
101
|
-
body html_message
|
102
|
-
end
|
103
|
-
|
104
|
-
# Setup the delivery method for this message only.
|
105
|
-
if Rails.env.test?
|
106
|
-
mail.delivery_method :test
|
107
|
-
else
|
108
|
-
config = { address: self.server_name,
|
109
|
-
port: self.port,
|
110
|
-
authentication: self.authentication_method.to_sym,
|
111
|
-
enable_starttls_auto: self.ssl.eql?(true) }
|
112
|
-
config[:domain] = self.domain unless self.domain.nil? or self.domain.empty?
|
113
|
-
config[:user_name] = self.username unless self.username.nil? or self.username.empty?
|
114
|
-
config[:password] = self.password unless self.password.nil? or self.password.empty?
|
115
|
-
|
116
|
-
mail.delivery_method :smtp, config
|
117
|
-
end
|
118
|
-
|
119
|
-
# Deliver it
|
120
|
-
mail.deliver
|
121
|
-
sent = true
|
122
|
-
|
123
|
-
rescue Exception=>e
|
124
|
-
App47::Logger.log_error "Unable to send SMTP confirmation email #{e.message}"
|
125
|
-
App47::Logger.log_error e.backtrace
|
126
|
-
set(verification_message: "Unable to send verification email to #{member.email}, error: #{e.message}")
|
127
|
-
end
|
128
|
-
sent
|
129
|
-
end
|
130
|
-
|
131
|
-
def build_message
|
132
|
-
template = File.read(Rails.root.join('app', 'assets', 'templates', 'email', "verify_smtp_configuration.liquid"))
|
133
|
-
engine = Liquid::Template.parse(template)
|
134
|
-
engine.render({
|
135
|
-
'account_name' => account.name,
|
136
|
-
'confirmation_url' => "#{SystemConfiguration.webui_url}/smtp_configuration/confirm_change?token=#{confirmation_token}"
|
137
|
-
})
|
138
|
-
end
|
139
|
-
|
140
|
-
def to_yaml_properties(wrap = true)
|
141
|
-
if wrap
|
142
|
-
self.class.new.to_yaml_properties(false)
|
143
|
-
else
|
144
|
-
super()
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
73
|
end
|
data/lib/web47core.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
require 'app/models/concerns/app47_logger'
|
2
2
|
require 'app/models/concerns/cdn_url'
|
3
|
+
require 'app/models/concerns/email_able'
|
4
|
+
require 'app/models/concerns/search_able'
|
5
|
+
require 'app/models/concerns/time_zone_able'
|
3
6
|
require 'app/models/concerns/standard_model'
|
4
7
|
require 'app/models/concerns/core_system_configuration'
|
5
8
|
require 'app/models/concerns/core_account'
|
9
|
+
require 'app/models/redis_configuration'
|
6
10
|
require 'app/models/notification'
|
7
11
|
require 'app/models/template'
|
8
12
|
require 'app/models/notification_template'
|