bullet_train 1.0.1 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,137 @@
1
+ module Memberships::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # See `docs/permissions.md` for details.
6
+ include Roles::Support
7
+
8
+ belongs_to :user, optional: true
9
+ belongs_to :team
10
+ belongs_to :invitation, optional: true, dependent: :destroy
11
+ belongs_to :added_by, class_name: "Membership", optional: true
12
+ belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
13
+
14
+ has_many :scaffolding_completely_concrete_tangible_things_assignments, class_name: "Scaffolding::CompletelyConcrete::TangibleThings::Assignment", dependent: :destroy
15
+ has_many :scaffolding_completely_concrete_tangible_things, through: :scaffolding_completely_concrete_tangible_things_assignments, source: :tangible_thing
16
+ has_many :reassignments_scaffolding_completely_concrete_tangible_things_reassignments, class_name: "Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment", dependent: :destroy, foreign_key: :membership_id
17
+
18
+ has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator", dependent: :destroy
19
+
20
+ after_destroy do
21
+ # if we're destroying a user's membership to the team they have set as
22
+ # current, then we need to remove that so they don't get an error.
23
+ if user&.current_team == team
24
+ user.current_team = nil
25
+ user.save
26
+ end
27
+ end
28
+
29
+ scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
30
+ scope :current, -> { where("user_id IS NOT NULL") }
31
+ scope :tombstones, -> { includes(:invitation).where("user_id IS NULL AND invitations.id IS NULL").references(:invitation) }
32
+ end
33
+
34
+ def name
35
+ full_name
36
+ end
37
+
38
+ def label_string
39
+ full_name
40
+ end
41
+
42
+ # we overload this method so that when setting the list of role ids
43
+ # associated with a membership, admins can never remove the last admin
44
+ # of a team.
45
+ def role_ids=(ids)
46
+ # if this membership was an admin, and the new list of role ids don't include admin.
47
+ if admin? && !ids.include?(Role.admin.id)
48
+ unless team.admins.count > 1
49
+ raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
50
+ end
51
+ end
52
+
53
+ super(ids)
54
+ end
55
+
56
+ def unclaimed?
57
+ user.nil? && !invitation.nil?
58
+ end
59
+
60
+ def tombstone?
61
+ user.nil? && invitation.nil?
62
+ end
63
+
64
+ def last_admin?
65
+ return false unless admin?
66
+ return false unless user.present?
67
+ team.memberships.current.select(&:admin?) == [self]
68
+ end
69
+
70
+ def nullify_user
71
+ if last_admin?
72
+ raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
73
+ end
74
+
75
+ if (user_was = user)
76
+ unless user_first_name.present?
77
+ self.user_first_name = user.first_name
78
+ end
79
+
80
+ unless user_last_name.present?
81
+ self.user_last_name = user.last_name
82
+ end
83
+
84
+ unless user_profile_photo_id.present?
85
+ self.user_profile_photo_id = user.profile_photo_id
86
+ end
87
+
88
+ unless user_email.present?
89
+ self.user_email = user.email
90
+ end
91
+
92
+ self.user = nil
93
+ save
94
+
95
+ user_was.invalidate_ability_cache
96
+
97
+ user_was.update(
98
+ current_team: user_was.teams.first,
99
+ former_user: user_was.teams.empty?
100
+ )
101
+ end
102
+
103
+ # we do this here just in case by some weird chance an active membership had an invitation attached.
104
+ invitation&.destroy
105
+ end
106
+
107
+ def email
108
+ user&.email || user_email.presence || invitation&.email
109
+ end
110
+
111
+ def full_name
112
+ user&.full_name || [first_name.presence, last_name.presence].join(" ").presence || email
113
+ end
114
+
115
+ def first_name
116
+ user&.first_name || user_first_name
117
+ end
118
+
119
+ def last_name
120
+ user&.last_name || user_last_name
121
+ end
122
+
123
+ def last_initial
124
+ return nil unless last_name.present?
125
+ "#{last_name}."
126
+ end
127
+
128
+ def first_name_last_initial
129
+ [first_name, last_initial].map(&:present?).join(" ")
130
+ end
131
+
132
+ # TODO utilize this.
133
+ # members shouldn't receive notifications unless they are either an active user or an outstanding invitation.
134
+ def should_receive_notifications?
135
+ invitation.present? || user.present?
136
+ end
137
+ end
@@ -0,0 +1,51 @@
1
+ module Teams::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # super scaffolding
6
+ unless scaffolding_things_disabled?
7
+ has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true
8
+ end
9
+
10
+ # memberships and invitations
11
+ has_many :memberships, dependent: :destroy
12
+ has_many :users, through: :memberships
13
+ has_many :invitations
14
+
15
+ # oauth for grape api
16
+ has_many :platform_applications, class_name: "Platform::Application", dependent: :destroy, foreign_key: :team_id
17
+
18
+ # integrations
19
+ has_many :integrations_stripe_installations, class_name: "Integrations::StripeInstallation", dependent: :destroy if stripe_enabled?
20
+
21
+ # validations
22
+ validates :name, presence: true
23
+ validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
24
+ end
25
+
26
+ def admins
27
+ memberships.current_and_invited.admins
28
+ end
29
+
30
+ def admin_users
31
+ admins.map(&:user).compact
32
+ end
33
+
34
+ def primary_contact
35
+ admin_users.min { |user| user.created_at }
36
+ end
37
+
38
+ def formatted_email_address
39
+ primary_contact.email
40
+ end
41
+
42
+ def invalidate_caches
43
+ users.map(&:invalidate_ability_cache)
44
+ end
45
+
46
+ def team
47
+ # some generic features appeal to the `team` method for security or scoping purposes, but sometimes those same
48
+ # generic functions need to function for a team model as well, so we do this.
49
+ self
50
+ end
51
+ end
@@ -0,0 +1,164 @@
1
+ module Users::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ if two_factor_authentication_enabled?
6
+ devise :two_factor_authenticatable, :two_factor_backupable, :omniauthable,
7
+ :registerable, :recoverable, :rememberable, :trackable, :validatable,
8
+ otp_secret_encryption_key: ENV["TWO_FACTOR_ENCRYPTION_KEY"]
9
+ else
10
+ devise :omniauthable, :database_authenticatable, :registerable,
11
+ :recoverable, :rememberable, :trackable, :validatable
12
+ end
13
+
14
+ # teams
15
+ has_many :memberships, dependent: :destroy
16
+ has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, through: :memberships
17
+ has_many :teams, through: :memberships
18
+ belongs_to :current_team, class_name: "Team", optional: true
19
+ accepts_nested_attributes_for :current_team
20
+
21
+ # oauth providers
22
+ has_many :oauth_stripe_accounts, class_name: "Oauth::StripeAccount" if stripe_enabled?
23
+
24
+ # platform functionality.
25
+ belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
26
+
27
+ # validations
28
+ validate :real_emails_only
29
+ validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
30
+
31
+ # callbacks
32
+ after_update :set_teams_time_zone
33
+ end
34
+
35
+ # TODO we need to update this to some sort of invalid email address or something
36
+ # people know to ignore. it would be a security problem to have this pointing
37
+ # at anybody's real email address.
38
+ def email_is_oauth_placeholder?
39
+ !!email.match(/noreply\+.*@bullettrain.co/)
40
+ end
41
+
42
+ def label_string
43
+ name
44
+ end
45
+
46
+ def name
47
+ full_name.present? ? full_name : email
48
+ end
49
+
50
+ def full_name
51
+ [first_name_was, last_name_was].select(&:present?).join(" ")
52
+ end
53
+
54
+ def details_provided?
55
+ first_name.present? && last_name.present? && current_team.name.present?
56
+ end
57
+
58
+ def send_welcome_email
59
+ UserMailer.welcome(self).deliver_later
60
+ end
61
+
62
+ def create_default_team
63
+ # This creates a `Membership`, because `User` `has_many :teams, through: :memberships`
64
+ # TODO The team name should take into account the user's current locale.
65
+ default_team = teams.create(name: "Your Team", time_zone: time_zone)
66
+ memberships.find_by(team: default_team).update role_ids: [Role.admin.id]
67
+ update(current_team: default_team)
68
+ end
69
+
70
+ def real_emails_only
71
+ if ENV["REALEMAIL_API_KEY"] && !Rails.env.test?
72
+ uri = URI("https://realemail.expeditedaddons.com")
73
+
74
+ # Change the input parameters here
75
+ uri.query = URI.encode_www_form({
76
+ api_key: ENV["REAL_EMAIL_KEY"],
77
+ email: email,
78
+ fix_typos: false
79
+ })
80
+
81
+ # Results are returned as a JSON object
82
+ result = JSON.parse(Net::HTTP.get_response(uri).body)
83
+
84
+ if result["syntax_error"]
85
+ errors.add(:email, "is not a valid email address")
86
+ elsif result["domain_error"] || (result.key?("mx_records_found") && !result["mx_records_found"])
87
+ errors.add(:email, "can't actually receive emails")
88
+ elsif result["is_disposable"]
89
+ errors.add(:email, "is a disposable email address")
90
+ end
91
+ end
92
+ end
93
+
94
+ def multiple_teams?
95
+ teams.count > 1
96
+ end
97
+
98
+ def one_team?
99
+ !multiple_teams?
100
+ end
101
+
102
+ def formatted_email_address
103
+ if details_provided?
104
+ "\"#{first_name} #{last_name}\" <#{email}>"
105
+ else
106
+ email
107
+ end
108
+ end
109
+
110
+ def administrating_team_ids
111
+ parent_ids_for(Role.admin, :memberships, :team)
112
+ end
113
+
114
+ def parent_ids_for(role, through, parent)
115
+ parent_id_column = "#{parent}_id"
116
+ key = "#{role.key}_#{through}_#{parent_id_column}s"
117
+ return ability_cache[key] if ability_cache && ability_cache[key]
118
+ role = nil if role.default?
119
+ value = send(through).with_role(role).distinct.pluck(parent_id_column)
120
+ current_cache = ability_cache || {}
121
+ current_cache[key] = value
122
+ update_column :ability_cache, current_cache
123
+ value
124
+ end
125
+
126
+ def invalidate_ability_cache
127
+ update_column(:ability_cache, {})
128
+ end
129
+
130
+ def otp_qr_code
131
+ issuer = I18n.t("application.name")
132
+ label = "#{issuer}:#{email}"
133
+ RQRCode::QRCode.new(otp_provisioning_uri(label, issuer: issuer))
134
+ end
135
+
136
+ def scaffolding_absolutely_abstract_creative_concepts_collaborators
137
+ Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator.joins(:membership).where(membership: {user_id: id})
138
+ end
139
+
140
+ def admin_scaffolding_absolutely_abstract_creative_concepts_ids
141
+ scaffolding_absolutely_abstract_creative_concepts_collaborators.admins.pluck(:creative_concept_id)
142
+ end
143
+
144
+ def editor_scaffolding_absolutely_abstract_creative_concepts_ids
145
+ scaffolding_absolutely_abstract_creative_concepts_collaborators.editors.pluck(:creative_concept_id)
146
+ end
147
+
148
+ def viewer_scaffolding_absolutely_abstract_creative_concepts_ids
149
+ scaffolding_absolutely_abstract_creative_concepts_collaborators.viewers.pluck(:creative_concept_id)
150
+ end
151
+
152
+ def developer?
153
+ return false unless ENV["DEVELOPER_EMAILS"]
154
+ # we use email_was so they can't try setting their email to the email of an admin.
155
+ return false unless email_was
156
+ ENV["DEVELOPER_EMAILS"].split(",").include?(email_was)
157
+ end
158
+
159
+ def set_teams_time_zone
160
+ teams.where(time_zone: nil).each do |team|
161
+ team.update(time_zone: time_zone) if team.users.count == 1
162
+ end
163
+ end
164
+ end
@@ -1,25 +1,5 @@
1
1
  class Invitation < ApplicationRecord
