web47core 2.0.1 → 2.2.15

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +111 -41
  3. data/app/controllers/status_controller.rb +8 -3
  4. data/app/helpers/core_form_helper.rb +6 -6
  5. data/app/helpers/core_helper.rb +1 -1
  6. data/app/helpers/core_link_helper.rb +47 -8
  7. data/app/helpers/core_nav_bar_helper.rb +5 -4
  8. data/app/helpers/core_table_helper.rb +80 -0
  9. data/app/helpers/model_modal_helper.rb +3 -3
  10. data/app/views/common/_create_actions.html.haml +12 -0
  11. data/app/views/common/_update_actions.html.haml +10 -0
  12. data/app/views/cron/_edit.html.haml +15 -17
  13. data/app/views/cron/_index.html.haml +74 -67
  14. data/app/views/delayed_job_metrics/_index.html.haml +27 -0
  15. data/app/views/delayed_job_metrics/index.html.haml +1 -0
  16. data/app/views/delayed_job_workers/_index.html.haml +27 -0
  17. data/app/views/delayed_job_workers/index.html.haml +1 -0
  18. data/app/views/delayed_jobs/_index.html.haml +47 -52
  19. data/app/views/delayed_jobs/_show.html.haml +15 -13
  20. data/app/views/system_configurations/_edit.html.haml +14 -9
  21. data/app/views/system_configurations/_show.html.haml +18 -12
  22. data/config/brakeman.ignore +26 -0
  23. data/config/brakeman.yml +2 -0
  24. data/config/locales/en.yml +21 -3
  25. data/lib/app/controllers/concerns/core_delayed_job_metrics_controller.rb +35 -0
  26. data/lib/app/controllers/concerns/core_delayed_job_workers_controller.rb +35 -0
  27. data/lib/app/controllers/concerns/core_delayed_jobs_controller.rb +2 -3
  28. data/lib/app/jobs/cron/command.rb +1 -4
  29. data/lib/app/jobs/cron/record_delayed_job_metrics.rb +25 -0
  30. data/lib/app/jobs/cron/restart_orphaned_delayed_jobs.rb +44 -0
  31. data/lib/app/jobs/cron/server.rb +32 -15
  32. data/lib/app/jobs/cron/switchboard_sync_configuration.rb +2 -0
  33. data/lib/app/jobs/cron/switchboard_sync_models.rb +2 -0
  34. data/lib/app/jobs/cron/trim_collection.rb +1 -1
  35. data/lib/app/jobs/cron/trim_delayed_job_metrics.rb +29 -0
  36. data/lib/app/jobs/cron/trim_delayed_job_workers.rb +39 -0
  37. data/lib/app/jobs/cron/trim_failed_delayed_jobs.rb +2 -0
  38. data/lib/app/models/api_token.rb +9 -0
  39. data/lib/app/models/command_job.rb +12 -11
  40. data/lib/app/models/concerns/api_tokenable.rb +38 -0
  41. data/lib/app/models/concerns/aws_configuration.rb +65 -0
  42. data/lib/app/models/concerns/cdn_url.rb +7 -0
  43. data/lib/app/models/concerns/core_smtp_configuration.rb +65 -0
  44. data/lib/app/models/concerns/core_system_configuration.rb +18 -201
  45. data/lib/app/models/concerns/delayed_job_configuration.rb +24 -0
  46. data/lib/app/models/concerns/email_able.rb +6 -2
  47. data/lib/app/models/concerns/google_sso_configuration.rb +32 -0
  48. data/lib/app/models/concerns/search_able.rb +16 -0
  49. data/lib/app/models/concerns/server_process_able.rb +69 -0
  50. data/lib/app/models/concerns/slack_configuration.rb +38 -0
  51. data/lib/app/models/concerns/standard_model.rb +5 -2
  52. data/lib/app/models/concerns/switchboard_configuration.rb +43 -0
  53. data/lib/app/models/concerns/twilio_configuration.rb +37 -0
  54. data/lib/app/models/concerns/zendesk_configuration.rb +92 -0
  55. data/lib/app/models/{delayed_job.rb → delayed/backend/delayed_job.rb} +41 -0
  56. data/lib/app/models/delayed/jobs/metric.rb +61 -0
  57. data/lib/app/models/delayed/jobs/run.rb +40 -0
  58. data/lib/app/models/delayed/jobs/worker.rb +43 -0
  59. data/lib/app/models/delayed/plugins/time_keeper.rb +33 -0
  60. data/lib/app/models/delayed/worker.rb +24 -0
  61. data/lib/app/models/email_notification.rb +2 -1
  62. data/lib/app/models/email_template.rb +5 -6
  63. data/lib/app/models/notification.rb +12 -2
  64. data/lib/app/models/sms_notification.rb +9 -6
  65. data/lib/app/models/template.rb +12 -12
  66. data/lib/web47core/version.rb +1 -1
  67. data/lib/web47core.rb +33 -9
  68. metadata +114 -69
