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.
- checksums.yaml +4 -4
- data/app/controllers/account/two_factors_controller.rb +18 -0
- data/app/controllers/concerns/account/controllers/base.rb +118 -0
- data/app/controllers/concerns/account/users/controller_base.rb +1 -1
- data/app/controllers/concerns/controllers/base.rb +119 -0
- data/app/controllers/concerns/devise_current_attributes.rb +13 -0
- data/app/controllers/concerns/registrations/controller_base.rb +39 -0
- data/app/controllers/concerns/sessions/controller_base.rb +15 -0
- data/app/controllers/registrations_controller.rb +9 -0
- data/app/controllers/sessions_controller.rb +3 -0
- data/app/helpers/account/buttons_helper.rb +12 -0
- data/app/helpers/account/dates_helper.rb +38 -0
- data/app/helpers/account/forms_helper.rb +67 -0
- data/app/helpers/account/locale_helper.rb +51 -0
- data/app/helpers/account/markdown_helper.rb +5 -0
- data/app/helpers/account/role_helper.rb +5 -0
- data/app/helpers/attributes_helper.rb +32 -0
- data/app/helpers/email_helper.rb +7 -0
- data/app/helpers/images_helper.rb +7 -0
- data/app/mailers/concerns/mailers/base.rb +16 -0
- data/app/mailers/devise_mailer.rb +10 -0
- data/app/mailers/user_mailer.rb +24 -0
- data/app/models/concerns/records/base.rb +60 -0
- data/app/views/account/two_factors/create.js.erb +1 -0
- data/app/views/account/two_factors/destroy.js.erb +1 -0
- data/app/views/devise/confirmations/new.html.erb +16 -0
- data/app/views/devise/mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/devise/mailer/password_change.html.erb +3 -0
- data/app/views/devise/mailer/reset_password_instructions.html.erb +30 -0
- data/app/views/devise/mailer/unlock_instructions.html.erb +7 -0
- data/app/views/devise/passwords/edit.html.erb +17 -0
- data/app/views/devise/passwords/new.html.erb +20 -0
- data/app/views/devise/registrations/_two_factor.html.erb +42 -0
- data/app/views/devise/registrations/edit.html.erb +43 -0
- data/app/views/devise/registrations/new.html.erb +30 -0
- data/app/views/devise/sessions/new.html.erb +59 -0
- data/app/views/devise/sessions/pre_otp.js.erb +11 -0
- data/app/views/devise/shared/_links.html.erb +21 -0
- data/app/views/devise/shared/_oauth.html.erb +9 -0
- data/app/views/devise/unlocks/new.html.erb +16 -0
- data/app/views/layouts/account.html.erb +1 -0
- data/app/views/layouts/devise.html.erb +1 -0
- data/config/locales/en/devise.en.yml +110 -0
- data/lib/bullet_train/version.rb +1 -1
- metadata +43 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e043af38dcb40348528dd6b925433147b3e58f0fd64d969c7396feebce04091a
|
4
|
+
data.tar.gz: 4546e61efd71d6b92c270e02a50ff33e558d6007d4cc761c925192d77562459f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
@@ -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,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,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,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,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,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
|