lesli_shield 1.1.6 → 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 (56) 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 -832
  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/users/confirmations_controller.rb +162 -49
  16. data/app/controllers/users/passwords_controller.rb +211 -28
  17. data/app/controllers/users/registrations_controller.rb +108 -83
  18. data/app/controllers/users/sessions_controller.rb +122 -84
  19. data/app/interfaces/lesli_shield/authorization_interface.rb +2 -2
  20. data/app/mailers/lesli_shield/devise_mailer.rb +73 -17
  21. data/app/models/concerns/lesli_shield/user_authentication.rb +54 -0
  22. data/app/models/lesli_shield/account.rb +4 -1
  23. data/app/models/lesli_shield/invite.rb +1 -1
  24. data/app/models/lesli_shield/user/role.rb +1 -1
  25. data/app/models/lesli_shield/user.rb +5 -2
  26. data/app/services/lesli_shield/profile/password_service.rb +47 -0
  27. data/app/services/lesli_shield/profile_service.rb +24 -0
  28. data/app/services/lesli_shield/user_registration_service.rb +41 -74
  29. data/app/services/lesli_shield/user_session_service.rb +1 -1
  30. data/app/services/lesli_shield/user_validator_service.rb +30 -32
  31. data/app/views/devise/confirmations/new.html.erb +58 -12
  32. data/app/views/devise/confirmations/show.html.erb +27 -60
  33. data/app/views/devise/passwords/edit.html.erb +59 -25
  34. data/app/views/devise/passwords/new.html.erb +61 -26
  35. data/app/views/devise/registrations/new.html.erb +80 -53
  36. data/app/views/devise/sessions/new.html.erb +53 -36
  37. data/app/views/devise/shared/_application-devise-simple.erb +18 -19
  38. data/app/views/devise/shared/_demo.html.erb +8 -6
  39. data/app/views/devise/shared/_error_messages.html.erb +45 -10
  40. data/app/views/devise/shared/_links.html.erb +6 -13
  41. data/app/views/lesli_shield/invites/_form.html.erb +1 -1
  42. data/app/views/lesli_shield/invites/index.html.erb +3 -2
  43. data/app/views/lesli_shield/profiles/_password-form.html.erb +68 -0
  44. data/app/views/lesli_shield/profiles/show.html.erb +29 -0
  45. data/app/views/lesli_shield/users/_information-form.html.erb +9 -1
  46. data/config/initializers/devise.rb +52 -328
  47. data/config/locales/translations.en.yml +65 -2
  48. data/config/locales/translations.es.yml +65 -2
  49. data/config/routes.rb +4 -0
  50. data/lib/lesli_shield/engine.rb +38 -4
  51. data/lib/lesli_shield/router.rb +52 -18
  52. data/lib/lesli_shield/version.rb +2 -2
  53. data/readme.md +4 -2
  54. metadata +30 -5
  55. data/app/assets/config/lesli_shield_manifest.js +0 -48
  56. data/app/assets/stylesheets/lesli_shield/application.tailwind.css +0 -2
@@ -32,7 +32,10 @@ Building a better future, one line of code at a time.
32
32
 
33
33
  module LesliShield
34
34
  class Account < ApplicationRecord
35
- belongs_to :account, class_name: "Lesli::Account"
35
+ belongs_to :account,
36
+ class_name: "Lesli::Account",
37
+ inverse_of: :shield
38
+ validates :account_id, uniqueness: true
36
39
  has_many :dashboards
37
40
  has_many :invites
38
41
 
@@ -1,6 +1,6 @@
1
1
  module LesliShield
2
2
  class Invite < ApplicationRecord
3
- belongs_to :account
3
+ belongs_to :account, class_name: "LesliShield::Account"
4
4
  belongs_to :user, class_name: "Lesli::User"
5
5
 
6
6
  validates :email, presence: true, on: :create
@@ -1,7 +1,7 @@
1
1
  module LesliShield
2
2
  class User::Role < ApplicationRecord
3
3
  self.table_name = 'lesli_shield_user_roles'
4
- belongs_to :user
4
+ belongs_to :user, class_name: "Lesli::User"
5
5
  belongs_to :role, class_name: "Lesli::Role"
6
6
  has_many :roles
7
7
  end