2
- # 🚫 DEFAULT BULLET TRAIN INVITATION FUNCTIONALITY
3
- # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
- # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
-
6
- belongs_to :team
7
- belongs_to :from_membership, class_name: "Membership"
8
- has_one :membership, dependent: :nullify
9
- has_many :roles, through: :membership
10
-
11
- accepts_nested_attributes_for :membership
12
-
13
- validates :email, presence: true
14
-
15
- before_create :generate_uuid
16
- after_create :set_added_by_membership
17
- after_create :send_invitation_email
18
-
19
- # ✅ YOUR APPLICATION'S INVITATION FUNCTIONALITY
20
- # This is the place where you should implement your own features on top of Bullet Train's functionality. There
21
- # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
22
-
2
+ include Invitations::Core
23
3
  # 🚅 add concerns above.
24
4
 
25
5
  # 🚅 add belongs_to associations above.
@@ -39,35 +19,4 @@ class Invitation < ApplicationRecord
39
19
  # 🚅 add delegations above.
40
20
 
41
21
  # 🚅 add methods above.
42
-
43
- # 🚫 DEFAULT BULLET TRAIN INVITATION FUNCTIONALITY
44
- # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
45
-
46
- def set_added_by_membership
47
- membership.update(added_by: from_membership)
48
- end
49
-
50
- def send_invitation_email
51
- UserMailer.invited(uuid).deliver_later
52
- end
53
-
54
- def generate_uuid
55
- self.uuid = SecureRandom.hex
56
- end
57
-
58
- def accept_for(user)
59
- User.transaction do
60
- user.memberships << membership
61
- user.update(current_team: team, former_user: false)
62
- destroy
63
- end
64
- end
65
-
66
- def name
67
- I18n.t("invitations.values.name", team_name: team.name)
68
- end
69
-
70
- def is_for?(user)
71
- user.email.downcase.strip == email.downcase.strip
72
- end
73
22
  end
