bullet_train 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 533e95a2c22629bce2db6569181981dd661621d7a2b17347d210f782554ac6f9
4
- data.tar.gz: 7d179440547451b7ff69480478a8f2cb481e79b4b4ee3e80e1f950b219768150
3
+ metadata.gz: 633b451afdfeee512883e8536928cc2dd247f15139d59e7bfd24cc4d637b735d
4
+ data.tar.gz: c2a59912f00000e2b72ae5193b6d5cd0ebcd34a736f01260f5abfa1a5c170c94
5
5
  SHA512:
6
- metadata.gz: '059cb145c72d2adf059902f5d0934d36362ad06ea6dc728a3951057b60758187e4f192e7bd24d62f0030fbd1c3d36e7797ed94ea3cd390b4c955a49fc4653301'
7
- data.tar.gz: 108c4abc412f29b2321b1f6acc5c6c6e1e35d358fda6ba14f322dcfb49591c7ccc3c29ca10989e30ffcb915c5c2df727b9f4d4e044776f6e3a6f92d391f91389
6
+ metadata.gz: 6cacd920c5c13800b167a1d1c3e9ae8fa6d6bc1cea28d1128f7404ce8bd9e0615957aad91b4cb7363bb10ac88823294511344bf9103febe2b0ed830508472cba
7
+ data.tar.gz: 1bc0e30c37ebd765df530a9d25b65a591736be8420949c68651569347280d7403d3889cdd59933384825c19d5727a25c7d16569a42e9baececfeb2970938b85a
@@ -1,146 +1,3 @@
1
1
  class Account::InvitationsController < Account::ApplicationController
2
- # the 'accept' action isn't covered by cancancan, because simply having the
3
- # link is authorization enough to claim membership on a team. see `#accept`
4
- # for more information.
5
- account_load_and_authorize_resource :invitation, :team, except: [:show]
6
-
7
- # we skip the onboarding requirement for users claiming and invitation.
8
- # this way the invitation gets accepted after they complete the devise
9
- # workflow, but before they're prompted to complete their onboarding.
10
- skip_before_action :ensure_onboarding_is_complete_and_set_next_step, only: :accept
11
- skip_before_action :authenticate_user!, only: :accept
12
-
13
- # GET /invitations
14
- # GET /invitations.json
15
- def index
16
- redirect_to [:account, @team, :memberships]
17
- end
18
-
19
- # GET /invitations/1
20
- # GET /invitations/1.json
21
- def show
22
- # it's important that we only allow invitations to be shown via their uuid,
23
- # otherwise team members can just step the id in the url to claim an
24
- # invitation that would escalate their privileges.
25
- @invitation = Invitation.find_by(uuid: params[:id])
26
- unless @invitation
27
- raise I18n.t("global.notifications.not_found")
28
- end
29
- @team = @invitation.team
30
-
31
- # backfill these objects for the locale magic, since we didn't use `account_load_and_authorize_resource`.
32
- @child_object = @invitation
33
- @parent_object = @team
34
-
35
- render layout: "devise"
36
- end
37
-
38
- # POST /invitations/1/accept
39
- # POST /invitations/1/accept.json
40
- def accept
41
- # unless the user is signed in.
42
- if !current_user.present?
43
-
44
- # keep track of the uuid of the invitation so we can reload it
45
- # after they sign up. at this point we don't even know if it's
46
- # valid, but that's fine.
47
- session[:invitation_uuid] = params[:id]
48
-
49
- # also, we'll queue devise up to return to the invitation url after a sign in.
50
- session["user_return_to"] = request.path
51
-
52
- # assume the user needs to create an account.
53
- # this is not the default for devise, but a sensible default here.
54
- redirect_to new_user_registration_path
55
-
56
- else
57
-
58
- @invitation = Invitation.find_by(uuid: params[:id])
59
-
60
- if @invitation
61
- @team = @invitation.team
62
- if @invitation.is_for?(current_user) || request.post?
63
- @invitation.accept_for(current_user)
64
- redirect_to account_dashboard_path, notice: I18n.t("invitations.notifications.welcome", team_name: @team.name)
65
- else
66
- redirect_to account_invitation_path(@invitation.uuid)
67
- end
68
- else
69
- redirect_to account_dashboard_path
70
- end
71
-
72
- end
73
- end
74
-
75
- # GET /invitations/new
76
- def new
77
- @invitation.build_membership
78
- @cancel_path = only_allow_path(params[:cancel_path])
79
- end
80
-
81
- # POST /invitations
82
- # POST /invitations.json
83
- def create
84
- @invitation.membership.team = current_team
85
- # this allows notifications to be sent to a user before they've accepted their invitation.
86
- @invitation.membership.user_email = @invitation.email
87
- @invitation.from_membership = current_membership
88
- respond_to do |format|
89
- if @invitation.save
90
- format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.created") }
91
- format.json { render :show, status: :created, location: [:account, @team, @invitation] }
92
- else
93
- format.html { render :new, status: :unprocessable_entity }
94
- format.json { render json: @invitation.errors, status: :unprocessable_entity }
95
- end
96
- end
97
- end
98
-
99
- # DELETE /invitations/1
100
- # DELETE /invitations/1.json
101
- def destroy
102
- @invitation.destroy
103
- respond_to do |format|
104
- format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.destroyed") }
105
- format.json { head :no_content }
106
- end
107
- end
108
-
109
- private
110
-
111
- def manageable_role_keys
112
- helpers.current_membership.manageable_roles.map(&:key)
113
- end
114
-
115
- # NOTE this method is only designed to work in the context of creating a invitation.
116
- # we don't provide any support for updating invitations.
117
- def invitation_params
118
- # we use strong params first.
119
- strong_params = params.require(:invitation).permit(
120
- :email,
121
- # 🚅 super scaffolding will insert new fields above this line.
122
- # 🚅 super scaffolding will insert new arrays above this line.
123
- membership_attributes: [
124
- :user_first_name,
125
- :user_last_name,
126
- role_ids: []
127
- ],
128
- )
129
-
130
- # after that, we have to be more careful how we assign the roles.
131
- # we can't let users assign roles to an invitation that they don't have permission
132
- # to assign, but they do have permission to assign some to other team members.
133
- if params[:invitation] && params[:invitation][:role_ids].present?
134
-
135
- # ensure the list of role keys from the form only includes keys that they're allowed to assign.
136
- assignable_role_keys_from_the_form = params[:invitation][:role_ids].map(&:to_i) & manageable_role_keys
137
-
138
- strong_params[:role_ids] = assignable_role_keys_from_the_form
139
-
140
- end
141
-
142
- # 🚅 super scaffolding will insert processing for new fields above this line.
143
-
144
- strong_params
145
- end
2
+ include Invitations::ControllerBase
146
3
  end
