bullet_train 1.0.5 → 1.0.9

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/account/two_factors_controller.rb +18 -0
  3. data/app/controllers/concerns/account/controllers/base.rb +118 -0
  4. data/app/controllers/concerns/account/users/controller_base.rb +1 -1
  5. data/app/controllers/concerns/controllers/base.rb +119 -0
  6. data/app/controllers/concerns/devise_current_attributes.rb +13 -0
  7. data/app/controllers/concerns/registrations/controller_base.rb +39 -0
  8. data/app/controllers/concerns/sessions/controller_base.rb +15 -0
  9. data/app/controllers/registrations_controller.rb +9 -0
  10. data/app/controllers/sessions_controller.rb +3 -0
  11. data/app/helpers/account/buttons_helper.rb +12 -0
  12. data/app/helpers/account/dates_helper.rb +38 -0
  13. data/app/helpers/account/forms_helper.rb +67 -0
  14. data/app/helpers/account/locale_helper.rb +51 -0
  15. data/app/helpers/account/markdown_helper.rb +5 -0
  16. data/app/helpers/account/role_helper.rb +5 -0
  17. data/app/helpers/attributes_helper.rb +32 -0
  18. data/app/helpers/email_helper.rb +7 -0
  19. data/app/helpers/images_helper.rb +7 -0
  20. data/app/mailers/concerns/mailers/base.rb +16 -0
  21. data/app/mailers/devise_mailer.rb +10 -0
  22. data/app/mailers/user_mailer.rb +24 -0
  23. data/app/models/concerns/records/base.rb +60 -0
  24. data/app/views/account/two_factors/create.js.erb +1 -0
  25. data/app/views/account/two_factors/destroy.js.erb +1 -0
  26. data/app/views/devise/confirmations/new.html.erb +16 -0
  27. data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
  28. data/app/views/devise/mailer/password_change.html.erb +3 -0
  29. data/app/views/devise/mailer/reset_password_instructions.html.erb +30 -0
  30. data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
  31. data/app/views/devise/passwords/edit.html.erb +17 -0
  32. data/app/views/devise/passwords/new.html.erb +20 -0
  33. data/app/views/devise/registrations/_two_factor.html.erb +42 -0
  34. data/app/views/devise/registrations/edit.html.erb +43 -0
  35. data/app/views/devise/registrations/new.html.erb +30 -0
  36. data/app/views/devise/sessions/new.html.erb +59 -0
  37. data/app/views/devise/sessions/pre_otp.js.erb +11 -0
  38. data/app/views/devise/shared/_links.html.erb +21 -0
  39. data/app/views/devise/shared/_oauth.html.erb +9 -0
  40. data/app/views/devise/unlocks/new.html.erb +16 -0
  41. data/app/views/layouts/account.html.erb +1 -0
  42. data/app/views/layouts/devise.html.erb +1 -0
  43. data/config/locales/en/devise.en.yml +110 -0
  44. data/lib/bullet_train/version.rb +1 -1
  45. metadata +43 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 633b451afdfeee512883e8536928cc2dd247f15139d59e7bfd24cc4d637b735d
4
- data.tar.gz: c2a59912f00000e2b72ae5193b6d5cd0ebcd34a736f01260f5abfa1a5c170c94
3
+ metadata.gz: e043af38dcb40348528dd6b925433147b3e58f0fd64d969c7396feebce04091a
4
+ data.tar.gz: 4546e61efd71d6b92c270e02a50ff33e558d6007d4cc761c925192d77562459f
5
5
  SHA512:
