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.
- 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
|