bullet_train 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/bullet_train_manifest.js +0 -0
  6. data/app/controllers/account/invitations_controller.rb +146 -0
  7. data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
  8. data/app/controllers/account/memberships_controller.rb +132 -0
  9. data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
  10. data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
  11. data/app/controllers/account/teams_controller.rb +122 -0
  12. data/app/controllers/account/users_controller.rb +59 -0
  13. data/app/helpers/account/invitations_helper.rb +2 -0
  14. data/app/helpers/account/memberships_helper.rb +9 -0
  15. data/app/helpers/account/teams_helper.rb +80 -0
  16. data/app/helpers/account/users_helper.rb +81 -0
  17. data/app/helpers/invitation_only_helper.rb +10 -0
  18. data/app/helpers/invitations_helper.rb +17 -0
  19. data/app/models/invitation.rb +73 -0
  20. data/app/models/membership.rb +164 -0
  21. data/app/models/memberships/reassignments/assignment.rb +12 -0
  22. data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
  23. data/app/models/memberships/reassignments.rb +5 -0
  24. data/app/models/team.rb +81 -0
  25. data/app/models/user.rb +191 -0
  26. data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
  27. data/app/views/account/invitations/_form.html.erb +49 -0
  28. data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
  29. data/app/views/account/invitations/index.json.jbuilder +1 -0
  30. data/app/views/account/invitations/new.html.erb +12 -0
  31. data/app/views/account/invitations/show.html.erb +30 -0
  32. data/app/views/account/invitations/show.json.jbuilder +1 -0
  33. data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
  34. data/app/views/account/memberships/_form.html.erb +45 -0
  35. data/app/views/account/memberships/_index.html.erb +66 -0
  36. data/app/views/account/memberships/_menu_item.html.erb +8 -0
  37. data/app/views/account/memberships/_tombstones.html.erb +59 -0
  38. data/app/views/account/memberships/edit.html.erb +22 -0
  39. data/app/views/account/memberships/index.html.erb +7 -0
  40. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
  41. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
  42. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
  43. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
  44. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
  45. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
  46. data/app/views/account/memberships/show.html.erb +60 -0
  47. data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
  48. data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
  49. data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
  50. data/app/views/account/teams/_form.html.erb +22 -0
  51. data/app/views/account/teams/_index.html.erb +33 -0
  52. data/app/views/account/teams/_menu_item.html.erb +8 -0
  53. data/app/views/account/teams/_team.json.jbuilder +9 -0
  54. data/app/views/account/teams/edit.html.erb +12 -0
  55. data/app/views/account/teams/index.html.erb +6 -0
  56. data/app/views/account/teams/index.json.jbuilder +1 -0
  57. data/app/views/account/teams/new.html.erb +88 -0
  58. data/app/views/account/teams/show.html.erb +25 -0
  59. data/app/views/account/teams/show.json.jbuilder +1 -0
  60. data/app/views/account/users/_breadcrumbs.html.erb +4 -0
  61. data/app/views/account/users/_form.html.erb +39 -0
  62. data/app/views/account/users/edit.html.erb +50 -0
  63. data/app/views/account/users/show.html.erb +17 -0
  64. data/config/locales/en/invitations.en.yml +69 -0
  65. data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
  66. data/config/locales/en/memberships.en.yml +99 -0
  67. data/config/locales/en/onboarding/user_details.en.yml +11 -0
  68. data/config/locales/en/onboarding/user_email.en.yml +13 -0
  69. data/config/locales/en/teams.en.yml +85 -0
  70. data/config/locales/en/users.en.yml +110 -0
  71. data/config/routes.rb +2 -0
  72. data/db/migrate/20161115160419_devise_create_users.rb +41 -0
  73. data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
  74. data/db/migrate/20161117154605_create_teams.rb +10 -0
  75. data/db/migrate/20161117154709_create_memberships.rb +11 -0
  76. data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
  77. data/db/migrate/20161204234150_create_invitations.rb +11 -0
  78. data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
  79. data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
  80. data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
  81. data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
  82. data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
  83. data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
  84. data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
  85. data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
  86. data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
  87. data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
  88. data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
  89. data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
  90. data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
  91. data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
  92. data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
  93. data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
  94. data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
  95. data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
  96. data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
  97. data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
  98. data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
  99. data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
  100. data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
  101. data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
  102. data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
  103. data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
  104. data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
  105. data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
  106. data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
  107. data/lib/bullet_train/engine.rb +4 -0
  108. data/lib/bullet_train/version.rb +3 -0
  109. data/lib/bullet_train.rb +6 -0
  110. data/lib/tasks/bullet_train_tasks.rake +4 -0
  111. metadata +168 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bcd7fed56a4e0b49d9ca96b95e4a804f6d8d48ef0443f45a28e9a9f28b811549