6
- metadata.gz: 6cacd920c5c13800b167a1d1c3e9ae8fa6d6bc1cea28d1128f7404ce8bd9e0615957aad91b4cb7363bb10ac88823294511344bf9103febe2b0ed830508472cba
7
- data.tar.gz: 1bc0e30c37ebd765df530a9d25b65a591736be8420949c68651569347280d7403d3889cdd59933384825c19d5727a25c7d16569a42e9baececfeb2970938b85a
6
+ metadata.gz: 4f448f5cacf14583c92761252adff72ee3c7b8474b291e1ccb3f4b7874acf341a329fc5ee96be8d948d7394739abd5508fa78a813b0d922daae5b82d791eafa0
7
+ data.tar.gz: b0865e4751fd3819475fea1bab30e30e503f21ad75d884e69db241af605a6178519ea3bb6b4c617a84b75c8f2888eb94ba377ee762c6218a1a21830617323c5e
@@ -0,0 +1,18 @@
1
+ class Account::TwoFactorsController < Account::ApplicationController
2
+ before_action :authenticate_user!
3
+
4
+ def create
5
+ @backup_codes = current_user.generate_otp_backup_codes!
6
+ @user = current_user
7
+
8
+ current_user.update(
9
+ otp_secret: User.generate_otp_secret,
10
+ otp_required_for_login: true
11
+ )
12
+ end
13
+
14
+ def destroy
15
+ @user = current_user
16
+ current_user.update(otp_required_for_login: false)
17
+ end
18
+ end
@@ -0,0 +1,118 @@
1
+ module Account::Controllers::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ include LoadsAndAuthorizesResource
6
+ include Fields::ControllerSupport
7
+
8
+ before_action :set_last_seen_at, if: proc {
9
+ user_signed_in? && (current_user.last_seen_at.nil? || current_user.last_seen_at < 1.minute.ago)
10
+ }
11
+
12
+ layout "account"
13
+
14
+ before_action :authenticate_user!
15
+ before_action :set_paper_trail_whodunnit
16
+ before_action :ensure_onboarding_is_complete_and_set_next_step
17
+ end
18
+
19
+ class_methods do
20
+ # this is a template method called by LoadsAndAuthorizesResource.
21
+ # it allows that module to understand what namespaces of a controller
22
+ # are organizing the controllers, and which are organizing the models.
23
+ def regex_to_remove_controller_namespace
24
+ /^Account::/
25
+ end
26
+ end
27
+
28
+ def adding_user_email?
29
+ is_a?(Account::Onboarding::UserEmailController)
30
+ end
31
+
32
+ def adding_user_details?
33
+ is_a?(Account::Onboarding::UserDetailsController)
34
+ end
35
+
36
+ def adding_team?
37
+ return true if request.get? && request.path == new_account_team_path
38
+ return true if request.post? && request.path == account_teams_path
39
+ false
40
+ end
41
+
42
+ def switching_teams?
43
+ return true if request.get? && request.path == account_teams_path
44
+ false
45
+ end
46
+
47
+ def managing_account?
48
+ is_a?(Account::UsersController) || self.class.module_parents.include?(Oauth)
49
+ end
50
+
51
+ def accepting_invitation?
52
+ (params[:controller] == "account/invitations") && (params[:action] == "show" || params[:action] == "accept")
53
+ end
54
+
55
+ def ensure_onboarding_is_complete_and_set_next_step
56
+ unless ensure_onboarding_is_complete
57
+ session[:after_onboarding_url] ||= request.url
58
+ end
59
+ end
60
+
61
+ def ensure_onboarding_is_complete
62
+ # This is temporary, but if we've gotten this far and `@team` is loaded, we need to ensure current_team is
63
+ # updated for the checks below. This entire concept of `current_team` is going away soon.
64
+ current_user.update(current_team: @team) if @team&.persisted?
65
+
66
+ # since the onboarding controllers are child classes of this class,
67
+ # we actually have to check to make sure we're not currently on that
68
+ # step otherwise we'll end up in an endless redirection loop.
69
+ if current_user.email_is_oauth_placeholder?
70
+ if adding_user_email?
71
+ return true
72
+ else
73
+ redirect_to edit_account_onboarding_user_email_path(current_user)
74
+ return false
75
+ end
76
+ end
77
+
78
+ # some team-related onboarding steps need to be skipped if we're in the process
79
+ # of creating a new team.
80
+ unless adding_team? || accepting_invitation?
81
+
82
+ # USER ONBOARDING STUFF
83
+ # first we make sure the user is properly onboarded.
84
+ unless current_team.present?
85
+
86
+ # don't force the user to create a team if they've already got one.
87
+ if current_user.teams.any?
88
+ current_user.update(current_team: current_user.teams.first)
89
+ else
90
+ redirect_to new_account_team_path
91
+ return false
92
+ end
93
+ end
94
+
95
+ # TEAM ONBOARDING STUFF.
96
+ # then make sure the team is properly onboarded.
97
+
98
+ # since the onboarding controllers are child classes of this class,
99
+ # we actually have to check to make sure we're not currently on that
100
+ # step otherwise we'll end up in an endless redirection loop.
101
+ unless current_user.details_provided?
102
+ if adding_user_details?
103
+ return true
104
+ else
105
+ redirect_to edit_account_onboarding_user_detail_path(current_user)
106
+ return false
107
+ end
108
+ end
109
+
110
+ end
111
+
112
+ true
113
+ end
114
+
115
+ def set_last_seen_at
116
+ current_user.update_attribute(:last_seen_at, Time.current)
117
+ end
118
+ end
@@ -1,7 +1,7 @@
1
1
  module Account::Users::ControllerBase