@@ -1,22 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ #
4
+ # The System configuration. Various configuration items that can be updated/defined at run time
5
+ #
6
+ # Use of this class allows you to simply ask for the configuration parameter directly without
7
+ # first having to get an instance of it.
8
+ #
9
+ # SystemConfiguration.queue_impl #=> 'RedisQueue'
10
+ #
11
+ # This method only is allowed for accessors, you should NEVER set values on the SystemConfiguration
12
+ # unless you are updating via the Admin or Stack UI, or during testing to setup a specific configuration
13
+ # for that.
14
+ #
3
15
  module CoreSystemConfiguration
4
16
  extend ActiveSupport::Concern
5
- #
6
- # The System configuration. Various configuration items that can be updated/defined at run time
7
- #
8
- # Use of this class allows you to simply ask for the configuration parameter directly without
9
- # first having to get an instance of it.
10
- #
11
- # SystemConfiguration.queue_impl #=> 'RedisQueue'
12
- #
13
- # This method only is allowed for accessors, you should NEVER set values on the SystemConfiguration
14
- # unless you are updating via the Admin or Stack UI, or during testing to setup a specific configuration
15
- # for that.
16
- #
17
+
17
18
  def self.included(base)
18
19
  base.class_eval do
19
20
  attr_accessor :configuration
21
+
20
22
  #
21
23
  # Fields
22
24
  #
@@ -28,79 +30,31 @@ module CoreSystemConfiguration
28
30
  field :short_cache, { type: Integer, default: 1 }
29
31
  field :medium_cache, { type: Integer, default: 5 }
30
32
  field :long_cache, { type: Integer, default: 15 }
31
- # SMTP configuration
32
- field :default_email, type: String, default: 'support@app47.com'
33
- field :support_email, type: String, default: 'support@app47.com'
34
- field :smtp_name, type: String
35
- field :smtp_address, type: String
36
- field :smtp_domain, type: String
37
- field :smtp_port, type: Integer, default: 587
38
- field :smtp_user_name, type: String
39
- field :smtp_password, type: String
40
- field :smtp_enable_starttls_auto, type: Boolean
41
- field :mailgun_api_key, type: String
42
- # Twillio support
43
- field :twilio_account_id, type: String
44
- field :twilio_auth_token, type: String
45
- field :twilio_phone_number, type: String
46
33
  # URLs
47
34
  field :base_url, type: String
48
35
  field :cdn_url, type: String
49
- # Slack support
50
- field :slack_api_url, type: String
51
- field :slack_support_channel, type: String, default: 'support'
52
- field :slack_sales_channel, type: String, default: 'sales'
36
+ field :asset_cdn_url, type: String
53
37
  # Time Zone Support
54
38
  field :default_time_zone, type: String, default: 'US/Eastern'
55
- # AWS
56
- field :aws_region, type: String
57
- field :aws_access_key_id, type: String
58
- field :aws_secret_access_key, type: String
59
- field :aws_auto_scaling_group_name, type: String
60
- # Zendesk
61
- field :zendesk_token, type: String
62
- field :zendesk_base_url, type: String, default: 'https://app47.zendesk.com'
63
- field :zendesk_documentation_path, type: String, default: 'hc'
64
- field :zendesk_support_path, type: String, default: 'hc/en-us/requests'
65
- field :zendesk_updates_path, type: String, default: 'hc'
66
- # Switchboard
67
- field :switchboard_base_url, type: String, default: 'https://switchboard.app47.com'
68
- field :switchboard_stack_id, type: String
69
- field :switchboard_stack_api_token, type: String
70
- field :switchboard_last_sync_at, type: Time
71
39
  # TTLs
72
40
  field :user_model_audit_log_ttl, type: Integer, default: 365
73
- field :slack_notification_ttl, type: Integer, default: 5
74
- field :email_notification_ttl, type: Integer, default: 180
75
41
  #
76
42
  # Validations
77
43
  #