@@ -0,0 +1,5 @@
1
+ module Invitations
2
+ def self.table_name_prefix
3
+ "users_"
4
+ end
5
+ end
@@ -1,40 +1,5 @@
1
1
  class Membership < ApplicationRecord
2
- # 🚫 DEFAULT BULLET TRAIN MEMBERSHIP FUNCTIONALITY
3
- # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
- # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
-
6
- # See `docs/permissions.md` for details.
7
- include Roles::Support
8
-
9
- belongs_to :user, optional: true
10
- belongs_to :team
11
- belongs_to :invitation, optional: true, dependent: :destroy
12
- belongs_to :added_by, class_name: "Membership", optional: true
13
- belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
14
-
15
- has_many :scaffolding_completely_concrete_tangible_things_assignments, class_name: "Scaffolding::CompletelyConcrete::TangibleThings::Assignment", dependent: :destroy
16
- has_many :scaffolding_completely_concrete_tangible_things, through: :scaffolding_completely_concrete_tangible_things_assignments, source: :tangible_thing
17
- has_many :reassignments_scaffolding_completely_concrete_tangible_things_reassignments, class_name: "Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignment", dependent: :destroy, foreign_key: :membership_id
18
-
19
- has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator", dependent: :destroy
20
-
21
- after_destroy do
22
- # if we're destroying a user's membership to the team they have set as
23
- # current, then we need to remove that so they don't get an error.
24
- if user&.current_team == team
25
- user.current_team = nil
26
- user.save
27
- end
28
- end
29
-
30
- scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
31
- scope :current, -> { where("user_id IS NOT NULL") }
32
- scope :tombstones, -> { includes(:invitation).where("user_id IS NULL AND invitations.id IS NULL").references(:invitation) }
33
-
34
- # ✅ YOUR APPLICATION'S MEMBERSHIP FUNCTIONALITY
35
- # This is the place where you should implement your own features on top of Bullet Train's functionality. There
36
- # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
37
-
2
+ include Memberships::Core
38
3
  # 🚅 add concerns above.