2
2
  extend ActiveSupport::Concern
3
3
 
4
- include do
4
+ included do
5
5
  load_and_authorize_resource
6
6
 
7
7
  before_action do
@@ -0,0 +1,119 @@
1
+ module Controllers::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # these are common for authentication workflows.
6
+ include InvitationOnlyHelper
7
+ include InvitationsHelper
8
+
9
+ include DeviseCurrentAttributes
10
+
11
+ around_action :set_locale
12
+ layout :layout_by_resource
13
+
14
+ before_action { @updating = request.headers["X-Cable-Ready"] == "update" }
15
+
16
+ # TODO Extract this into an optional `bullet_train-sentry` package.
17
+ before_action :set_sentry_context
18
+
19
+ skip_before_action :verify_authenticity_token, if: -> { controller_name == "sessions" && action_name == "create" }
20
+
21
+ rescue_from CanCan::AccessDenied do |exception|
22
+ if current_user.nil?
23
+ respond_to do |format|
24
+ format.html do
25
+ session["user_return_to"] = request.path
26
+ redirect_to [:new, :user, :session], alert: exception.message
27
+ end
28
+ end
29
+ elsif current_user.teams.none?
30
+ respond_to do |format|
31
+ format.html { redirect_to [:new, :account, :team], alert: exception.message }
32
+ end
33
+ else
34
+ respond_to do |format|
35
+ # TODO we do this for now because it ensures `current_team` doesn't remain set in an invalid state.
36
+ format.html { redirect_to [:account, current_user.teams.first], alert: exception.message }
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ # this is an ugly hack, but it's what is recommended at
43
+ # https://github.com/plataformatec/devise/wiki/How-To:-Create-custom-layouts
44
+ def layout_by_resource
45
+ if devise_controller?
46
+ "devise"
47
+ else
48
+ "public"
49
+ end
50
+ end
51
+
52
+ def after_sign_in_path_for(resource_or_scope)
53
+ resource = resource_or_scope.class.name.downcase
54
+ stored_location_for(resource) || account_dashboard_path
55
+ end
56
+
57
+ def after_sign_up_path_for(resource_or_scope)
58
+ resource = resource_or_scope.class.name.downcase
59
+ stored_location_for(resource) || account_dashboard_path
60
+ end
61
+
62
+ def current_team
63
+ helpers.current_team
64
+ end
65
+
66
+ def current_membership
67
+ helpers.current_membership
68
+ end
69
+
70
+ def current_locale
71
+ helpers.current_locale
72
+ end
73
+
74
+ def enforce_invitation_only
75
+ if invitation_only?
76
+ unless helpers.invited?
77
+ redirect_to [:account, :teams], notice: t("teams.notifications.invitation_only")
78
+ end
79
+ end
80
+ end
81
+
82
+ def set_locale
83
+ I18n.locale = [
84
+ current_user&.locale,
85
+ current_user&.current_team&.locale,
86
+ http_accept_language.compatible_language_from(I18n.available_locales),
87
+ I18n.default_locale.to_s
88
+ ].compact.find { |potential_locale| I18n.available_locales.include?(potential_locale.to_sym) }
89
+ yield
90
+ I18n.locale = I18n.default_locale
91
+ end
92
+
93
+ # Whitelist the account namespace and prevent JavaScript
94
+ # embedding when passing paths as parameters in links.
95
+ def only_allow_path(path)
96
+ return if path.nil?
97
+ account_namespace_regexp = /^\/account\/*+/
98
+ scheme = URI.parse(path).scheme
99
+ return nil unless path.match?(account_namespace_regexp) && scheme != "javascript"
100
+ path
101
+ end
102
+
103
+ # TODO Extract this into an optional `bullet_train-sentry` package.
104
+ def set_sentry_context
105
+ return unless ENV["SENTRY_DSN"]
106
+
107
+ Sentry.configure_scope do |scope|
108
+ scope.set_user(id: current_user.id, email: current_user.email) if current_user
109
+
110
+ scope.set_context(
111
+ "request",
112
+ {
113
+ url: request.url,
114
+ params: params.to_unsafe_h
115
+ }
116
+ )
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,13 @@
1
+ module DeviseCurrentAttributes
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_current_user
6
+ end
7
+
8
+ def set_current_user
9
+ if current_user
10
+ Current.user = current_user
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ module Registrations::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ def new
6
+ if invitation_only?
7
+ unless session[:invitation_uuid] || session[:invitation_key]
8
+ return redirect_to root_path
9
+ end
10
+ end
11
+
12
+ # do all the regular devise stuff.
13
+ super
14
+ end
15
+
16
+ def create
17
+ # do all the regular devise stuff first.
18
+ super
19
+
20
+ # if current_user is defined, that means they were successful registering.
21
+ if current_user
22
+
23
+ # TODO i think this might be redundant. we've added a hook into `session["user_return_to"]` in the
24
+ # `invitations#accept` action and that might be enough to get them where they're supposed to be after
25
+ # either creating a new account or signing into an existing account.
26
+ handle_outstanding_invitation
27
+
28
+ # if the user doesn't have a team at this point, create one.
29
+ unless current_user.teams.any?
30
+ current_user.create_default_team
31
+ end
32
+
33
+ # send the welcome email.
34
+ current_user.send_welcome_email unless current_user.email_is_oauth_placeholder?
35
+
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ module Sessions::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ def pre_otp
6
+ if (@email = params["user"]["email"].downcase.strip.presence)
7
+ @user = User.find_by(email: @email)
8
+ end
9
+
10
+ respond_to do |format|
11
+ format.js
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,9 @@
1
+ # i really, really wanted this controller in a namespace, but devise doesn't
2
+ # appear to support it. instead, i got the following error:
3
+ #
4
+ # 'Authentication::RegistrationsController' is not a supported controller name.
5
+ # This can lead to potential routing problems.
6
+
7
+ class RegistrationsController < Devise::RegistrationsController
8
+ include Registrations::ControllerBase
9
+ end
@@ -0,0 +1,3 @@
1
+ class SessionsController < Devise::SessionsController
2
+ include Sessions::ControllerBase
3
+ end
@@ -0,0 +1,12 @@
1
+ module Account::ButtonsHelper
2
+ def first_button_primary(context = nil)
3
+ @global ||= {}
4
+
5
+ if !@global[context]
6
+ @global[context] = true
7
+ "button"
8
+ else
9
+ "button-secondary"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,38 @@
1
+ module Account::DatesHelper
2
+ # e.g. October 11, 2018
3
+ def display_date(timestamp)
4
+ return nil unless timestamp
5
+ if local_time(timestamp).year == local_time(Time.now).year
6
+ local_time(timestamp).strftime("%B %-d")
7
+ else
8
+ local_time(timestamp).strftime("%B %-d, %Y")
9
+ end
10
+ end
11
+
12
+ # e.g. October 11, 2018 at 4:22 PM
13
+ # e.g. Yesterday at 2:12 PM
14
+ # e.g. April 24 at 7:39 AM
15
+ def display_date_and_time(timestamp)
16
+ return nil unless timestamp
17
+
18
+ # today?
19
+ if local_time(timestamp).to_date == local_time(Time.now).to_date
20
+ "Today at #{display_time(timestamp)}"
21
+ # yesterday?
22
+ elsif (local_time(timestamp).to_date) == (local_time(Time.now).to_date - 1.day)
23
+ "Yesterday at #{display_time(timestamp)}"
24
+ else
25
+ "#{display_date(timestamp)} at #{display_time(timestamp)}"
26
+ end
27
+ end
28
+
29
+ # e.g. 4:22 PM
30
+ def display_time(timestamp)
31
+ local_time(timestamp).strftime("%l:%M %p")
32
+ end
33
+
34
+ def local_time(time)
35
+ return time if current_user.time_zone.nil?
36
+ time.in_time_zone(current_user.time_zone)
37
+ end
38
+ end
@@ -0,0 +1,67 @@
1
+ module Account::FormsHelper
2
+ PRESENCE_VALIDATORS = [ActiveRecord::Validations::PresenceValidator, ActiveModel::Validations::PresenceValidator]
3
+
4
+ def presence_validated?(object, attribute)
5
+ validators = object.class.validators
6
+ validators.select! do |validator|
7
+ PRESENCE_VALIDATORS.include?(validator.class) && validator.attributes.include?(attribute)
8
+ end
9
+ validators.any?
10
+ end
11
+
12
+ def flush_content_for(name)
13
+ content_for name, flush: true do
14
+ ""
15
+ end
16
+ end
17
+
18
+ def options_with_labels(options, namespace)
19
+ hash = {}
20
+ options.each do |option|
21
+ hash[option] = t([namespace, option].join("."))
22
+ end
23
+ hash
24
+ end
25
+
26
+ def if_present(string)
27
+ string.present? ? string : nil
28
+ end
29
+
30
+ def id_for(form, method)
31
+ [form.object.class.name, form.index, method].compact.join("_").underscore
32
+ end
33
+
34
+ def model_key(form)
35
+ form.object.class.name.pluralize.underscore
36
+ end
37
+
38
+ def labels_for(form, method)
39
+ keys = [:placeholder, :label, :help, :options_help]
40
+ path = [model_key(form), (current_fields_namespace || :fields), method].compact
41
+ Struct.new(*keys).new(*keys.map { |key| t((path + [key]).join("."), default: "").presence })
42
+ end
43
+
44
+ def options_for(form, method)
45
+ # e.g. "scaffolding/completely_concrete/tangible_things.fields.text_area_value.options"
46
+ path = [model_key(form), (current_fields_namespace || :fields), method, :options]
47
+ t(path.compact.join("."))
48
+ end
49
+
50
+ def legacy_label_for(form, method)
51
+ # e.g. 'scaffolding/things.labels.name'
52
+ key = "#{model_key(form)}.labels.#{method}"
53
+ # e.g. 'scaffolding/things.labels.name' or 'scaffolding.things.labels.name' or nil
54
+ t(key, default: "").presence || t(key.tr("/", "."), default: "").presence
55
+ end
56
+
57
+ def within_fields_namespace(namespace)
58
+ @fields_namespaces ||= []
59
+ @fields_namespaces << namespace
60
+ yield
61
+ @fields_namespaces.pop
62
+ end
63
+
64
+ def current_fields_namespace
65
+ @fields_namespaces&.last
66
+ end
67
+ end
@@ -0,0 +1,51 @@
1
+ module Account::LocaleHelper
2
+ def current_locale
3
+ current_user.locale || current_team.locale || "en"
4
+ end
5
+
6
+ # as of now, we only calculate a possessive version of nouns in english.
7
+ # if you're aware of another language where we can do this, please don't hesitate to reach out!
8
+ def possessive_string(string)
9
+ [:en].include?(I18n.locale) ? string.possessive : string
10
+ end
11
+
12
+ def model_locales(model)
13
+ name = model.label_string.presence
14
+ return {} unless name
15
+
16
+ hash = {}
17
+ prefix = model.class.name.split("::").last.underscore
18
+ hash[:"#{prefix}_name"] = name
19
+ hash[:"#{prefix.pluralize}_possessive"] = possessive_string(name)
20
+
21
+ hash
22
+ end
23
+
24
+ def models_locales(*models)
25
+ hash = {}
26
+ models.compact.each do |model|
27
+ hash.merge! model_locales(model)
28
+ end
29
+ hash
30
+ end
31
+
32
+ # this is a bit scary, no?
33
+ def account_controller?
34
+ controller.class.name.match(/^Account::/)
35
+ end
36
+
37
+ def t(key, options = {})
38
+ if account_controller?
39
+ # give preference to the options they've passed in.
40
+ options = models_locales(@child_object, @parent_object).merge(options)
41
+ end
42
+ super(key, options)
43
+ end
44
+
45
+ # like 't', but if the key isn't found, it returns nil.
46
+ def ot(key, options = {})
47
+ t(key, options)
48
+ rescue I18n::MissingTranslationData => _
49
+ nil
50
+ end
51
+ end
@@ -0,0 +1,5 @@
1
+ module Account::MarkdownHelper
2
+ def markdown(string)
3
+ CommonMarker.render_html(string, :UNSAFE, [:table]).html_safe
4
+ end
5
+ end
@@ -0,0 +1,5 @@
1
+ module Account::RoleHelper
2
+ def role_options_for(object)
3
+ object.class.assignable_roles.map { |role| [role.id, t("#{object.class.to_s.pluralize.underscore}.fields.role_ids.options.#{role.key}.label")] }
4
+ end
5
+ end
@@ -0,0 +1,32 @@
1
+ module AttributesHelper
2
+ def current_attributes_object
3
+ @_attributes_helper_objects ? @_attributes_helper_objects.last : nil
4
+ end
5
+
6
+ def current_attributes_strategy
7
+ @_attributes_helper_strategies ? @_attributes_helper_strategies.last : nil
8
+ end
9
+
10
+ def with_attribute_settings(options)
11
+ @_attributes_helper_objects ||= []
12
+ @_attributes_helper_strategies ||= []
13
+
14
+ if options[:object]
15
+ @_attributes_helper_objects << options[:object]
16
+ end
17
+
18
+ if options[:strategy]
19
+ @_attributes_helper_strategies << options[:strategy]
20
+ end
21
+
22
+ yield
23
+
24
+ if options[:strategy]
25
+ @_attributes_helper_strategies.pop
26
+ end
27
+
28
+ if options[:object]
29
+ @_attributes_helper_objects.pop
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module EmailHelper
2
+ def email_image_tag(image, **options)
3
+ image_underscore = image.tr("-", "_")
4
+ attachments.inline[image_underscore] = File.read(Rails.root.join("app/javascript/images/#{image}"))
5
+ image_tag attachments.inline[image_underscore].url, **options
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module ImagesHelper
2
+ def image_width_for_height(filename, target_height)
3
+ source_width, source_height = FastImage.size("#{Rails.root}/app/javascript/images/#{filename}")
4
+ ratio = source_width.to_f / source_height.to_f
5
+ (target_height * ratio).to_i
6
+ end
7
+ end
@@ -0,0 +1,16 @@
1
+ module Mailers::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ default from: "#{I18n.t("application.name")} <#{I18n.t("application.support_email")}>"
6
+ layout "mailer"
7
+
8
+ helper :email
9
+ helper :application
10
+ helper :images
11
+ helper "account/teams"
12
+ helper "account/users"
13
+ helper "account/locale"
14
+ helper "fields/trix_editor"
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ class DeviseMailer < Devise::Mailer
2
+ def headers_for(action, opts)
3
+ headers = super(action, opts)
4
+ if resource.full_name.present?
5
+ headers[:to] = "\"#{resource.full_name}\" <#{resource.email}>"
6
+ @email = headers[:to]
7
+ end
8
+ headers
9
+ end
10
+ end
@@ -0,0 +1,24 @@
1
+ class UserMailer < ApplicationMailer
2
+ def welcome(user)
3
+ @user = user
4
+ @cta_url = account_dashboard_url
5
+ @values = {
6
+ # are there any substitution values you want to include?
7
+ }
8
+ mail(to: @user.email, subject: I18n.t("user_mailer.welcome.subject", **@values))
9
+ end
10
+
11
+ # technically not a 'user' email, but they'll be a user soon.
12
+ # didn't seem worth creating an entirely new mailer for.
13
+ def invited(uuid)
14
+ @invitation = Invitation.find_by_uuid uuid
15
+ return if @invitation.nil?
16
+ @cta_url = accept_account_invitation_url(@invitation.uuid)
17
+ @values = {
18
+ # Just in case the inviting user has been removed from the team...
19
+ inviter_name: @invitation.from_membership&.user&.full_name || @invitation.from_membership.name,
20
+ team_name: @invitation.team.name,
21
+ }
22
+ mail(to: @invitation.email, subject: I18n.t("user_mailer.invited.subject", **@values))
23
+ end
24
+ end