lesli_shield 1.1.5 → 1.1.7

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -0
  3. data/app/assets/stylesheets/lesli_shield/application.css +140 -1
  4. data/app/assets/stylesheets/lesli_shield/confirmations.css +917 -18
  5. data/app/assets/stylesheets/lesli_shield/confirmations.tailwind.css +918 -0
  6. data/app/assets/stylesheets/lesli_shield/passwords.css +872 -18
  7. data/app/assets/stylesheets/lesli_shield/passwords.tailwind.css +873 -0
  8. data/app/assets/stylesheets/lesli_shield/registrations.css +904 -770
  9. data/app/assets/stylesheets/lesli_shield/registrations.tailwind.css +908 -0
  10. data/app/assets/stylesheets/lesli_shield/sessions.css +984 -32
  11. data/app/assets/stylesheets/lesli_shield/sessions.tailwind.css +983 -1
  12. data/app/controllers/lesli_shield/profile/passwords_controller.rb +119 -0
  13. data/app/controllers/lesli_shield/profiles_controller.rb +63 -0
  14. data/app/controllers/lesli_shield/sessions_controller.rb +1 -2
  15. data/app/controllers/lesli_shield/users_controller.rb +3 -7
  16. data/app/controllers/users/confirmations_controller.rb +162 -49
  17. data/app/controllers/users/passwords_controller.rb +211 -28
  18. data/app/controllers/users/registrations_controller.rb +108 -83
  19. data/app/controllers/users/sessions_controller.rb +122 -84
  20. data/app/interfaces/lesli_shield/authentication_interface.rb +3 -3
  21. data/app/interfaces/lesli_shield/authorization_interface.rb +2 -2
  22. data/app/mailers/lesli_shield/devise_mailer.rb +73 -17
  23. data/app/models/concerns/lesli_shield/user_authentication.rb +54 -0
  24. data/app/models/lesli_shield/account.rb +4 -1
  25. data/app/models/lesli_shield/invite.rb +1 -1
  26. data/app/models/lesli_shield/user/role.rb +1 -1
  27. data/app/models/lesli_shield/user.rb +5 -2
  28. data/app/services/lesli_shield/profile/password_service.rb +47 -0
  29. data/app/services/lesli_shield/profile_service.rb +24 -0
  30. data/app/services/lesli_shield/user_registration_service.rb +41 -74
  31. data/app/services/lesli_shield/user_session_service.rb +1 -1
  32. data/app/services/lesli_shield/user_validator_service.rb +35 -39
  33. data/app/views/devise/confirmations/new.html.erb +58 -12
  34. data/app/views/devise/confirmations/show.html.erb +27 -60
  35. data/app/views/devise/passwords/edit.html.erb +59 -25
  36. data/app/views/devise/passwords/new.html.erb +61 -26
  37. data/app/views/devise/registrations/new.html.erb +80 -53
  38. data/app/views/devise/sessions/new.html.erb +53 -36
  39. data/app/views/devise/shared/_application-devise-simple.erb +18 -19
  40. data/app/views/devise/shared/_demo.html.erb +8 -6
  41. data/app/views/devise/shared/_error_messages.html.erb +45 -10
  42. data/app/views/devise/shared/_links.html.erb +6 -13
  43. data/app/views/lesli_shield/invites/_form.html.erb +1 -1
  44. data/app/views/lesli_shield/invites/index.html.erb +3 -2
  45. data/app/views/lesli_shield/profiles/_password-form.html.erb +68 -0
  46. data/app/views/lesli_shield/profiles/show.html.erb +29 -0
  47. data/app/views/lesli_shield/users/_information-card.html.erb +35 -45
  48. data/app/views/lesli_shield/users/_information-form.html.erb +9 -1
  49. data/config/initializers/devise.rb +52 -328
  50. data/config/locales/translations.en.yml +65 -2
  51. data/config/locales/translations.es.yml +65 -2
  52. data/config/routes.rb +4 -0
  53. data/lib/lesli_shield/engine.rb +44 -4
  54. data/lib/lesli_shield/router.rb +52 -18
  55. data/lib/lesli_shield/version.rb +2 -2
  56. data/readme.md +6 -4
  57. metadata +30 -5
  58. data/app/assets/config/lesli_shield_manifest.js +0 -48
  59. data/app/assets/stylesheets/lesli_shield/application.tailwind.css +0 -0