@@ -1,5 +1,8 @@
1
1
  module LesliShield
2
- class User < ::Lesli::User
3
- #has_many :activities, class_name: "Lesli::Item::Activity"
2
+ # Namespace for Shield-owned records associated with Lesli::User, such as
3
+ # User::Role and User::Session. The authenticatable model remains
4
+ # Lesli::User; defining a second ActiveRecord subclass for the same table
5
+ # causes association type mismatches and can confuse Devise mappings.
6
+ module User
4
7
  end
5
8
  end
@@ -0,0 +1,47 @@
1
+ module LesliShield
2
+ module Profile
3
+ class PasswordService < Lesli::ApplicationLesliService
4
+ def initialize(user)
5
+ super(user)
6
+ self.resource = user
7
+ end
8
+
9
+ def update(params, current_session_id: nil)
10
+ password_updated = false
11
+
12
+ resource.transaction do
13
+ password_updated = resource.update_with_password(params)
14
+
15
+ if password_updated
16
+ clear_password_expiration
17
+ revoke_other_sessions(current_session_id)
18
+ end
19
+ end
20
+
21
+ unless password_updated
22
+ resource.errors.full_messages.each { |message| error(message) }
23
+ end
24
+
25
+ self
26
+ end
27
+
28
+ private
29
+
30
+ def clear_password_expiration
31
+ return unless resource.has_attribute?(:password_expiration_at)
32
+ return if resource.password_expiration_at.nil?
33
+
34
+ resource.update_column(:password_expiration_at, nil)
35
+ end
36
+
37
+ def revoke_other_sessions(current_session_id)
38
+ return if current_session_id.blank?
39
+
40
+ current_session = resource.sessions.find_by(id: current_session_id)
41
+ return unless current_session
42
+
43
+ resource.sessions.where.not(id: current_session.id).destroy_all
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,24 @@
1
+ module LesliShield
2
+ class ProfileService < Lesli::ApplicationLesliService
3
+ def initialize(user)
4
+ super(user)
5
+ self.resource = user
6
+ end
7
+
8
+ def show
9
+ resource
10
+ end
11
+
12
+ def update(params)
13
+ resource.assign_attributes(params)
14
+ resource.alias = resource.full_name_initials if resource.alias.blank?
15
+
16
+ unless resource.save
17
+ error(resource.errors.full_messages.to_sentence)
18
+ return self
19
+ end
20
+
21
+ self
22
+ end
23
+ end
24
+ end
@@ -33,96 +33,63 @@ Building a better future, one line of code at a time.
33
33
  module LesliShield
34
34
  class UserRegistrationService < Lesli::ApplicationLesliService
35
35
 
36
- def initialize(current_user)
37
- @resource = current_user
38
- @current_user = current_user
36
+ def initialize(user)
37
+ super(user)
38
+ self.resource = user
39
39
  end
40
40
 
41
- def confirm
42
-
43
- # confirm the user
44
- current_user.confirm
45
-
46
-
47
- # force token deletion so we are sure nobody
48
- # will be able to use the token again
49
- current_user.update(confirmation_token: nil)
50
-
51
- # Minimum security settings required
52
- #self.settings.create_with(:value => false).find_or_create_by(:name => "mfa_enabled")
53
- #self.settings.create_with(:value => :email).find_or_create_by(:name => "mfa_method")
41
+ # Devise owns confirmation. This service performs only the Lesli account
42
+ # provisioning that must happen after Devise accepts the token.
43
+ def provision_account!
44
+ assign_account! if resource.account.blank?
45
+ assign_initial_role! unless resource.user_roles.exists?
46
+ synchronize_role_privileges!
54
47
 
48
+ resource
55
49
  end
56
50
 
57
- def create_account
58
-
59
- add_account() if resource.account.blank?
60
- add_role() if resource.user_roles.empty?
51
+ private
61
52
 
62
- # Synchronize roles for the very first time
63
- resource.roles.each do |role|
64
- LesliShield::RolePrivilegeService.new(nil).synchronize(role)
53
+ def assign_account!
54
+ account = if multiaccount?
55
+ create_owned_account!
56
+ else
57
+ Lesli::Account.first!
65
58
  end
66
59
 