@@ -1,132 +1,3 @@
1
1
  class Account::MembershipsController < Account::ApplicationController
2
- account_load_and_authorize_resource :membership, :team, member_actions: [:demote, :promote, :reinvite], collection_actions: [:search]
3
-
4
- def index
5
- unless @memberships.count > 0
6
- redirect_to account_team_invitations_path(@team), notice: I18n.t("memberships.notifications.no_members")
7
- end
8
- end
9
-
10
- def search
11
- # TODO This is a particularly crazy example where we're doing the search logic ourselves in SQL.
12
- # In the future, I could see us replacing this with a recommended example using Elasticsearch and the `searchkick` Ruby Gem.
13
- limit = params[:limit] || 100
14
- page = [params[:page].to_i, 1].max # Ensure we never have a negative or zero page value
15
- search_term = "%#{params[:search]&.upcase}%"
16
- offset = (page - 1) * limit
17
- # Currently we're only searching on user.first_name, user.last_name, memberships.user_first_name and memberships.user_last_name. Should we also search on the email address?
18
- # This query could use impromement. Currently if you search for "Ad Pal" you wouldn't find a user "Adam Pallozzi"
19
- query = "UPPER(first_name) LIKE :search_term OR UPPER(last_name) LIKE :search_term OR UPPER(user_first_name) LIKE :search_term OR UPPER(user_last_name) LIKE :search_term"
20
- # We're using left outer join here because we may get memberships that don't belong to a membership yet
21
- memberships = @team.memberships.accessible_by(current_ability, :show).left_outer_joins(:user).where(query, search_term: search_term)
22
- total_results = memberships.size
23
- # the Areal.sql(LOWER(COALESCE...)) part means that if the user record doesn't exist or if there are records that start with a lower case letter, we will still sort everything correctly using the user.first_name instead.
24
- memberships_array = memberships.limit(limit).offset(offset).order(Arel.sql("LOWER(COALESCE(first_name, user_first_name) )")).map { |membership| {id: membership.id, text: membership.label_string.to_s} }
25
- results = {results: memberships_array, pagination: {more: (total_results > page * limit)}}
26
- render json: results.to_json
27
- end
28
-
29
- def show
30
- end
31
-
32
- def edit
33
- end
34
-
35
- # PATCH/PUT /account/memberships/:id
36
- # PATCH/PUT /account/memberships/:id.json
37
- def update
38
- respond_to do |format|
39
- if @membership.update(membership_params)
40
- format.html { redirect_to [:account, @membership], notice: I18n.t("memberships.notifications.updated") }
41
- format.json { render :show, status: :ok, location: [:account, @membership] }
42
- else
43
- format.html { render :edit, status: :unprocessable_entity }
44
- format.json { render json: @membership.errors, status: :unprocessable_entity }
45
- end
46
- rescue RemovingLastTeamAdminException => _
47
- format.html { redirect_to [:account, @team, :memberships], alert: I18n.t("memberships.notifications.cant_demote") }
48
- format.json { render json: {exception: I18n.t("memberships.notifications.cant_demote")}, status: :unprocessable_entity }
49
- end
50
- end
51
-
52
- def demote
53
- @membership.roles.delete Role.admin
54
- redirect_to account_team_memberships_path(@team)
55
- rescue RemovingLastTeamAdminException => _
56
- redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_demote")
57
- end
58
-
59
- def promote
60
- @membership.roles << Role.admin unless @membership.roles.include?(Role.admin)
61
- redirect_to account_team_memberships_path(@team)
62
- end
63
-
64
- def destroy
65
- # Instead of destroying the membership, we nullify the user_id and use the membership record as a 'Tombstone' for referencing past associations (eg message at-mentions and Scaffolding::CompletelyConcrete::TangibleThings::Assignment)
66
-
67
- user_was = @membership.user
68
- @membership.nullify_user
69
-
70
- if user_was == current_user
71
- # if a user removes themselves from a team, we'll have to send them to their dashboard.
72
- redirect_to account_dashboard_path, notice: I18n.t("memberships.notifications.you_removed_yourself", team_name: @team.name)
73
- else
74
- redirect_to [:account, @team, :memberships], notice: I18n.t("memberships.notifications.destroyed")
75
- end
76
- rescue RemovingLastTeamAdminException
77
- redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_remove")
78
- end
79
-
80
- def reinvite
81
- @invitation = Invitation.new(membership: @membership, team: @team, email: @membership.user_email, from_membership: current_membership)
82
- if @invitation.save
83
- redirect_to [:account, @team, :memberships], notice: I18n.t("account.memberships.notifications.reinvited")
84
- else
85
- redirect_to [:account, @team, :memberships], notice: "There was an error creating the invitation (#{@invitation.errors.full_messages.to_sentence})"
86
- end
87
- end
88
-
89
- private
90
-
91
- def manageable_role_keys
92
- helpers.current_membership.manageable_roles.map(&:key)
93
- end
94
-
95
- # NOTE this method is only designed to work in the context of updating a membership.
96
- # we don't provide any support for creating memberships other than by an invitation.
97
- def membership_params
98
- # we use strong params first.
99
- strong_params = params.require(:membership).permit(
100
- :user_first_name,
101
- :user_last_name,
102
- :user_profile_photo_id,
103
- # 🚅 super scaffolding will insert new fields above this line.
104
- # 🚅 super scaffolding will insert new arrays above this line.
105
- )
106
-
107
- # after that, we have to be more careful how we update the roles.
108
- # we can't let users remove roles from a membership that they don't have permission
109
- # to remove, but we want to allow them to add or remove other roles they do have
110
- # permission to assign to other team members.
111
- if params[:membership] && params[:membership][:role_ids].present?
112
-
113
- # first, start with the list of role keys already assigned to this membership.
114
- existing_role_keys = @membership.role_ids
115
-
116
- # generate a list of role keys we can't allow the current user to remove from this membership.
117
- existing_role_keys_that_are_unmanageable = existing_role_keys - manageable_role_keys
118
-
119
- # now let's ensure the list of role keys from the form only includes keys that they're allowed to assign.
120
- assignable_role_keys_from_the_form = params[:membership][:role_ids].map(&:to_s) & manageable_role_keys
121
-
122
- # any role keys that are manageable by the current user have to then come from the form data,
123
- # otherwise we can assume they were removed by being unchecked.
124
- strong_params[:role_ids] = existing_role_keys_that_are_unmanageable + assignable_role_keys_from_the_form
125
-
126
- end
127
-
128
- # 🚅 super scaffolding will insert processing for new fields above this line.
129
-
130
- strong_params
131
- end
2
+ include Account::Memberships::ControllerBase
132
3
  end