78
44
  validates :environment, presence: true, uniqueness: true
79
- validates :slack_support_channel, presence: true
80
- validates :slack_sales_channel, presence: true
81
45
  validates :default_time_zone, presence: true
82
- validates :switchboard_base_url, url: true
83
- validates :zendesk_base_url, url: true
84
46
  end
85
47
  base.extend ClassMethods
86
48
  end
87
49
 
50
+ #
51
+ # Class methods for SystemConfiguration
52
+ #
88
53
  module ClassMethods
89
54
  def configuration
90
55
  SystemConfiguration.find_or_create_by!(environment: Rails.env)
91
56
  end
92
57
 
93
- def smtp_configuration
94
- output = {}
95
- config = configuration
96
- fields = %w[name address domain port user_name password enable_starttls_auto]
97
- fields.each do |field|
98
- field_name = "smtp_#{field}".to_sym
99
- output[field.to_sym] = config.send(field_name)
100
- end
101
- output
102
- end
103
-
104
58
  #
105
59
  # NOTE: Currently ignored Codacy issue: When using 'method_missing', fall back on 'super'
106
60
  #
@@ -121,18 +75,6 @@ module CoreSystemConfiguration
121
75
  end
122
76
  end
123
77
 
124
- #
125
- # Make sure the password doesn't get blanked out on an update
126
- #
127
- def secure_fields
128
- super + %i[smtp_password
129
- aws_access_secret
130
- mailgun_api_key
131
- switchboard_stack_api_token
132
- twilio_auth_token
133
- zendesk_token]
134
- end
135
-
136
78
  #
137
79
  # Cache times in minutes
138
80
  #
@@ -147,129 +89,4 @@ module CoreSystemConfiguration
147
89
  def long_cache_time
148
90
  long_cache.minutes
149
91
  end
150
-
151
- #
152
- # Determine if SMTP is configured
153
- #
154
- def smtp_configured?
155
- smtp_name.present? && smtp_address.present? && smtp_domain.present?
156
- end
157
-
158
- #
159
- # Determine if mailgun is configured
160
- #
161
- def mail_gun_configured?
162
- smtp_configured? && mailgun_api_key.present?
163
- end
164
-
165
- #
166
- # Determine if AWS is configured
167
- #
168
- def aws_configured?
169
- [aws_region.present?, aws_access_key_id.present?, aws_secret_access_key.present?].all?
170
- end
171
-
172
- #
173
- # Determine if auto scaling group is configured
174
- #
175
- def aws_auto_scaling_configured?
176
- aws_configured? && aws_auto_scaling_group_name.present?
177
- end
178
-
179
- #
180
- # Return the zendesk documentation URL
181
- #
182
- def zendesk_documentation_url(user = nil)
183
- zendesk_url(forward_to: zendesk_documentation_path, user: user)
184
- end
185
-
186
- #
187
- # Return the zendesk support URL
188
- #
189
- def zendesk_requests_url(user = nil)
190
- zendesk_url(forward_to: zendesk_support_path, user: user)
191
- end
192
-
193
- #
194
- # Return the zendesk support URL
195
- #
196
- def zendesk_new_request_url(user = nil)
197
- zendesk_url(forward_to: "#{zendesk_support_path}/new", user: user)
198
- end
199
-
200
- #
201
- # Return the zendesk update URL
202
- #
203
- def zendesk_updates_url(user = nil)
204
- zendesk_url(forward_to: zendesk_updates_path, user: user)
205
- end
206
-
207
- #
208
- # Generate a Zendesk URL
209
- #
210
- # If a user is passed in and Zendesk is configured then return a JWT enabled URL for
211
- # SSO authentication to Zendesk.
212
- #
213
- def zendesk_url(forward_to: zendesk_documentation_path, user: nil)
214
- config = SystemConfiguration.configuration
215
- path = if config.zendesk_configured? && user.present?
216
- time_now = Time.now.to_i
217
- jti = "#{time_now}/#{rand(36 ** 64).to_s(36)}"
218
- payload = { jwt: JWT.encode({ iat: time_now, # Seconds since epoch, determine when this token is stale
219
- jti: jti, # Unique token identifier, helps prevent replay attacks
220
- name: user.name,
221
- email: user.email }, config.zendesk_token),
222
- return_to: CGI.escape([config.zendesk_base_url, forward_to].join('/')) }
223
- ['access/jwt', payload.to_query].join('?')
224
- else
225
- forward_to
226
- end
227
- [config.zendesk_base_url, path].join('/')
228
- end
229
-
230
- #
231
- # Is zendesk configured?
232
- #
233
- def zendesk_configured?
234
- [zendesk_token.present?,
235
- zendesk_base_url.present?,
236
- zendesk_documentation_path.present?,
237
- zendesk_support_path.present?].all?
238
- end
239
-
240
- #
241
- # Public: Determine if switchboard is configured
242
- #
243
- # Examples
244
- #
245
- # switchboard_configured?
246
- # # => true || false
247
- #
248
- def switchboard_configured?
249
- [switchboard_base_url.present?, switchboard_stack_api_token.present?, switchboard_stack_id.present?].all?
250
- end
251
-
252
- #
253
- # Determine if twillio is configured at a system configuration
254
- #
255
- # Examples
256
- #
257
- # switchboard_configured?
258
- # # => true || false
259
- #
260
- def twilio_configured?
261
- [twilio_account_id.present?, twilio_auth_token.present?, twilio_phone_number.present?].all?
262
- end
263
-
264
- #
265
- # Determine if Slack is configured
266
- #
267
- # Examples
268
- #
269
- # switchboard_configured?
270
- # # => true || false
271
- #
272
- def slack_configured?
273
- slack_api_url.present?
274
- end
275
92
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # The Slack Configuration to be used inside of SystemConfiguration
5
+ #
6
+ module DelayedJobConfiguration
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ #
12
+ # Fields
13
+ #
14
+ field :delayed_job_max_allowed_method, type: String, default: 'max'
15
+ field :delayed_job_max_allowed_factor, type: Integer, default: 5
16
+ field :delayed_job_restart_orphaned, type: Boolean, default: false
17
+ #
18
+ # Validations
19
+ #
20
+ validates :delayed_job_max_allowed_method, inclusion: { in: %w[max min avg] }
21
+ validates :delayed_job_max_allowed_factor, numericality: { greater_than: 0 }
22
+ end
23
+ end
24
+ end
@@ -61,7 +61,7 @@ module EmailAble
61
61
  #