67
- # update user :)
68
- resource.save
69
-
70
- # # initialize user dependencies
71
- # def after_account_assignation
72
- # return unless self.account
73
-
74
- # #Courier::One::Firebase::User.sync_user(self)
75
- # # Lesli::Courier.new(:lesli_calendar).from(:calendar_service, self).create({
76
- # # name: "Personal Calendar",
77
- # # default: true
78
- # # })
79
- # end
80
-
60
+ resource.update!(account: account)
81
61
  end
82
62
 
83
- private
84
-
85
- # check if instance is for multi-account
86
- ALLOW_MULTI_ACCOUNT = Lesli.config.security.dig(:allow_multiaccount)
87
-
88
- # This is necessary when user is creating a brand new account
89
- # and the platform is configured to create new Lesli accounts for every user
90
- # The platform use the same account when the platform is not saas but
91
- # proprietary instance (only one company is using the platform)
92
- # Another scenario where it is not needed to create a new account for the
93
- # user is when the user is confirming the account through an invitation
94
- def add_account
95
-
96
- if ALLOW_MULTI_ACCOUNT === true
97
- # create new account for the new user only if multi-account is allowed
98
- account = Lesli::Account.create!({
99
- user: resource, # set user as owner of his just created account
100
- name: "Lesli", # temporary company name
101
- email: resource.email,
102
- status: :active # account is active due user already confirmed his email
103
- })
104
- else
105
- # if multi-account is not allowed user belongs to the first account in instance
106
- account = Lesli::Account.first
107
- end
63
+ def create_owned_account!
64
+ Lesli::Account.create!(
65
+ user: resource,
66
+ name: "Lesli",
67
+ email: resource.email,
68
+ status: :active
69
+ )
70
+ end
108
71
 
109
- # add user to his own account
110
- resource.account = account
72
+ def assign_initial_role!
73
+ role = resource.account.roles.find_by!(name: initial_role_name)
74
+ resource.user_roles.create!(role: role)
111
75
  end
112
76
 
113
- def add_role
114
- if ALLOW_MULTI_ACCOUNT == true
115
- # add owner role to user only if multi-account is allowed
116
- resource.user_roles.create({ role: account.roles.find_by(name: "owner") })
117
- else
118
- # Assigning default role if defined in account settings
119
- # Otherwise, the default role is "limited"
120
- default_role_name = Lesli.config.shield.dig(:default_role) || "guest"
77
+ def initial_role_name
78
+ return "owner" if multiaccount?
121
79
 
122
- default_role = resource.account.roles.find_by(name: default_role_name)
80
+ Lesli.config.shield.dig(:default_role).presence || "guest"
81
+ end
123
82
 
124
- resource.user_roles.create({ role: default_role })
83
+ def synchronize_role_privileges!
84
+ resource.roles.find_each do |role|
85
+ LesliShield::RolePrivilegeService.new(nil).synchronize(role)
125
86
  end
126
87
  end
88
+
89
+ # Read the setting at execution time. Applications and tests can change
90
+ # Lesli configuration after this service class has been loaded.
91
+ def multiaccount?
92
+ Lesli.config.security.dig(:allow_multiaccount) == true
93
+ end
127
94
  end
128
95
  end
@@ -55,7 +55,7 @@ module LesliShield
55
55
  def create(remote_ip, user_agent=nil, session_source="devise_standard_session")
56
56
 
57
57
  # register a new unique session
