bullet_train 1.0.1 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,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
@@ -0,0 +1,46 @@
1
+ module Invitations::Base
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ belongs_to :team
6
+ belongs_to :from_membership, class_name: "Membership"
7
+ has_one :membership, dependent: :nullify
8
+ has_many :roles, through: :membership
9
+
10
+ accepts_nested_attributes_for :membership
11
+
12
+ validates :email, presence: true
13
+
14
+ before_create :generate_uuid
15
+ after_create :set_added_by_membership
16
+ after_create :send_invitation_email
17
+ end
18
+
19
+ def set_added_by_membership
20
+ membership.update(added_by: from_membership)
21
+ end
22
+
23
+ def send_invitation_email
24
+ UserMailer.invited(uuid).deliver_later
25
+ end
26
+
27
+ def generate_uuid
28
+ self.uuid = SecureRandom.hex
29
+ end
30
+
31
+ def accept_for(user)
32
+ User.transaction do
33
+ user.memberships << membership
34
+ user.update(current_team: team, former_user: false)
35
+ destroy
36
+ end
37
+ end
38
+
39
+ def name
40
+ I18n.t("invitations.values.name", team_name: team.name)
41
+ end
42
+
43
+ def is_for?(user)
44
+ user.email.downcase.strip == email.downcase.strip
45
+ end
46
+ end