62
62
  # Reset the bounced email
63
63
  #
64
- def reset_bounce_status
64
+ def reset_bounce_status(current_member = nil)
65
65
  return unless SystemConfiguration.mail_gun_configured? && email_bounced?
66
66
 
67
67
  reset_url = "https://api.mailgun.net/v3/#{SystemConfiguration.smtp_domain}/bounces/#{CGI.escape(email)}"
@@ -69,7 +69,11 @@ module EmailAble
69
69
  rescue RestClient::Exception => error
70
70
  log_error "Unable to reset email bounce status: #{inspect}", error
71
71
  ensure
72
- set email_bounced_at: nil, email_bounce_reason: nil
72
+ if current_member.present?
73
+ update_and_log current_member, email_bounced_at: nil, email_bounce_reason: nil
74
+ else
75
+ set email_bounced_at: nil, email_bounce_reason: nil
76
+ end
73
77
  end
74
78
 
75
79
  #
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Google SSO Configuration
5
+ #
6
+ module GoogleSsoConfiguration
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ #
12
+ # Fields
13
+ #
14
+ field :google_client_id, type: String
15
+ field :google_client_secret, type: String
16
+ end
17
+ end
18
+
19
+ #
20
+ # Make sure the password doesn't get blanked out on an update
21
+ #
22
+ def secure_fields
23
+ super + %i[google_client_secret]
24
+ end
25
+
26
+ #
27
+ # Determine if AWS is configured
28
+ #
29
+ def google_sso_configured?
30
+ google_client_id.present? && google_client_secret.present?
31
+ end
32
+ end
@@ -39,6 +39,22 @@ module SearchAble
39
39
  end
40
40
  end
41
41
 
42
+ def method_missing(method, *args)
43
+ if method.to_s.start_with? 'sorted_'
44
+ send(method.to_s.sub(/^sorted_/, '')).asc(:sort_text)
45
+ else
46
+ super
47
+ end
48
+ end
49
+
50
+ def respond_to_missing?(method_name, include_private = false)
51
+ super || method_name.to_s.start_with?('sorted_')
52
+ end
53
+
54
+ def respond_to?(method, include_private = false)
55
+ super || method.to_s.start_with?('sorted_')
56
+ end
57
+
42
58
  #
43
59
  # Place holder to call to allow for work to be done before we gather up search text fields
