bullet_train 1.0.4 → 1.0.5

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