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
@@ -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,53 +153,42 @@ 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
- @resource.logs.create({
158
- title: "session_creation_failed",
159
- description: "user_locked"
160
- })
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")
161
161
 
162
162
  return false
163
-
164
163
  end
165
164
 
166
- return true
165
+ true
167
166
  end
168
167
 
169
- def confirmed?
170
168
 
171
- # check if user is already confirmed
172
- 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
173
173
 
174
- # save a invalid credentials log for the requested user
175
- @resource.logs.create({
176
- title: "session_creation_failed",
177
- description: "email_not_confirmed"
178
- })
174
+ unless current_user.active? && !temporarily_locked
175
+
176
+ # save a locked log for the requested user
177
+ log_failure("user_locked")
179
178
 
180
179
  return false
181
180
 
182
181
  end
183
182
 
184
183
  return true
185
-
186
184
  end
187
185
 
188
186
  def roles_empty?
189
187
 
190
188
  # check if user has roles assigned
191
- if @resource.roles.empty?
189
+ unless current_user.roles.exists?
192
190
 
193
- @resource.logs.create({
194
- title: "session_creation_failed",
195
- description: "user_has_no_assigned_role"
196
- })
191
+ log_failure("user_has_no_assigned_role")
197
192
 
198
193
  return true
199
194
 
@@ -206,16 +201,17 @@ module LesliShield
206
201
  def active_roles?
207
202
 
208
203
  # check user has at least one active role before authorize the sign-in request
209
- unless (@resource.roles.map {|role| role.active}.include? true)
210
- @resource.logs.create({
211
- title: "session_creation_failed",
212
- description: "user_has_no_active_role"
213
- })
204
+ unless current_user.roles.exists?(active: true)
205
+ log_failure("user_has_no_active_role")
214
206
 
215
207
  return false
216
208
  end
217
209
 
218
210
  return true
219
211
  end
212
+
213
+ def log_failure(description)
214
+ failures.push(description) unless failures.include?(description)
215
+ end
220
216
  end
221
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 %>
@@ -1,33 +1,68 @@
1
1
 
2
- <%= render("devise/shared/application-devise-simple", title:"Forgot your password?") do %>
2
+ <%= render(
3
+ "devise/shared/application-devise-simple",
4
+ title: I18n.t("lesli_shield.devise/passwords.view_request_title")
5
+ ) do %>
3
6
 
4
7
  <%= form_for(
5
- resource,
6
- as: resource_name,
7
- url: user_password_path(),
8
- html: { class: "has-text-centered" },
8
+ resource,
9
+ as: resource_name,
10
+ url: user_password_path,
11
+ html: { class: "text-left" },
9
12
  builder: LesliView::Forms::Builder
10
- ) do |form| %>
11
-
12
- <%= render "devise/shared/error_messages", resource: resource %>
13
-
14
- <%= form.field_control_email(
15
- :email,
16
- icon: "person_search",
17
- label: "Enter your email address you use in Lesli. We'll send instructions for setting a new password to your email.")
18
- %>
19
-
20
- <%= form.field_control_button("Send me reset password instructions", icon:"mail") %>
21
-
22
- <% if Lesli.config.security.dig(:allow_registration) %>
23
- <p class="account has-text-centered">
24
- Still not registered?
25
- <%= link_to(new_user_registration_path, :class => "has-text-primary") do %>
26
- <u><b>Create account</b></u>
27
- <% end %>
28
- </p>
29
- <% end %>
13
+ ) do |form| %>
14
+
15
+ <p class="mb-6 text-center text-sm leading-6 text-slate-600">
16
+ <%= I18n.t("lesli_shield.devise/passwords.view_request_description") %>
17
+ </p>
18
+
19
+ <%= render "devise/shared/error_messages", resource: resource %>
20
+
21
+ <%= form.field_control_email(
22
+ :email,
23
+ icon: "mail",
24
+ label: I18n.t("lesli_shield.devise/passwords.field_email"),
25
+ required: true,
26
+ autofocus: true,
27
+ autocomplete: "email",
28
+ autocapitalize: "none",
29
+ spellcheck: false
30
+ ) %>
31
+
32
+ <%= form.field_control_button(
33
+ I18n.t("lesli_shield.devise/passwords.button_send_instructions"),
34
+ {
35
+ class: "is-fullwidth",
36
+ data: {
37
+ turbo_submits_with: I18n.t(
38
+ "lesli_shield.devise/passwords.button_sending"
39
+ )
40
+ }
41
+ },
42
+ icon: "mail"
43
+ ) %>
44
+
45
+ <% if Lesli.config.security.dig(:allow_registration) %>
46
+ <p class="mt-6 text-center text-sm text-slate-600">
47
+ <%= I18n.t(
48
+ "lesli_shield.devise/sessions.view_still_not_registered"
49
+ ) %>
50
+ <%= link_to(
51
+ I18n.t(
52
+ "lesli_shield.devise/registrations.button_create_account"
53
+ ),
54
+ new_user_registration_path,
55
+ class: "font-semibold text-[var(--lesli-color-primary)] underline underline-offset-2 hover:opacity-80 focus-visible:rounded-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--lesli-color-primary)]"
56
+ ) %>
57
+ </p>
30
58
  <% end %>
59
+ <% end %>
31
60
 
32
- <%= render("devise/shared/links", login:true, password_reset:false, magic_link:false, otp:false) %>
61
+ <%= render(
62
+ "devise/shared/links",
63
+ login: true,
64
+ password_reset: false,
65
+ magic_link: false,
66
+ otp: false
67
+ ) %>
33
68
  <% end %>