@@ -1,63 +1,3 @@
1
1
  class Account::Onboarding::UserDetailsController < Account::ApplicationController
2
- layout "devise"
3
- load_and_authorize_resource class: "User"
4
-
5
- # this is because cancancan doesn't let us set the instance variable name above.
6
- before_action do
7
- @user = @user_detail
8
- end
9
-
10
- # GET /users/1/edit
11
- def edit
12
- flash[:notice] = nil
13
- end
14
-
15
- # PATCH/PUT /users/1
16
- # PATCH/PUT /users/1.json
17
- def update
18
- respond_to do |format|
19
- if @user.update(user_params)
20
- # if you update your own user account, devise will normally kick you out, so we do this instead.
21
- bypass_sign_in current_user.reload
22
-
23
- if @user.details_provided?
24
- format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
25
- else
26
- format.html {
27
- flash[:error] = I18n.t("global.notifications.all_fields_required")
28
- redirect_to edit_account_onboarding_user_detail_path(@user)
29
- }
30
- end
31
-
32
- format.json { render :show, status: :ok, location: [:account, @user] }
33
- else
34
- format.html { render :edit, status: :unprocessable_entity }
35
- format.json { render json: @user.errors, status: :unprocessable_entity }
36
- end
37
- end
38
- end
39
-
40
- private
41
-
42
- # Never trust parameters from the scary internet, only allow the white list through.
43
- def user_params
44
- permitted_attributes = [
45
- :first_name,
46
- :last_name,
47
- :time_zone,
48
- # 🚅 super scaffolding will insert new fields above this line.
49
- ]
50
-
51
- permitted_hash = {
52
- # 🚅 super scaffolding will insert new arrays above this line.
53
- }
54
-
55
- if can? :edit, @user.current_team
56
- permitted_hash[:current_team_attributes] = [:id, :name]
57
- end
58
-
59
- params.require(:user).permit(permitted_attributes, permitted_hash)
60
-
61
- # 🚅 super scaffolding will insert processing for new fields above this line.
62
- end
2
+ include Account::Onboarding::UserDetails::ControllerBase
63
3
  end
@@ -1,65 +1,3 @@
1
1
  class Account::Onboarding::UserEmailController < Account::ApplicationController
2
- layout "devise"
3
- load_and_authorize_resource class: "User"
4
-
5
- # this is because cancancan doesn't let us set the instance variable name above.
6
- before_action do
7
- @user = @user_email
8
- end
9
-
10
- # GET /users/1/edit
11
- def edit
12
- flash[:notice] = nil
13
- if @user.email_is_oauth_placeholder?
14
- @user.email = nil
15
- end
16
- end
17
-
18
- # PATCH/PUT /users/1
19
- # PATCH/PUT /users/1.json
20
- def update
21
- respond_to do |format|
22
- if @user.update(user_params)
23
- # if you update your own user account, devise will normally kick you out, so we do this instead.
24
- bypass_sign_in current_user.reload
25
-
26
- if !@user.email_is_oauth_placeholder?
27
- @user.send_welcome_email
28
- format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
29
- else
30
- format.html {
31
- flash[:error] = I18n.t("global.notifications.all_fields_required")
32
- redirect_to edit_account_onboarding_user_detail_path(@user)
33
- }
34
- end
35
-
36
- format.json { render :show, status: :ok, location: [:account, @user] }
37
- else
38
-
39
- # this is just checking whether the error on the email field is taking the email
40
- # address is already taken.
41
- @email_taken = begin
42
- @user.errors.details[:email].select { |error| error[:error] == :taken }.any?
43
- rescue
44
- false
45
- end
46
-
47
- format.html { render :edit, status: :unprocessable_entity }
48
- format.json { render json: @user.errors, status: :unprocessable_entity }
49
- end
50
- end
51
- end
52
-
53
- private
54
-
55
- # Never trust parameters from the scary internet, only allow the white list through.
56
- def user_params
57
- params.require(:user).permit(
58
- :email,
59
- # 🚅 super scaffolding will insert new fields above this line.
60
- # 🚅 super scaffolding will insert new arrays above this line.
61
- )
62
-
63
- # 🚅 super scaffolding will insert processing for new fields above this line.
64
- end
2
+ include Account::Onboarding::UserEmail::ControllerBase
65
3
  end
@@ -1,122 +1,3 @@
1
1
  class Account::TeamsController < Account::ApplicationController