44
60
  #
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Mixin for objects that act as servers
5
+ #
6
+ module ServerProcessAble
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ #
12
+ # Fields
13
+ #
14
+ field :host_name, type: String
15
+ field :pid, type: Integer
16
+ field :running, type: Boolean, default: false
17
+ field :last_check_in_at, type: Time
18
+ #
19
+ # Validations
20
+ #
21
+ validates :host_name, presence: true
22
+ validates :pid, presence: true
23
+ end
24
+ base.extend ClassMethods
25
+ end
26
+
27
+ #
28
+ # Methods for the concrete class this concern is attached too, ways to find or create or just find the
29
+ # associated server.
30
+ #
31
+ module ClassMethods
32
+ #
33
+ # Find a record for this server
34
+ #
35
+ def find_or_create_server
36
+ find_or_create_by!(host_name: Socket.gethostname, pid: Process.pid)
37
+ end
38
+
39
+ #
40
+ # Find a worker, return nil if not found
41
+ #
42
+ def find_server
43
+ find_by(host_name: Socket.gethostname, pid: Process.pid)
44
+ rescue StandardError
45
+ nil
46
+ end
47
+ end
48
+
49
+ #
50
+ # Perform a check in for the server
51
+ #
52
+ def check_in
53
+ set(last_check_in_at: Time.now.utc)
54
+ end
55
+
56
+ #
57
+ # Stop the worker
58
+ #
59
+ def stop
60
+ set(running: false, last_check_in_at: Time.now.utc)
61
+ end
62
+
63
+ #
64
+ # Start the worker
65
+ #
66
+ def start
67
+ set(running: true, last_check_in_at: Time.now.utc)
68
+ end
69
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # The Slack Configuration to be used inside of SystemConfiguration
5
+ #
6
+ module SlackConfiguration
7
+ extend ActiveSupport::Concern
8
+
9
+ def self.included(base)
10
+ base.class_eval do
11
+ #
12
+ # Fields
13
+ #
14
+ field :slack_api_url, type: String
15
+ field :slack_support_channel, type: String, default: 'support'
16
+ field :slack_sales_channel, type: String, default: 'sales'
17
+ field :slack_notification_ttl, type: Integer, default: 5
18
+ #
19
+ # Validations
20
+ #
21
+ validates :slack_api_url, url: true, allow_blank: true
22
+ validates :slack_support_channel, presence: true
23
+ validates :slack_sales_channel, presence: true
24
+ end
25
+ end
26
+
27
+ #
28
+ # Determine if Slack is configured
29
+ #
30
+ # Examples
31
+ #
32
+ # slack_configured??
33
+ # # => true || false
34
+ #
35
+ def slack_configured?
36
+ [slack_api_url.present?, slack_support_channel.present?, slack_sales_channel.present?].all?
37
+ end
38
+ end
@@ -42,9 +42,12 @@ module StandardModel
42
42
  # Used by calling 'model.without_callback(*.args, &block) do'
43
43
  def without_callback(*args, &_block)
44
44
  skip_callback(*args)
45
- yield
46
- ensure
45
+ block_result = yield
47
46
  set_callback(*args)
47
+ block_result
48
+ rescue StandardError
49
+ set_callback(*args)
50
+ nil
48
51
  end
49
52
 