39
4
 
40
5
  # 🚅 add belongs_to associations above.
@@ -54,111 +19,4 @@ class Membership < ApplicationRecord
54
19
  # 🚅 add delegations above.
55
20
 
56
21
  # 🚅 add methods above.
57
-
58
- # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
59
- # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
60
-
61
- def name
62
- full_name
63
- end
64
-
65
- def label_string
66
- full_name
67
- end
68
-
69
- # we overload this method so that when setting the list of role ids
70
- # associated with a membership, admins can never remove the last admin
71
- # of a team.
72
- def role_ids=(ids)
73
- # if this membership was an admin, and the new list of role ids don't include admin.
74
- if admin? && !ids.include?(Role.admin.id)
75
- unless team.admins.count > 1
76
- raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
77
- end
78
- end
79
-
80
- super(ids)
81
- end
82
-
83
- def unclaimed?
84
- user.nil? && !invitation.nil?
85
- end
86
-
87
- def tombstone?
88
- user.nil? && invitation.nil?
89
- end
90
-
91
- def last_admin?
92
- return false unless admin?
93
- return false unless user.present?
94
- team.memberships.current.select(&:admin?) == [self]
95
- end
96
-
97
- def nullify_user
98
- if last_admin?
99
- raise RemovingLastTeamAdminException.new("You can't remove the last team admin.")
100
- end
101
-
102
- if (user_was = user)
103
- unless user_first_name.present?
104
- self.user_first_name = user.first_name
105
- end
106
-
107
- unless user_last_name.present?
108
- self.user_last_name = user.last_name
109
- end
110
-
111
- unless user_profile_photo_id.present?
112
- self.user_profile_photo_id = user.profile_photo_id
113
- end
114
-
115
- unless user_email.present?
116
- self.user_email = user.email
117
- end
118
-
119
- self.user = nil
120
- save
121
-
122
- user_was.invalidate_ability_cache
123
-
124
- user_was.update(
125
- current_team: user_was.teams.first,
126
- former_user: user_was.teams.empty?
127
- )
128
- end
129
-
130
- # we do this here just in case by some weird chance an active membership had an invitation attached.
131
- invitation&.destroy
132
- end
133
-
134
- def email
135
- user&.email || user_email.presence || invitation&.email
136
- end
137
-
138
- def full_name
139
- user&.full_name || [first_name.presence, last_name.presence].join(" ").presence || email
140
- end
141
-
142
- def first_name
143
- user&.first_name || user_first_name
144
- end
145
-
146
- def last_name
147
- user&.last_name || user_last_name
148
- end
149
-
150
- def last_initial
151
- return nil unless last_name.present?
152
- "#{last_name}."
153
- end
154
-
155
- def first_name_last_initial
156
- [first_name, last_initial].map(&:present?).join(" ")
157
- end
158
-
159
- # TODO utilize this.
160
- # members shouldn't receive notifications unless they are either an active user or an outstanding invitation.
161
- def should_receive_notifications?
162
- invitation.present? || user.present?
163
- end
164
22
  end