2
- load_and_authorize_resource :team, class: "Team", prepend: true
3
-
4
- prepend_before_action do
5
- if params["action"] == "new"
6
- current_user.current_team = nil
7
- end
8
- end
9
-
10
- before_action :enforce_invitation_only, only: [:create]
11
-
12
- before_action do
13
- # for magic locales.
14
- @child_object = @team
15
- end
16
-
17
- # GET /teams
18
- # GET /teams.json
19
- def index
20
- # if a user doesn't have multiple teams, we try to simplify the team ui/ux
21
- # as much as possible. links to this page should go to the current team
22
- # dashboard. however, some other links to this page are actually in branch
23
- # logic and will not display at all. instead, users will be linked to the
24
- # "new team" page. (see the main account sidebar menu for an example of
25
- # this.)
26
- unless current_user.multiple_teams?
27
- redirect_to account_team_path(current_team)
28
- end
29
- end
30
-
31
- # POST /teams/1/switch
32
- def switch_to
33
- current_user.current_team = @team
34
- current_user.save
35
- redirect_to account_dashboard_path
36
- end
37
-
38
- # GET /teams/1
39
- # GET /teams/1.json
40
- def show
41
- # I don't think this is the best place to close the loop on the onboarding process, but practically speaking it's
42
- # the easiest place to implement this at the moment, because all the onboarding steps redirect here on success.
43
- if session[:after_onboarding_url].present?
44
- redirect_to session.delete(:after_onboarding_url)
45
- end
46
-
47
- current_user.current_team = @team
48
- current_user.save
49
- end
50
-
51
- # GET /teams/new
52
- def new
53
- render :new, layout: "devise"
54
- end
55
-
56
- # GET /teams/1/edit
57
- def edit
58
- end
59
-
60
- # POST /teams
61
- # POST /teams.json
62
- def create
63
- @team = Team.new(team_params)
64
-
65
- respond_to do |format|
66
- if @team.save
67
-
68
- # also make the creator of the team the default admin.
69
- @team.memberships.create(user: current_user, roles: [Role.admin])
70
-
71
- current_user.current_team = @team
72
- current_user.former_user = false
73
- current_user.save
74
-
75
- format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.created") }
76
- format.json { render :show, status: :created, location: [:account, @team] }
77
- else
78
- format.html { render :new, layout: "devise" }
79
- format.json { render json: @team.errors, status: :unprocessable_entity }
80
- end
81
- end
82
- end
83
-
84
- # PATCH/PUT /teams/1
85
- # PATCH/PUT /teams/1.json
86
- def update
87
- respond_to do |format|
88
- if @team.update(team_params)
89
- format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.updated") }
90
- format.json { render :show, status: :ok, location: [:account, @team] }
91
- else
92
- format.html { render :edit, status: :unprocessable_entity }
93
- format.json { render json: @team.errors, status: :unprocessable_entity }
94
- end
95
- end
96
- end
97
-
98
- # # DELETE /teams/1
99
- # # DELETE /teams/1.json
100
- # def destroy
101
- # @team.destroy
102
- # respond_to do |format|
103
- # format.html { redirect_to account_teams_url, notice: 'Team was successfully destroyed.' }
104
- # format.json { head :no_content }
105
- # end
106
- # end
107
-
108
- private
109
-
110
- # Never trust parameters from the scary internet, only allow the white list through.
111
- def team_params
112
- params.require(:team).permit(
113
- :name,
114
- :time_zone,
115
- :locale,
116
- # 🚅 super scaffolding will insert new fields above this line.
117
- # 🚅 super scaffolding will insert new arrays above this line.
118
- )
119
-
120
- # 🚅 super scaffolding will insert processing for new fields above this line.
121
- end
2
+ include Account::Teams::ControllerBase
122
3
  end
@@ -1,59 +1,3 @@
1
1
  class Account::UsersController < Account::ApplicationController
2
- load_and_authorize_resource
3
-
4
- before_action do
5
- # for magic locales.
6
- @child_object = @user
7
- end
8
-
9
- # GET /account/users/1/edit
10
- def edit
11
- end
12
-
13
- # GET /account/users/1
14
- def show
15
- end
16
-
17
- def updating_password?
18
- params[:user].key?(:password)
19
- end
20
-
21
- # PATCH/PUT /account/users/1
22
- # PATCH/PUT /account/users/1.json
23
- def update
24
- respond_to do |format|
25
- if updating_password? ? @user.update_with_password(user_params) : @user.update_without_password(user_params)
26
- # if you update your own user account, devise will normally kick you out, so we do this instead.
27
- bypass_sign_in current_user.reload
28
- format.html { redirect_to [:edit, :account, @user], notice: t("users.notifications.updated") }
29
- format.json { render :show, status: :ok, location: [:account, @user] }
30
- else
31
- format.html { render :edit, status: :unprocessable_entity }
32
- format.json { render json: @user.errors, status: :unprocessable_entity }
33
- end
34
- end
35
- end
36
-
37
- private
38
-
39
- # Never trust parameters from the scary internet, only allow the white list through.
40
- def user_params
41
- # TODO enforce permissions on updating the user's team name.
42
- params.require(:user).permit(
43
- :email,
44
- :first_name,
45
- :last_name,
46
- :time_zone,
47
- :current_password,
48
- :password,
49
- :password_confirmation,
50
- :profile_photo_id,
51
- :locale,
52
- # 🚅 super scaffolding will insert new fields above this line.
53
- current_team_attributes: [:name],
54
- # 🚅 super scaffolding will insert new arrays above this line.
55
- )
56
-
57
- # 🚅 super scaffolding will insert processing for new fields above this line.
58
- end
2
+ include Account::Users::ControllerBase
59
3
  end