4
+ data.tar.gz: 1bbc923e87403a718037831d9a7c6c7beeafb8192169b7897ce56ec0845c27dc
5
+ SHA512:
6
+ metadata.gz: 53052501e61f60b496c5e82cd6f0c0cca4a1310a6093146c07f719556cd2005a260042de0dd05a289e0230e41e83d3b02195430134c1706d91a5c105bde35f09
7
+ data.tar.gz: 9b34d71773c57d358ca7322ecdaf9948a676a42eca564245dc891f9d4a9bc2f94b731350e59d5d952aeedf7b45521bf2a153159c6ced6ccad8d1c1aff18cb97f
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022 Andrew Culver
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # BulletTrain
2
+ Short description and motivation.
3
+
4
+ ## Usage
5
+ How to use my plugin.
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem "bullet_train"
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+ ```bash
21
+ $ gem install bullet_train
22
+ ```
23
+
24
+ ## Contributing
25
+ Contribution directions go here.
26
+
27
+ ## License
28
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
File without changes
@@ -0,0 +1,146 @@
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
146
+ end
@@ -0,0 +1,83 @@
1
+ class Account::Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignmentsController < Account::ApplicationController
2
+ account_load_and_authorize_resource :scaffolding_completely_concrete_tangible_things_reassignment, through: :membership, through_association: :reassignments_scaffolding_completely_concrete_tangible_things_reassignments
3
+
4
+ # GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments
5
+ # GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.json
6
+ def index
7
+ # if you only want these objects shown on their parent's show page, uncomment this:
8
+ redirect_to [:account, @membership]
9
+ end
10
+
11
+ # GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
12
+ # GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
13
+ def show
14
+ end
15
+
16
+ # GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new
17
+ def new
18
+ end
19
+
20
+ # GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id/edit
21
+ def edit
22
+ end
23
+
24
+ # POST /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments
25
+ # POST /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.json
26
+ def create
27
+ respond_to do |format|
28
+ if @scaffolding_completely_concrete_tangible_things_reassignment.save
29
+ format.html { redirect_to [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.created") }
30
+ format.json { render :show, status: :created, location: [:account, @scaffolding_completely_concrete_tangible_things_reassignment] }
31
+ else
32
+ format.html { render :new, status: :unprocessable_entity }
33
+ format.json { render json: @scaffolding_completely_concrete_tangible_things_reassignment.errors, status: :unprocessable_entity }
34
+ end
35
+ end
36
+ end
37
+
38
+ # PATCH/PUT /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
39
+ # PATCH/PUT /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
40
+ def update
41
+ respond_to do |format|
42
+ if @scaffolding_completely_concrete_tangible_things_reassignment.update(scaffolding_completely_concrete_tangible_things_reassignment_params)
43
+ format.html { redirect_to [:account, @scaffolding_completely_concrete_tangible_things_reassignment], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.updated") }
44
+ format.json { render :show, status: :ok, location: [:account, @scaffolding_completely_concrete_tangible_things_reassignment] }
45
+ else
46
+ format.html { render :edit, status: :unprocessable_entity }
47
+ format.json { render json: @scaffolding_completely_concrete_tangible_things_reassignment.errors, status: :unprocessable_entity }
48
+ end
49
+ end
50
+ end
51
+
52
+ # DELETE /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
53
+ # DELETE /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
54
+ def destroy
55
+ @scaffolding_completely_concrete_tangible_things_reassignment.destroy
56
+ respond_to do |format|
57
+ format.html { redirect_to [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.destroyed") }
58
+ format.json { head :no_content }
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ # Never trust parameters from the scary internet, only allow the white list through.
65
+ def scaffolding_completely_concrete_tangible_things_reassignment_params
66
+ strong_params = params.require(:memberships_reassignments_scaffolding_completely_concrete_tangible_things_reassignment).permit(
67
+ # 🚅 super scaffolding will insert new fields above this line.
68
+ # 🚅 super scaffolding will insert new arrays above this line.
69
+ membership_ids: [],
70
+ )
71
+
72
+ strong_params[:membership_ids] = create_models_if_new(strong_params[:membership_ids]) do |email|
73
+ # stub out a new membership and invitation with no special permissions.
74
+ membership = current_team.memberships.create(user_email: email, added_by: current_membership)
75
+ current_team.invitations.create(email: email, from_membership: current_membership, membership: membership)
76
+ membership
77
+ end
78
+
79
+ # 🚅 super scaffolding will insert processing for new fields above this line.
80
+
81
+ strong_params
82
+ end
83
+ end
@@ -0,0 +1,132 @@
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
132
+ end
@@ -0,0 +1,63 @@
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
63
+ end
@@ -0,0 +1,65 @@
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
65
+ end
@@ -0,0 +1,122 @@
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
122
+ end