@@ -0,0 +1,119 @@
1
+ module LesliShield
2
+ module Profile
3
+ class PasswordsController < ApplicationController
4
+ PASSWORD_CHANGE_RATE_LIMIT_WINDOW = 15.minutes
5
+
6
+ skip_before_action :authorize_request
7
+
8
+ rate_limit(
9
+ to: 10,
10
+ within: PASSWORD_CHANGE_RATE_LIMIT_WINDOW,
11
+ by: -> { current_user&.id || request.remote_ip },
12
+ with: :respond_to_password_change_rate_limit,
13
+ only: :update,
14
+ name: "profile-password-update"
15
+ )
16
+
17
+ def update
18
+ @active_profile_tab = "security"
19
+ @password = PasswordService
20
+ .new(current_user)
21
+ .update(
22
+ password_params,
23
+ current_session_id: session[:user_session_id]
24
+ )
25
+ @user = @password.result
26
+
27
+ if @password.successful?
28
+ bypass_sign_in(@user)
29
+ record_password_event(
30
+ operation: "password_change_succeeded",
31
+ description: "Password changed successfully"
32
+ )
33
+
34
+ respond_with_lesli(
35
+ turbo: [
36
+ stream_notification_success(success_message),
37
+ stream_redirection(profile_path(tab: "security"))
38
+ ],
39
+ json: { message: success_message },
40
+ redirect: profile_path(tab: "security")
41
+ )
42
+ else
43
+ record_password_event(
44
+ operation: "password_change_failed",
45
+ description: "Password change failed validation"
46
+ )
47
+
48
+ respond_with_lesli(
49
+ turbo: [
50
+ stream_notification_danger(@password.errors_as_sentence),
51
+ turbo_stream.replace(
52
+ "shield-profile-password-form",
53
+ partial: "lesli_shield/profiles/password-form",
54
+ locals: { user: @user }
55
+ )
56
+ ],
57
+ json: { errors: @password.errors },
58
+ html: "lesli_shield/profiles/show",
59
+ status: :unprocessable_content
60
+ )
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def password_params
67
+ params.require(:user).permit(
68
+ :current_password,
69
+ :password,
70
+ :password_confirmation
71
+ )
72
+ end
73
+
74
+ def success_message
75
+ I18n.t("lesli_shield.profiles.password_updated")
76
+ end
77
+
78
+ def record_password_event(operation:, description:)
79
+ log(operation: operation, description: description)
80
+ rescue StandardError => exception
81
+ Rails.error.report(
82
+ exception,
83
+ handled: true,
84
+ context: {
85
+ controller: self.class.name,
86
+ operation: operation
87
+ }
88
+ )
89
+ end
90
+
91
+ def respond_to_password_change_rate_limit
92
+ @active_profile_tab = "security"
93
+ @user = current_user
94
+ message = I18n.t(
95
+ "lesli_shield.profiles.password_too_many_attempts"
96
+ )
97
+
98
+ response.set_header(
99
+ "Retry-After",
100
+ PASSWORD_CHANGE_RATE_LIMIT_WINDOW.to_i.to_s
101
+ )
102
+
103
+ respond_with_lesli(
104
+ turbo: [
105
+ stream_notification_danger(message),
106
+ turbo_stream.replace(
107
+ "shield-profile-password-form",
108
+ partial: "lesli_shield/profiles/password-form",
109
+ locals: { user: @user }
110
+ )
111
+ ],
112
+ json: { error: message },
113
+ html: "lesli_shield/profiles/show",
114
+ status: :too_many_requests
115
+ )
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,63 @@
1
+ module LesliShield
2
+ class ProfilesController < ApplicationController
3
+ PROFILE_FIELDS = %i[
4
+ id
5
+ uid
6
+ alias
7
+ title
8
+ email
9
+ first_name
10
+ last_name
11
+ telephone
12
+ ].freeze
13
+
14
+ skip_before_action :authorize_request
15
+ before_action :set_profile
16
+
17
+ def show
18
+ respond_with_lesli(json: profile_payload)
19
+ end
20
+
21
+ def update
22
+ @profile.update(profile_params)
23
+ @user = @profile.result
24
+
25
+ if @profile.successful?
26
+ respond_with_lesli(
27
+ turbo: stream_notification_success("Profile updated successfully!"),
28
+ json: profile_payload,
29
+ redirect: profile_path
30
+ )
31
+ else
32
+ respond_with_lesli(
33
+ turbo: stream_notification_danger(@profile.errors_as_sentence),
34
+ json: { errors: @profile.errors },
35
+ html: :show,
36
+ status: :unprocessable_content
37
+ )
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def set_profile
44
+ @profile = LesliShield::ProfileService.new(current_user)
45
+ @user = @profile.show
46
+ end
47
+
48
+ def profile_params
49
+ params.require(:user).permit(
50
+ :alias,
51
+ :title,
52
+ :email,
53
+ :first_name,
54
+ :last_name,
55
+ :telephone
56
+ )
57
+ end
58
+
59
+ def profile_payload
60
+ @user.as_json(only: PROFILE_FIELDS)
61
+ end
62
+ end
63
+ end
@@ -6,8 +6,7 @@ module LesliShield
6
6
  def index