@@ -0,0 +1,150 @@
1
+ module Account::Invitations::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ # the 'accept' action isn't covered by cancancan, because simply having the
6
+ # link is authorization enough to claim membership on a team. see `#accept`
7
+ # for more information.
8
+ account_load_and_authorize_resource :invitation, :team, except: [:show]
9
+
10
+ # we skip the onboarding requirement for users claiming and invitation.
11
+ # this way the invitation gets accepted after they complete the devise
12
+ # workflow, but before they're prompted to complete their onboarding.
13
+ skip_before_action :ensure_onboarding_is_complete_and_set_next_step, only: :accept
14
+ skip_before_action :authenticate_user!, only: :accept
15
+ end
16
+
17
+ # GET /invitations
18
+ # GET /invitations.json
19
+ def index
20
+ redirect_to [:account, @team, :memberships]
21
+ end
22
+
23
+ # GET /invitations/1
24
+ # GET /invitations/1.json
25
+ def show
26
+ # it's important that we only allow invitations to be shown via their uuid,
27
+ # otherwise team members can just step the id in the url to claim an
28
+ # invitation that would escalate their privileges.
29
+ @invitation = Invitation.find_by(uuid: params[:id])
30
+ unless @invitation
31
+ raise I18n.t("global.notifications.not_found")
32
+ end
33
+ @team = @invitation.team
34
+
35
+ # backfill these objects for the locale magic, since we didn't use `account_load_and_authorize_resource`.
36
+ @child_object = @invitation
37
+ @parent_object = @team
38
+
39
+ render layout: "devise"
40
+ end
41
+
42
+ # POST /invitations/1/accept
43
+ # POST /invitations/1/accept.json
44
+ def accept
45
+ # unless the user is signed in.
46
+ if !current_user.present?
47
+
48
+ # keep track of the uuid of the invitation so we can reload it
49
+ # after they sign up. at this point we don't even know if it's
50
+ # valid, but that's fine.
51
+ session[:invitation_uuid] = params[:id]
52
+
53
+ # also, we'll queue devise up to return to the invitation url after a sign in.
54
+ session["user_return_to"] = request.path
55
+
56
+ # assume the user needs to create an account.
57
+ # this is not the default for devise, but a sensible default here.
58
+ redirect_to new_user_registration_path
59
+
60
+ else
61
+
62
+ @invitation = Invitation.find_by(uuid: params[:id])
63
+
64
+ if @invitation
65
+ @team = @invitation.team
66
+ if @invitation.is_for?(current_user) || request.post?
67
+ @invitation.accept_for(current_user)
68
+ redirect_to account_dashboard_path, notice: I18n.t("invitations.notifications.welcome", team_name: @team.name)
69
+ else
70
+ redirect_to account_invitation_path(@invitation.uuid)
71
+ end
72
+ else
73
+ redirect_to account_dashboard_path
74
+ end
75
+
76
+ end
77
+ end
78
+
79
+ # GET /invitations/new
80
+ def new
81
+ @invitation.build_membership
82
+ @cancel_path = only_allow_path(params[:cancel_path])
83
+ end
84
+
85
+ # POST /invitations
86
+ # POST /invitations.json
87
+ def create
88
+ @invitation.membership.team = current_team
89
+ # this allows notifications to be sent to a user before they've accepted their invitation.
90
+ @invitation.membership.user_email = @invitation.email
91
+ @invitation.from_membership = current_membership
92
+ respond_to do |format|
93
+ if @invitation.save
94
+ format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.created") }
95
+ format.json { render :show, status: :created, location: [:account, @team, @invitation] }
96
+ else
97
+ format.html { render :new, status: :unprocessable_entity }
98
+ format.json { render json: @invitation.errors, status: :unprocessable_entity }
99
+ end
100
+ end
101
+ end
102
+
103
+ # DELETE /invitations/1
104
+ # DELETE /invitations/1.json
105
+ def destroy
106
+ @invitation.destroy
107
+ respond_to do |format|
108
+ format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.destroyed") }
109
+ format.json { head :no_content }
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def manageable_role_keys
116
+ helpers.current_membership.manageable_roles.map(&:key)
117
+ end
118
+
119
+ # NOTE this method is only designed to work in the context of creating a invitation.
120
+ # we don't provide any support for updating invitations.
121
+ def invitation_params
122
+ # we use strong params first.
123
+ strong_params = params.require(:invitation).permit(
124
+ :email,
125
+ # 🚅 super scaffolding will insert new fields above this line.
126
+ # 🚅 super scaffolding will insert new arrays above this line.
127
+ membership_attributes: [
128
+ :user_first_name,
129
+ :user_last_name,
130
+ role_ids: []
131
+ ],
132
+ )
133
+
134
+ # after that, we have to be more careful how we assign the roles.
135
+ # we can't let users assign roles to an invitation that they don't have permission
136
+ # to assign, but they do have permission to assign some to other team members.
137
+ if params[:invitation] && params[:invitation][:role_ids].present?
138
+
139
+ # ensure the list of role keys from the form only includes keys that they're allowed to assign.
140
+ assignable_role_keys_from_the_form = params[:invitation][:role_ids].map(&:to_i) & manageable_role_keys
141
+
142
+ strong_params[:role_ids] = assignable_role_keys_from_the_form
143
+
144
+ end
145
+
146
+ # 🚅 super scaffolding will insert processing for new fields above this line.
147
+
148
+ strong_params
149
+ end
150
+ end
@@ -0,0 +1,136 @@
1
+ module Account::Memberships::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ account_load_and_authorize_resource :membership, :team, member_actions: [:demote, :promote, :reinvite], collection_actions: [:search]
6
+ end
7
+
8
+ def index
9
+ unless @memberships.count > 0
10
+ redirect_to account_team_invitations_path(@team), notice: I18n.t("memberships.notifications.no_members")
11
+ end
12
+ end
13
+
14
+ def search
15
+ # TODO This is a particularly crazy example where we're doing the search logic ourselves in SQL.
16
+ # In the future, I could see us replacing this with a recommended example using Elasticsearch and the `searchkick` Ruby Gem.
17
+ limit = params[:limit] || 100
18
+ page = [params[:page].to_i, 1].max # Ensure we never have a negative or zero page value
19
+ search_term = "%#{params[:search]&.upcase}%"
20
+ offset = (page - 1) * limit
21
+ # Currently we're only searching on user.first_name, user.last_name, memberships.user_first_name and memberships.user_last_name. Should we also search on the email address?
22
+ # This query could use impromement. Currently if you search for "Ad Pal" you wouldn't find a user "Adam Pallozzi"
23
+ query = "UPPER(first_name) LIKE :search_term OR UPPER(last_name) LIKE :search_term OR UPPER(user_first_name) LIKE :search_term OR UPPER(user_last_name) LIKE :search_term"
24
+ # We're using left outer join here because we may get memberships that don't belong to a membership yet
25
+ memberships = @team.memberships.accessible_by(current_ability, :show).left_outer_joins(:user).where(query, search_term: search_term)
26
+ total_results = memberships.size
27
+ # the Areal.sql(LOWER(COALESCE...)) part means that if the user record doesn't exist or if there are records that start with a lower case letter, we will still sort everything correctly using the user.first_name instead.
28
+ memberships_array = memberships.limit(limit).offset(offset).order(Arel.sql("LOWER(COALESCE(first_name, user_first_name) )")).map { |membership| {id: membership.id, text: membership.label_string.to_s} }
29
+ results = {results: memberships_array, pagination: {more: (total_results > page * limit)}}
30
+ render json: results.to_json
31
+ end
32
+
33
+ def show
34
+ end
35
+
36
+ def edit
37
+ end
38
+
39
+ # PATCH/PUT /account/memberships/:id
40
+ # PATCH/PUT /account/memberships/:id.json
41
+ def update
42
+ respond_to do |format|
43
+ if @membership.update(membership_params)
44
+ format.html { redirect_to [:account, @membership], notice: I18n.t("memberships.notifications.updated") }
45
+ format.json { render :show, status: :ok, location: [:account, @membership] }
46
+ else
47
+ format.html { render :edit, status: :unprocessable_entity }
48
+ format.json { render json: @membership.errors, status: :unprocessable_entity }
49
+ end
50
+ rescue RemovingLastTeamAdminException => _
51
+ format.html { redirect_to [:account, @team, :memberships], alert: I18n.t("memberships.notifications.cant_demote") }
52
+ format.json { render json: {exception: I18n.t("memberships.notifications.cant_demote")}, status: :unprocessable_entity }
53
+ end
54
+ end
55
+
56
+ def demote
57
+ @membership.roles.delete Role.admin
58
+ redirect_to account_team_memberships_path(@team)
59
+ rescue RemovingLastTeamAdminException => _
60
+ redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_demote")
61
+ end
62
+
63
+ def promote
64
+ @membership.roles << Role.admin unless @membership.roles.include?(Role.admin)
65
+ redirect_to account_team_memberships_path(@team)
66
+ end
67
+
68
+ def destroy
69
+ # Instead of destroying the membership, we nullify the user_id and use the membership record as a 'Tombstone' for referencing past associations (eg message at-mentions and Scaffolding::CompletelyConcrete::TangibleThings::Assignment)
70
+
71
+ user_was = @membership.user
72
+ @membership.nullify_user
73
+
74
+ if user_was == current_user
75
+ # if a user removes themselves from a team, we'll have to send them to their dashboard.
76
+ redirect_to account_dashboard_path, notice: I18n.t("memberships.notifications.you_removed_yourself", team_name: @team.name)
77
+ else
78
+ redirect_to [:account, @team, :memberships], notice: I18n.t("memberships.notifications.destroyed")
79
+ end
80
+ rescue RemovingLastTeamAdminException
81
+ redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_remove")
82
+ end
83
+
84
+ def reinvite
85
+ @invitation = Invitation.new(membership: @membership, team: @team, email: @membership.user_email, from_membership: current_membership)
86
+ if @invitation.save
87
+ redirect_to [:account, @team, :memberships], notice: I18n.t("account.memberships.notifications.reinvited")
88
+ else
89
+ redirect_to [:account, @team, :memberships], notice: "There was an error creating the invitation (#{@invitation.errors.full_messages.to_sentence})"
90
+ end
91
+ end
92
+
93
+ private
94
+
95
+ def manageable_role_keys
96
+ helpers.current_membership.manageable_roles.map(&:key)
97
+ end
98
+
99
+ # NOTE this method is only designed to work in the context of updating a membership.
100
+ # we don't provide any support for creating memberships other than by an invitation.
101
+ def membership_params
102
+ # we use strong params first.
103
+ strong_params = params.require(:membership).permit(
104
+ :user_first_name,
105
+ :user_last_name,
106
+ :user_profile_photo_id,
107
+ # 🚅 super scaffolding will insert new fields above this line.
108
+ # 🚅 super scaffolding will insert new arrays above this line.
109
+ )
110
+
111
+ # after that, we have to be more careful how we update the roles.
112
+ # we can't let users remove roles from a membership that they don't have permission
113
+ # to remove, but we want to allow them to add or remove other roles they do have
114
+ # permission to assign to other team members.
115
+ if params[:membership] && params[:membership][:role_ids].present?
116
+
117
+ # first, start with the list of role keys already assigned to this membership.
118
+ existing_role_keys = @membership.role_ids
119
+
120
+ # generate a list of role keys we can't allow the current user to remove from this membership.
121
+ existing_role_keys_that_are_unmanageable = existing_role_keys - manageable_role_keys
122
+
123
+ # now let's ensure the list of role keys from the form only includes keys that they're allowed to assign.
124
+ assignable_role_keys_from_the_form = params[:membership][:role_ids].map(&:to_s) & manageable_role_keys
125
+
126
+ # any role keys that are manageable by the current user have to then come from the form data,
127
+ # otherwise we can assume they were removed by being unchecked.
128
+ strong_params[:role_ids] = existing_role_keys_that_are_unmanageable + assignable_role_keys_from_the_form
129
+
130
+ end
131
+
132
+ # 🚅 super scaffolding will insert processing for new fields above this line.
133
+
134
+ strong_params
135
+ end
136
+ end
@@ -0,0 +1,67 @@
1
+ module Account::Onboarding::UserDetails::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ layout "devise"
6
+ load_and_authorize_resource class: "User"
7
+
8
+ # this is because cancancan doesn't let us set the instance variable name above.
9
+ before_action do
10
+ @user = @user_detail
11
+ end
12
+ end
13
+
14
+ # GET /users/1/edit
15
+ def edit
16
+ flash[:notice] = nil
17
+ end
18
+
19
+ # PATCH/PUT /users/1
20
+ # PATCH/PUT /users/1.json
21
+ def update
22
+ respond_to do |format|
23
+ if @user.update(user_params)
24
+ # if you update your own user account, devise will normally kick you out, so we do this instead.
25
+ bypass_sign_in current_user.reload
26
+
27
+ if @user.details_provided?
28
+ format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
29
+ else
30
+ format.html {
31
+ flash[:error] = I18n.t("global.notifications.all_fields_required")
32
+ redirect_to edit_account_onboarding_user_detail_path(@user)
33
+ }
34
+ end
35
+
36
+ format.json { render :show, status: :ok, location: [:account, @user] }
37
+ else
38
+ format.html { render :edit, status: :unprocessable_entity }
39
+ format.json { render json: @user.errors, status: :unprocessable_entity }
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # Never trust parameters from the scary internet, only allow the white list through.
47
+ def user_params
48
+ permitted_attributes = [
49
+ :first_name,
50
+ :last_name,
51
+ :time_zone,
52
+ # 🚅 super scaffolding will insert new fields above this line.
53
+ ]
54
+
55
+ permitted_hash = {
56
+ # 🚅 super scaffolding will insert new arrays above this line.
57
+ }
58
+
59
+ if can? :edit, @user.current_team
60
+ permitted_hash[:current_team_attributes] = [:id, :name]
61
+ end
62
+
63
+ params.require(:user).permit(permitted_attributes, permitted_hash)
64
+
65
+ # 🚅 super scaffolding will insert processing for new fields above this line.
66
+ end
67
+ end
@@ -0,0 +1,69 @@
1
+ module Account::Onboarding::UserEmail::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ layout "devise"
6
+ load_and_authorize_resource class: "User"
7
+
8
+ # this is because cancancan doesn't let us set the instance variable name above.
9
+ before_action do
10
+ @user = @user_email
11
+ end
12
+ end
13
+
14
+ # GET /users/1/edit
15
+ def edit
16
+ flash[:notice] = nil
17
+ if @user.email_is_oauth_placeholder?
18
+ @user.email = nil
19
+ end
20
+ end
21
+
22
+ # PATCH/PUT /users/1
23
+ # PATCH/PUT /users/1.json
24
+ def update
25
+ respond_to do |format|
26
+ if @user.update(user_params)
27
+ # if you update your own user account, devise will normally kick you out, so we do this instead.
28
+ bypass_sign_in current_user.reload
29
+
30
+ if !@user.email_is_oauth_placeholder?
31
+ @user.send_welcome_email
32
+ format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
33
+ else
34
+ format.html {
35
+ flash[:error] = I18n.t("global.notifications.all_fields_required")
36
+ redirect_to edit_account_onboarding_user_detail_path(@user)
37
+ }
38
+ end
39
+
40
+ format.json { render :show, status: :ok, location: [:account, @user] }
41
+ else
42
+
43
+ # this is just checking whether the error on the email field is taking the email
44
+ # address is already taken.
45
+ @email_taken = begin
46
+ @user.errors.details[:email].select { |error| error[:error] == :taken }.any?
47
+ rescue
48
+ false
49
+ end
50
+
51
+ format.html { render :edit, status: :unprocessable_entity }
52
+ format.json { render json: @user.errors, status: :unprocessable_entity }
53
+ end
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # Never trust parameters from the scary internet, only allow the white list through.
60
+ def user_params
61
+ params.require(:user).permit(
62
+ :email,
63
+ # 🚅 super scaffolding will insert new fields above this line.
64
+ # 🚅 super scaffolding will insert new arrays above this line.
65
+ )
66
+
67
+ # 🚅 super scaffolding will insert processing for new fields above this line.
68
+ end
69
+ end
@@ -0,0 +1,126 @@
1
+ module Account::Teams::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ load_and_authorize_resource :team, class: "Team", prepend: true
6
+
7
+ prepend_before_action do
8
+ if params["action"] == "new"
9
+ current_user.current_team = nil
10
+ end
11
+ end
12
+
13
+ before_action :enforce_invitation_only, only: [:create]
14
+
15
+ before_action do
16
+ # for magic locales.
17
+ @child_object = @team
18
+ end
19
+ end
20
+
21
+ # GET /teams
22
+ # GET /teams.json
23
+ def index
24
+ # if a user doesn't have multiple teams, we try to simplify the team ui/ux
25
+ # as much as possible. links to this page should go to the current team
26
+ # dashboard. however, some other links to this page are actually in branch
27
+ # logic and will not display at all. instead, users will be linked to the
28
+ # "new team" page. (see the main account sidebar menu for an example of
29
+ # this.)
30
+ unless current_user.multiple_teams?
31
+ redirect_to account_team_path(current_team)
32
+ end
33
+ end
34
+
35
+ # POST /teams/1/switch
36
+ def switch_to
37
+ current_user.current_team = @team
38
+ current_user.save
39
+ redirect_to account_dashboard_path
40
+ end
41
+
42
+ # GET /teams/1
43
+ # GET /teams/1.json
44
+ def show
45
+ # I don't think this is the best place to close the loop on the onboarding process, but practically speaking it's
46
+ # the easiest place to implement this at the moment, because all the onboarding steps redirect here on success.
47
+ if session[:after_onboarding_url].present?
48
+ redirect_to session.delete(:after_onboarding_url)
49
+ end
50
+
51
+ current_user.current_team = @team
52
+ current_user.save
53
+ end
54
+
55
+ # GET /teams/new
56
+ def new
57
+ render :new, layout: "devise"
58
+ end
59
+
60
+ # GET /teams/1/edit
61
+ def edit
62
+ end
63
+
64
+ # POST /teams
65
+ # POST /teams.json
66
+ def create
67
+ @team = Team.new(team_params)
68
+
69
+ respond_to do |format|
70
+ if @team.save
71
+
72
+ # also make the creator of the team the default admin.
73
+ @team.memberships.create(user: current_user, roles: [Role.admin])
74
+
75
+ current_user.current_team = @team
76
+ current_user.former_user = false
77
+ current_user.save
78
+
79
+ format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.created") }
80
+ format.json { render :show, status: :created, location: [:account, @team] }
81
+ else
82
+ format.html { render :new, layout: "devise" }
83
+ format.json { render json: @team.errors, status: :unprocessable_entity }
84
+ end
85
+ end
86
+ end
87
+
88
+ # PATCH/PUT /teams/1
89
+ # PATCH/PUT /teams/1.json
90
+ def update
91
+ respond_to do |format|
92
+ if @team.update(team_params)
93
+ format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.updated") }
94
+ format.json { render :show, status: :ok, location: [:account, @team] }
95
+ else
96
+ format.html { render :edit, status: :unprocessable_entity }
97
+ format.json { render json: @team.errors, status: :unprocessable_entity }
98
+ end
99
+ end
100
+ end
101
+
102
+ # # DELETE /teams/1
103
+ # # DELETE /teams/1.json
104
+ # def destroy
105
+ # @team.destroy
106
+ # respond_to do |format|
107
+ # format.html { redirect_to account_teams_url, notice: 'Team was successfully destroyed.' }
108
+ # format.json { head :no_content }
109
+ # end
110
+ # end
111
+
112
+ private
113
+
114
+ # Never trust parameters from the scary internet, only allow the white list through.
115
+ def team_params
116
+ params.require(:team).permit(
117
+ :name,
118
+ :time_zone,
119
+ :locale,
120
+ # 🚅 super scaffolding will insert new fields above this line.
121
+ # 🚅 super scaffolding will insert new arrays above this line.
122
+ )
123
+
124
+ # 🚅 super scaffolding will insert processing for new fields above this line.
125
+ end
126
+ end
@@ -0,0 +1,63 @@
1
+ module Account::Users::ControllerBase
2
+ extend ActiveSupport::Concern
3
+
4
+ include do
5
+ load_and_authorize_resource
6
+
7
+ before_action do
8
+ # for magic locales.
9
+ @child_object = @user
10
+ end
11
+ end
12
+
13
+ # GET /account/users/1/edit
14
+ def edit
15
+ end
16
+
17
+ # GET /account/users/1
18
+ def show
19
+ end
20
+
21
+ def updating_password?
22
+ params[:user].key?(:password)
23
+ end
24
+
25
+ # PATCH/PUT /account/users/1
26
+ # PATCH/PUT /account/users/1.json
27
+ def update
28
+ respond_to do |format|
29
+ if updating_password? ? @user.update_with_password(user_params) : @user.update_without_password(user_params)
30
+ # if you update your own user account, devise will normally kick you out, so we do this instead.
31
+ bypass_sign_in current_user.reload
32
+ format.html { redirect_to [:edit, :account, @user], notice: t("users.notifications.updated") }
33
+ format.json { render :show, status: :ok, location: [:account, @user] }
34
+ else
35
+ format.html { render :edit, status: :unprocessable_entity }
36
+ format.json { render json: @user.errors, status: :unprocessable_entity }
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Never trust parameters from the scary internet, only allow the white list through.
44
+ def user_params
45
+ # TODO enforce permissions on updating the user's team name.
46
+ params.require(:user).permit(
47
+ :email,
48
+ :first_name,
49
+ :last_name,
50
+ :time_zone,
51
+ :current_password,
52
+ :password,
53
+ :password_confirmation,
54
+ :profile_photo_id,
55
+ :locale,
56
+ # 🚅 super scaffolding will insert new fields above this line.
57
+ current_team_attributes: [:name],
58
+ # 🚅 super scaffolding will insert new arrays above this line.
59
+ )
60
+
61
+ # 🚅 super scaffolding will insert processing for new fields above this line.
62
+ end
63
+ end
@@ -1,4 +1,4 @@
1
- module Invitations::Core
1
+ module Invitations::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -1,4 +1,4 @@
1
- module Memberships::Core
1
+ module Memberships::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -1,4 +1,4 @@
1
- module Teams::Core
1
+ module Teams::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -1,4 +1,4 @@
1
- module Users::Core
1
+ module Users::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
@@ -1,3 +1,3 @@
1
1
  module BulletTrain