50
53
  #
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Switchboard configuration, both the fields and methods needed to determine configuration
5
+ #
6
+ module SwitchboardConfiguration
7
+ extend ActiveSupport::Concern
8
+ #
9
+ # Switchboard configuration
10
+ #
11
+ def self.included(base)
12
+ base.class_eval do
13
+ # Switchboard
14
+ field :switchboard_base_url, type: String, default: 'https://switchboard.app47.com'
15
+ field :switchboard_stack_id, type: String
16
+ field :switchboard_stack_api_token, type: String
17
+ field :switchboard_last_sync_at, type: Time
18
+ #
19
+ # Validations
20
+ #
21
+ validates :switchboard_base_url, url: true
22
+ end
23
+ end
24
+
25
+ #
26
+ # Make sure the password doesn't get blanked out on an update
27
+ #
28
+ def secure_fields
29
+ super + %i[switchboard_stack_api_token]
30
+ end
31
+
32
+ #
33
+ # Public: Determine if switchboard is configured
34
+ #
35
+ # Examples
36
+ #
37
+ # switchboard_configured?
38
+ # # => true || false
39
+ #
40
+ def switchboard_configured?
41
+ [switchboard_base_url.present?, switchboard_stack_api_token.present?, switchboard_stack_id.present?].all?
42
+ end
43
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Twilio configuration, both fields and methods to determine configuration
5
+ #
6
+ module TwilioConfiguration
7
+ extend ActiveSupport::Concern
8
+ #
9
+ # The Twilio Configuration
10
+ #
11
+ def self.included(base)
12
+ base.class_eval do
13
+ field :twilio_account_id, type: String
14
+ field :twilio_auth_token, type: String
15
+ field :twilio_phone_number, type: String
16
+ end
17
+ end
18
+
19
+ #
20
+ # Make sure the password doesn't get blanked out on an update
21
+ #
22
+ def secure_fields
23
+ super + %i[twilio_auth_token]
24
+ end
25
+
26
+ #
27
+ # Determine if twilio is configured at a system configuration
28
+ #
29
+ # Examples
30
+ #
31
+ # twilio_configured??
32
+ # # => true || false
33
+ #
34
+ def twilio_configured?
35
+ [twilio_account_id.present?, twilio_auth_token.present?, twilio_phone_number.present?].all?
36
+ end
37
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Add zendesk capability to an object to integrate via JWT with Zendesk. Most commonly this will be added
5
+ # to SystemConfiguration, however this can also be added to a different object (i.e., switchboard).
6
+ #
7
+ module ZendeskConfiguration
8
+ extend ActiveSupport::Concern
9
+ def self.included(base)
10
+ base.class_eval do
11
+ #
12
+ # Fields
13
+ #
14
+ field :zendesk_token, type: String
15
+ field :zendesk_base_url, type: String, default: 'https://app47.zendesk.com'
16
+ field :zendesk_documentation_path, type: String, default: 'hc'
17
+ field :zendesk_support_path, type: String, default: 'hc/en-us/requests'
18
+ field :zendesk_updates_path, type: String, default: 'hc'
19
+ #
20
+ # Validations
21
+ #
22
+ validates :zendesk_base_url, url: true
23
+ end
24
+ end
25
+
26
+ #
27
+ # Make sure the password doesn't get blanked out on an update
28
+ #
29
+ def secure_fields
30
+ super + %i[zendesk_token]
31
+ end
32
+
33
+ #
34
+ # Return the zendesk documentation URL
35
+ #
36
+ def zendesk_documentation_url(user = nil)
37
+ zendesk_url(forward_to: zendesk_documentation_path, user: user)
38
+ end
39
+
40
+ #
41
+ # Return the zendesk support URL
42
+ #
43
+ def zendesk_requests_url(user = nil)
44
+ zendesk_url(forward_to: zendesk_support_path, user: user)
45
+ end
46
+
47
+ #
48
+ # Return the zendesk support URL
49
+ #
50
+ def zendesk_new_request_url(user = nil)
51
+ zendesk_url(forward_to: "#{zendesk_support_path}/new", user: user)
52
+ end
53
+
54
+ #
55
+ # Return the zendesk update URL
56
+ #
57
+ def zendesk_updates_url(user = nil)
58
+ zendesk_url(forward_to: zendesk_updates_path, user: user)
59
+ end
60
+
61
+ #
62
+ # Generate a Zendesk URL
63
+ #
64
+ # If a user is passed in and Zendesk is configured then return a JWT enabled URL for
65
+ # SSO authentication to Zendesk.
66
+ #
67
+ def zendesk_url(forward_to: zendesk_documentation_path, user: nil)
68
+ path = if zendesk_configured? && user.present?
69
+ time_now = Time.now.to_i
70
+ jti = "#{time_now}/#{rand(36**64).to_s(36)}"
71
+ payload = { jwt: JWT.encode({ iat: time_now, # Seconds since epoch, determine when this token is stale
72
+ jti: jti, # Unique token identifier, helps prevent replay attacks
73
+ name: user.name,
74
+ email: user.email }, zendesk_token),
75
+ return_to: CGI.escape([zendesk_base_url, forward_to].join('/')) }
76
+ ['access/jwt', payload.to_query].join('?')
77
+ else
78
+ forward_to
79
+ end
80
+ [zendesk_base_url, path].join('/')
81
+ end
82
+
83
+ #
84
+ # Is zendesk configured?
85
+ #
86
+ def zendesk_configured?
87
+ [zendesk_token.present?,
88
+ zendesk_base_url.present?,
89
+ zendesk_documentation_path.present?,
90
+ zendesk_support_path.present?].all?
91
+ end
92
+ end