@@ -0,0 +1,5 @@
1
+ module Memberships
2
+ def self.table_name_prefix
3
+ "memberships_"
4
+ end
5
+ end
data/app/models/team.rb CHANGED
@@ -1,35 +1,6 @@
1
1
  class Team < ApplicationRecord
2
- # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
3
- # Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
4
- # (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
5
-
6
- # Outgoing webhooks.
2
+ include Teams::Core
7
3
  include Webhooks::Outgoing::TeamSupport
8
-
9
- # super scaffolding
10
- unless scaffolding_things_disabled?
11
- has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true
12
- end
13
-
14
- # memberships and invitations
15
- has_many :memberships, dependent: :destroy
16
- has_many :users, through: :memberships
17
- has_many :invitations
18
-
19
- # oauth for grape api
20
- has_many :platform_applications, class_name: "Platform::Application", dependent: :destroy, foreign_key: :team_id
21
-
22
- # integrations
23
- has_many :integrations_stripe_installations, class_name: "Integrations::StripeInstallation", dependent: :destroy if stripe_enabled?
24
-
25
- # validations
26
- validates :name, presence: true
27
- validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
28
-
29
- # ✅ YOUR APPLICATION'S TEAM FUNCTIONALITY
30
- # This is the place where you should implement your own features on top of Bullet Train's functionality. There
31
- # are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
32
-
33
4
  # 🚅 add concerns above.
34
5
 
35
6
  # 🚅 add belongs_to associations above.
@@ -49,33 +20,4 @@ class Team < ApplicationRecord
49
20
  # 🚅 add delegations above.
50
21
 
51
22
  # 🚅 add methods above.
52
-
53
- # 🚫 DEFAULT BULLET TRAIN TEAM FUNCTIONALITY
54
- # We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
55
-
56
- def admins
57
- memberships.current_and_invited.admins
58
- end
59
-
60
- def admin_users
61
- admins.map(&:user).compact
62
- end
63
-
64
- def primary_contact
65
- admin_users.min { |user| user.created_at }
66
- end
67
-
68
- def formatted_email_address
69
- primary_contact.email
70
- end
71
-
72
- def invalidate_caches
73
- users.map(&:invalidate_ability_cache)
74
- end
75
-
76
- def team
77
- # some generic features appeal to the `team` method for security or scoping purposes, but sometimes those same
78
- # generic functions need to function for a team model as well, so we do this.
79
- self
80
- end
81
23
  end
@@ -0,0 +1,5 @@
1
+ module Teams
2
+ def self.table_name_prefix
3
+ "teams_"
4
+ end
5
+ end