2
- VERSION = "1.0.4"
2
+ VERSION = "1.0.5"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bullet_train
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Culver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-01-23 00:00:00.000000000 Z
11
+ date: 2022-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -41,16 +41,22 @@ files:
41
41
  - app/controllers/account/onboarding/user_email_controller.rb
42
42
  - app/controllers/account/teams_controller.rb
43
43
  - app/controllers/account/users_controller.rb
44
+ - app/controllers/concerns/account/invitations/controller_base.rb
45
+ - app/controllers/concerns/account/memberships/controller_base.rb
46
+ - app/controllers/concerns/account/onboarding/user_details/controller_base.rb
47
+ - app/controllers/concerns/account/onboarding/user_email/controller_base.rb
48
+ - app/controllers/concerns/account/teams/controller_base.rb
49
+ - app/controllers/concerns/account/users/controller_base.rb
44
50
  - app/helpers/account/invitations_helper.rb
45
51
  - app/helpers/account/memberships_helper.rb
46
52
  - app/helpers/account/teams_helper.rb
47
53
  - app/helpers/account/users_helper.rb
48
54
  - app/helpers/invitation_only_helper.rb
49
55
  - app/helpers/invitations_helper.rb
50
- - app/models/concerns/invitations/core.rb
51
- - app/models/concerns/memberships/core.rb
52
- - app/models/concerns/teams/core.rb
53
- - app/models/concerns/users/core.rb
56
+ - app/models/concerns/invitations/base.rb
57
+ - app/models/concerns/memberships/base.rb
58
+ - app/models/concerns/teams/base.rb
59
+ - app/models/concerns/users/base.rb
54
60
  - app/models/invitation.rb
55
61
  - app/models/invitations.rb
56
62
  - app/models/membership.rb