bullet_train 1.0.5 → 1.0.9

Sign up to get free protection for your applications and to get access to all the features.
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