7
7
  @sessions = respond_with_pagination(UserSessionService.new(current_user, query).index())
8
8
  respond_with_lesli(
9
- :html => @sessions,
10
- :json => @sessions
9
+ json: @sessions
11
10
  )
12
11
  end
13
12
 
@@ -41,13 +41,9 @@ module LesliShield
41
41
 
42
42
  # GET /users/1
43
43
  def show
44
- @user_roles = @user.result.roles
45
- @activities = @user.result.logs
46
- .order(id: :desc)
47
- .select(:id, :operation, :description, Date2.new.db_column('created_at', as:'date'))
48
- .limit(7)
49
- .as_json
50
- @sessions = @user.result.sessions
44
+ @user_roles = @user.roles
45
+ @activities = @user.logs
46
+ @sessions = @user.sessions
51
47
  @user = @user.show
52
48
  end
53
49
 
@@ -29,73 +29,186 @@ Building a better future, one line of code at a time.
29
29
  @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
30
30
 
31
31
  // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
32
- // ·
32
+ // ·
33
33
  =end
34
- class Users::ConfirmationsController < Devise::ConfirmationsController
35
34
 
35
+ class Users::ConfirmationsController < Devise::ConfirmationsController
36
+ CONFIRMATION_RATE_LIMIT_WINDOW = 1.hour
37
+
38
+ rate_limit(
39
+ to: 10,
40
+ within: CONFIRMATION_RATE_LIMIT_WINDOW,
41
+ by: -> { request.remote_ip },
42
+ with: :respond_to_confirmation_rate_limit,
43
+ only: :create,
44
+ name: "confirmation-request-ip"
45
+ )
46
+
47
+ rate_limit(
48
+ to: 3,
49
+ within: CONFIRMATION_RATE_LIMIT_WINDOW,
50
+ by: :confirmation_email_rate_limit_key,
51
+ with: :respond_to_confirmation_rate_limit,
52
+ only: :create,
53
+ name: "confirmation-request-email"
54
+ )
55
+
56
+ before_action :protect_confirmation_token, only: :show
57
+
58
+ # Devise validates and consumes the raw token. Account provisioning and the
59
+ # successful audit event share the same transaction so a partial account can
60
+ # never be left behind and a provisioning failure leaves the token reusable.
36
61
  def show
62
+ self.resource = confirm_user_and_provision_account
63
+
64
+ if resource.errors.empty?
65
+ send_welcome_email_safely(resource)
66
+ set_flash_message!(:notice, :confirmed)
67
+ respond_with_navigational(resource) do
68
+ redirect_to after_confirmation_path_for(resource_name, resource)
69
+ end
70
+ else
71
+ respond_with_navigational(
72
+ resource.errors,
73
+ status: :unprocessable_content
74
+ ) do
75
+ render :new, status: :unprocessable_content
76
+ end
77
+ end
78
+ end
79
+
80
+ protected
81
+
82
+ # Keep the user on the resend page so Devise's generic paranoid-mode notice
83
+ # is visible without disclosing whether the email exists or is confirmed.
84
+ def after_resending_confirmation_instructions_path_for(_resource_name)
85
+ new_user_confirmation_path
86
+ end
87
+
88
+ private
89
+
90
+ def confirm_user_and_provision_account
91
+ confirmed_user = nil
37
92
 
38
- # delete all previus messages
39
- flash.clear
93
+ resource_class.transaction do
94
+ confirmed_user = resource_class.confirm_by_token(
95
+ params[:confirmation_token]
96
+ )
40
97
 
41
- # get the confirmation token sent through get params
42
- token = params[:confirmation_token]
98
+ if confirmed_user.errors.empty?
99
+ LesliShield::UserRegistrationService
100
+ .new(confirmed_user)
101
+ .provision_account!
43
102
 
44
- # validate that token were sent
45
- if token.blank?
46
- return flash[:danger] = I18n.t("core.users/confirmations.messages_warning_invalid_token")
103
+ confirmed_user.update!(confirmation_token: nil)
104
+ record_successful_confirmation!(confirmed_user)
105
+ end
47
106
  end
48
107
 
49
- # check if token belongs to a unconfirmed user
50
- user = Lesli::User.find_by(:confirmation_token => token, :confirmed_at => nil)
108
+ confirmed_user
109
+ rescue StandardError => exception
110
+ Rails.error.report(
111
+ exception,
112
+ handled: true,
113
+ context: {
114
+ controller: self.class.name,
115
+ operation: "user_confirmation_succeeded"
116
+ }
117
+ )
118
+
119
+ confirmation_failure_resource(confirmed_user)
120
+ end
51
121
 