58
- current_session = current_user.sessions.create({
58
+ current_session = current_user.sessions.create!({
59
59
  :remote => remote_ip,
60
60
 
61
61
  :agent_os => user_agent&.dig(:os) || "unknown",
@@ -33,11 +33,17 @@ Building a better future, one line of code at a time.
33
33
  module LesliShield
34
34
  class UserValidatorService < Lesli::ApplicationLesliService
35
35
 
36
- def validate
36
+ def initialize(user)
37
+ raise ArgumentError, "user is required" unless user
38
+
39
+ super(user)
40
+ end
41
+
42
+ def validate
43
+ account_present?()
37
44
  active?()
38
- confirmed?()
39
- roles_empty?()
40
- active_roles?()
45
+ active_roles?() unless roles_empty?()
46
+ self
41
47
  end
42
48
 
43
49
 
@@ -69,10 +75,10 @@ module LesliShield
69
75
  password_settings = []
70
76
 
71
77
  # get password settings from the database
72
- if @resource.account
78
+ if current_user.account
73
79
 
74
80
  # if user is already registered
75
- password_settings = @resource.account.settings.where(password_values.join(" or "))
81
+ password_settings = current_user.account.settings.where(password_values.join(" or "))
76
82
 
77
83
  elseif Account.first
78
84
 
@@ -85,7 +91,7 @@ module LesliShield
85
91
 
86
92
  # exit validations if password complexity is not enabled
87
93
  if settings[:name] == 'password_enforce_complexity' && settings[:value] != "1"
88
- failures = []
94
+ failures.clear
89
95
  break
90
96
  end
91
97
 
@@ -147,42 +153,40 @@ module LesliShield
147
153
 
148
154
  private
149
155
 
150
-
151
- # check if user is able to create a new session
152
- def active?
153
-
154
- unless @resource.active? && @resource.locked_until != nil
155
-
156
- # save a locked log for the requested user
157
- log_failure("user_locked")
156
+ # Devise owns password, confirmation, and failed-attempt locking. This
157
+ # service contains only the additional access rules defined by Shield.
158
+ def account_present?
159
+ unless current_user.account.present?
160
+ log_failure("user_has_no_account")
158
161
 
159
162
  return false
160
-
161
163
  end
162
164
 
163
- return true
165
+ true
164
166
  end
165
167
 
166
- def confirmed?
167
168
 
168
- # check if user is already confirmed
169
- unless @resource.confirmed?
169
+ # check if user is able to create a new session
170
+ def active?
171
+ temporarily_locked = current_user.locked_until.present? &&
172
+ current_user.locked_until > Time.current
170
173
 
171
- # save a invalid credentials log for the requested user
172
- log_failure("email_not_confirmed")
174
+ unless current_user.active? && !temporarily_locked
175
+
176
+ # save a locked log for the requested user
177
+ log_failure("user_locked")
173
178
 
174
179
  return false
175
180
 
176
181
  end
177
182
 
178
183
  return true
179
-
180
184
  end
181
185
 
182
186
  def roles_empty?
183
187
 
184
188
  # check if user has roles assigned
185
- if @resource.roles.empty?
189
+ unless current_user.roles.exists?
186
190
 
187
191
  log_failure("user_has_no_assigned_role")
188
192
 
@@ -197,7 +201,7 @@ module LesliShield
197
201
  def active_roles?
198
202
 
199
203
  # check user has at least one active role before authorize the sign-in request
200
- unless (@resource.roles.map {|role| role.active}.include? true)
204
+ unless current_user.roles.exists?(active: true)
201
205
  log_failure("user_has_no_active_role")
202
206
 
203
207
  return false
@@ -207,13 +211,7 @@ module LesliShield
207
211
  end
208
212
 
209
213
  def log_failure(description)
210
- @resource.log(
211
- engine: LesliShield,
212
- source: self.class.name,
213
- action: "validate",
214
- operation: "session_creation_failed",
215
- description: description
216
- )
214
+ failures.push(description) unless failures.include?(description)
217
215
  end
218
216
  end
219
217
  end
@@ -1,16 +1,62 @@
1
- <h2>Resend confirmation instructions</h2>
1
+ <%= render(
2
+ "devise/shared/application-devise-simple",
3
+ title: I18n.t("lesli_shield.devise/confirmations.view_title")
4
+ ) do %>
2
5
 
3
- <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %>
4
- <%= render "devise/shared/error_messages", resource: resource %>
6
+ <%= form_for(
7
+ resource,
8
+ as: resource_name,
9
+ url: user_confirmation_path,
10
+ html: { method: :post, class: "text-left" },
11
+ builder: LesliView::Forms::Builder
12
+ ) do |form| %>
5
13
 
6
- <div class="field">
7
- <%= f.label :email %><br />
8
- <%= f.email_field :email, autofocus: true, autocomplete: "email", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %>
9
- </div>
14
+ <p class="mb-6 text-center text-sm leading-6 text-slate-600">
15
+ <%= I18n.t(
16
+ "lesli_shield.devise/confirmations.view_description"
17
+ ) %>
18
+ </p>
10
19
 
11
- <div class="actions">
12
- <%= f.submit "Resend confirmation instructions" %>
13
- </div>
14
- <% end %>
20
+ <%= render "devise/shared/error_messages", resource: resource %>
21
+
22
+ <%= form.field_control_email(
23
+ :email,
24
+ icon: "mail",
25
+ label: I18n.t(
26
+ "lesli_shield.devise/confirmations.field_email"
27
+ ),
28
+ message: resource.errors[:email].to_sentence.presence,
29
+ category: resource.errors[:email].any? ? :danger : nil,
30
+ value: resource.pending_reconfirmation? ?
31
+ resource.unconfirmed_email : resource.email,
32
+ required: true,
33
+ autofocus: true,
34
+ autocomplete: "email",
35
+ autocapitalize: "none",
36
+ spellcheck: false
37
+ ) %>
15
38
 
16
- <%= render "devise/shared/links" %>
39
+ <%= form.field_control_button(
40
+ I18n.t(
41
+ "lesli_shield.devise/confirmations.button_send_instructions"
42
+ ),
43
+ {
44
+ class: "w-full",
45
+ data: {
46
+ turbo_submits_with: I18n.t(
47
+ "lesli_shield.devise/confirmations.button_sending"
48
+ )
49
+ }
50
+ },
51
+ icon: "mark_email_unread"
52
+ ) %>
53
+ <% end %>
54
+
55
+ <%= render(
56
+ "devise/shared/links",
57
+ login: true,
58
+ password_reset: false,
59
+ magic_link: false,
60
+ otp: false
61
+ ) %>
62
+ <% end %>
@@ -1,61 +1,28 @@
1
- <%
2
- =begin
3
-
4
- Lesli
5
-
6
- Copyright (c) 2023, Lesli Technologies, S. A.
7
-
8
- This program is free software: you can redistribute it and/or modify
9
- it under the terms of the GNU General Public License as published by
10
- the Free Software Foundation, either version 3 of the License, or
11
- (at your option) any later version.
12
-
13
- This program is distributed in the hope that it will be useful,
14
- but WITHOUT ANY WARRANTY; without even the implied warranty of
15
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
- GNU General Public License for more details.
17
-
18
- You should have received a copy of the GNU General Public License
19
- along with this program. If not, see http://www.gnu.org/licenses/.
20
-
21
- Lesli · Ruby on Rails SaaS Development Framework.
22
-
23
- Made with ♥ by https://www.lesli.tech
24
- Building a better future, one line of code at a time.
25
-
26
- @contact hello@lesli.tech
27
- @website https://www.lesli.tech
28
- @license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
29
-
30
- // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
31
- // ·
32
- =end
33
- %>
34
-
35
- <% content_for :application_custom_assets do %>
36
- <script>
37
- //setTimeout(() => { window.location.href = "/"; }, 10000)
38
- </script>
39
- <% end %>
40
-
41
- <%= render("devise/shared/application-devise-simple", title:"Account confirmation") do %>
42
-
43
- <% if flash[:danger] %>
44
- <% flash.each do |type, msg| %>
45
- <div class="notification is-<%= type %>">
46
- <%= msg %>
47
- </div>
48
- <% end %>
49
- <% end %>
50
-
51
- <% if flash[:success] %>
52
- <div class="confirmation-message">
53
- <p>Your account has been successfully verified</p>
54
- </div>
55
- <% end %>
56
-
57
- <%= render("devise/shared/links", login:true) %>
58
-
1
+ <%= render(
2
+ "devise/shared/application-devise-simple",
3
+ title: I18n.t("lesli_shield.devise/confirmations.view_success_title")
4
+ ) do %>
5
+ <section
6
+ class="my-8 w-full max-w-lg rounded-xl border border-emerald-200 bg-white p-8 text-center shadow-sm"
7
+ role="status"
8
+ >
9
+ <span
10
+ class="material-symbols-outlined text-5xl text-emerald-600"
11
+ aria-hidden="true"
12
+ >
13
+ verified
14
+ </span>
15
+
16
+ <p class="mt-4 text-sm leading-6 text-slate-600">
17
+ <%= I18n.t(
18
+ "lesli_shield.devise/confirmations.view_success_description"
19
+ ) %>
20
+ </p>
21
+
22
+ <%= link_to(
23
+ I18n.t("lesli_shield.devise/shared.button_login"),
24
+ new_user_session_path,
25
+ class: "mt-6 inline-flex min-h-11 items-center justify-center rounded-lg bg-[var(--lesli-color-primary)] px-5 py-2.5 text-sm font-semibold text-white transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--lesli-color-primary)]"
26
+ ) %>
27
+ </section>
59
28
  <% end %>
60
-
61
-
@@ -1,36 +1,70 @@
1
- <%= render("devise/shared/application-devise-simple", title:"Password update") do %>
1
+ <%= render(
2
+ "devise/shared/application-devise-simple",
3
+ title: I18n.t("lesli_shield.devise/passwords.view_update_title")
4
+ ) do %>
2
5
 
3
6
  <%= form_for(
4
- resource,
5
- as: resource_name,
6
- url: user_password_path(reset_password_token: params[:reset_password_token]),
7
- html: { class: "has-text-centered", method: :put },
7
+ resource,
8
+ as: resource_name,
9
+ url: user_password_path,
10
+ html: { class: "text-left", method: :put },
8
11
  builder: LesliView::Forms::Builder
9
- ) do |form| %>
12
+ ) do |form| %>
10
13
 
11
- <%= render "devise/shared/error_messages", resource: resource %>
14
+ <p class="mb-6 text-center text-sm leading-6 text-slate-600">
15
+ <%= I18n.t("lesli_shield.devise/passwords.view_update_description") %>
16
+ </p>
12
17
 
13
- <%= form.field_control_builder(
14
- label_html:form.label(:password),
15
- control_html:form.password_field(:password, autocomplete: "current-password", placeholder: "* * * * * *"),
16
- message_text: flash.first&.second,
17
- category: flash.first&.first,
18
- icon: "key"
19
- ) %>
18
+ <%= render "devise/shared/error_messages", resource: resource %>
20
19
 
21
- <%= form.field_control_builder(
22
- label_html:form.label(:password_confirmation),
23
- control_html:form.password_field(:password_confirmation, autocomplete: "current-password", placeholder: "* * * * * *"),
24
- message_text: flash.first&.second,
25
- category: flash.first&.first,
26
- icon: "key"
27
- ) %>
20
+ <%= form.hidden_field :reset_password_token %>
28
21
 
29
- <input type="hidden" name="user[reset_password_token]" value="<%= params[:reset_password_token] %>">
22
+ <%= form.field_control_password(
23
+ :password,
24
+ icon: "key",
25
+ label: I18n.t("lesli_shield.devise/passwords.field_password"),
26
+ message: I18n.t(
27
+ "lesli_shield.devise/passwords.field_password_hint",
28
+ count: @minimum_password_length
29
+ ),
30
+ required: true,
31
+ autofocus: true,
32
+ autocomplete: "new-password",
33
+ minlength: @minimum_password_length,
34
+ value: ""
35
+ ) %>
30
36
 
31
- <%= form.field_control_button("Cambiar contraseña", icon:"key") %>
37
+ <%= form.field_control_password(
38
+ :password_confirmation,
39
+ icon: "key",
40
+ label: I18n.t(
41
+ "lesli_shield.devise/passwords.field_password_confirmation"
42
+ ),
43
+ required: true,
44
+ autocomplete: "new-password",
45
+ minlength: @minimum_password_length,
46
+ value: ""
47
+ ) %>
32
48
 
33
- <% end %>
49
+ <%= form.field_control_button(
50
+ I18n.t("lesli_shield.devise/passwords.button_update_password"),
51
+ {
52
+ class: "is-fullwidth",
53
+ data: {
54
+ turbo_submits_with: I18n.t(
55
+ "lesli_shield.devise/passwords.button_updating"
56
+ )
57
+ }
58
+ },
59
+ icon: "key"
60
+ ) %>
61
+ <% end %>
34
62
 
35
- <%= render("devise/shared/links", login:true, password_reset:false, magic_link:false, otp:false) %>
63
+ <%= render(
64
+ "devise/shared/links",
65
+ login: true,
66
+ password_reset: false,
67
+ magic_link: false,
68
+ otp: false
69
+ ) %>
36
70
  <% end %>