52
- # validate that user were found
53
- if user.blank?
54
- return flash[:danger] = I18n.t("core.users/confirmations.messages_warning_invalid_token")
122
+ def confirmation_failure_resource(user)
123
+ failed_resource = if user&.persisted?
124
+ resource_class.find_by(id: user.id)
55
125
  end
56
126
 
57
- # register a log with a validation atempt for the user
58
- log = user.log(engine: LesliShield, source: self.class.name, action: action_name, operation: "user_confirmation", description: "Confirmation process started")
59
-
60
- # create a new instance of the registration service
61
- registration_service = LesliShield::UserRegistrationService.new(user)
127
+ failed_resource ||= resource_class.new
128
+ failed_resource.errors.add(
129
+ :base,
130
+ I18n.t(
131
+ "lesli_shield.devise/confirmations.message_confirmation_failed"
132
+ )
133
+ )
134
+ failed_resource
135
+ end
62
136
 
63
- # confirm the user
64
- registration_service.confirm
137
+ def record_successful_confirmation!(user)
138
+ user.log(
139
+ engine: LesliShield,
140
+ source: self.class.name,
141
+ action: action_name,
142
+ operation: "user_confirmation_succeeded",
143
+ description: "User confirmed successfully"
144
+ )
145
+ end
65
146
 
66
- # send a welcome email to user as is confirmed
67
- LesliShield::DeviseMailer.with(user: resource).welcome.deliver_later
147
+ def send_welcome_email_safely(user)
148
+ LesliShield::DeviseMailer.welcome(user).deliver_later
149
+ rescue StandardError => exception
150
+ Rails.error.report(
151
+ exception,
152
+ handled: true,
153
+ context: {
154
+ controller: self.class.name,
155
+ operation: "welcome_email_delivery"
156
+ }
157
+ )
158
+ end
68
159
 
69
- # let the user knows that the confirmation is done
70
- flash[:success] = I18n.t("core.users/confirmations.messages_success_email_updated")
71
-
72
- # setup the new account
73
- registration_service.create_account if user.account.blank?
160
+ def confirmation_email_rate_limit_key
161
+ normalized_email = confirmation_request_params[:email]
162
+ .to_s
163
+ .strip
164
+ .downcase
165
+ identity = normalized_email.presence || "blank:#{request.remote_ip}"
166
+
167
+ OpenSSL::HMAC.hexdigest(
168
+ "SHA256",
169
+ Rails.application.secret_key_base,
170
+ identity
171
+ )
172
+ end
74
173
 
75
- log.update(description: 'User confirmed successfully') if defined?(LesliAudit)
174
+ def confirmation_request_params
175
+ params
176
+ .fetch(resource_name, ActionController::Parameters.new)
177
+ .permit(:email)
76
178
  end
77
179
 
78
-
79
- # @controller_action_param :email [String] The registered user email
80
- # @return [Json] Json that contains wheter the email confirmation was sent or not.
81
- # If it is not successful, it returs an error message
82
- # @description Resends a email confirmation an already registered user
83
- # @example
84
- # # Executing this controller's action from javascript's frontend
85
- # let email = 'john.doe@email.com';
86
- # let data = {
87
- # user: {
88
- # email: email
89
- # }
90
- # };
91
- # this.http.post('127.0.0.1/conformation', data);
92
- def create
93
- super do |resource|
94
- if successfully_sent?(resource)
95
- return respond_with_successful
96
- else
97
- return respond_with_error(resource.errors.full_messages.to_sentence)
180
+ def respond_to_confirmation_rate_limit
181
+ message = I18n.t(
182
+ "lesli_shield.devise/confirmations.message_too_many_attempts"
183
+ )
184
+
185
+ response.set_header(
186
+ "Retry-After",
187
+ CONFIRMATION_RATE_LIMIT_WINDOW.to_i.to_s
188
+ )
189
+
190
+ respond_to do |format|
191
+ format.html { render_confirmation_rate_limit(message) }
192
+ format.json do
193
+ render json: { error: message }, status: :too_many_requests
98
194
  end
195
+ format.any { head :too_many_requests }
99
196
  end
100
197
  end
198
+
199
+ def render_confirmation_rate_limit(message)
200
+ flash.now[:alert] = message
201
+ self.resource = resource_class.new(
202
+ email: confirmation_request_params[:email]
203
+ )
204
+ render :new, status: :too_many_requests
205
+ end
206
+
207
+ def protect_confirmation_token
208
+ # Prevent the token in the query string from being sent to third-party
209
+ # origins while keeping a valid Origin for the resend form rendered when
210
+ # Devise rejects an invalid or expired token.
211
+ response.set_header("Referrer-Policy", "same-origin")
212
+ response.set_header("Cache-Control", "no-store")
213
+ end
101
214
  end