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.
- checksums.yaml +4 -4
- data/Rakefile +3 -0
- data/app/assets/stylesheets/lesli_shield/application.css +140 -1
- data/app/assets/stylesheets/lesli_shield/confirmations.css +917 -18
- data/app/assets/stylesheets/lesli_shield/confirmations.tailwind.css +918 -0
- data/app/assets/stylesheets/lesli_shield/passwords.css +872 -18
- data/app/assets/stylesheets/lesli_shield/passwords.tailwind.css +873 -0
- data/app/assets/stylesheets/lesli_shield/registrations.css +904 -832
- data/app/assets/stylesheets/lesli_shield/registrations.tailwind.css +908 -0
- data/app/assets/stylesheets/lesli_shield/sessions.css +984 -32
- data/app/assets/stylesheets/lesli_shield/sessions.tailwind.css +983 -1
- data/app/controllers/lesli_shield/profile/passwords_controller.rb +119 -0
- data/app/controllers/lesli_shield/profiles_controller.rb +63 -0
- data/app/controllers/lesli_shield/sessions_controller.rb +1 -2
- data/app/controllers/users/confirmations_controller.rb +162 -49
- data/app/controllers/users/passwords_controller.rb +211 -28
- data/app/controllers/users/registrations_controller.rb +108 -83
- data/app/controllers/users/sessions_controller.rb +122 -84
- data/app/interfaces/lesli_shield/authorization_interface.rb +2 -2
- data/app/mailers/lesli_shield/devise_mailer.rb +73 -17
- data/app/models/concerns/lesli_shield/user_authentication.rb +54 -0
- data/app/models/lesli_shield/account.rb +4 -1
- data/app/models/lesli_shield/invite.rb +1 -1
- data/app/models/lesli_shield/user/role.rb +1 -1
- data/app/models/lesli_shield/user.rb +5 -2
- data/app/services/lesli_shield/profile/password_service.rb +47 -0
- data/app/services/lesli_shield/profile_service.rb +24 -0
- data/app/services/lesli_shield/user_registration_service.rb +41 -74
- data/app/services/lesli_shield/user_session_service.rb +1 -1
- data/app/services/lesli_shield/user_validator_service.rb +30 -32
- data/app/views/devise/confirmations/new.html.erb +58 -12
- data/app/views/devise/confirmations/show.html.erb +27 -60
- data/app/views/devise/passwords/edit.html.erb +59 -25
- data/app/views/devise/passwords/new.html.erb +61 -26
- data/app/views/devise/registrations/new.html.erb +80 -53
- data/app/views/devise/sessions/new.html.erb +53 -36
- data/app/views/devise/shared/_application-devise-simple.erb +18 -19
- data/app/views/devise/shared/_demo.html.erb +8 -6
- data/app/views/devise/shared/_error_messages.html.erb +45 -10
- data/app/views/devise/shared/_links.html.erb +6 -13
- data/app/views/lesli_shield/invites/_form.html.erb +1 -1
- data/app/views/lesli_shield/invites/index.html.erb +3 -2
- data/app/views/lesli_shield/profiles/_password-form.html.erb +68 -0
- data/app/views/lesli_shield/profiles/show.html.erb +29 -0
- data/app/views/lesli_shield/users/_information-form.html.erb +9 -1
- data/config/initializers/devise.rb +52 -328
- data/config/locales/translations.en.yml +65 -2
- data/config/locales/translations.es.yml +65 -2
- data/config/routes.rb +4 -0
- data/lib/lesli_shield/engine.rb +38 -4
- data/lib/lesli_shield/router.rb +52 -18
- data/lib/lesli_shield/version.rb +2 -2
- data/readme.md +4 -2
- metadata +30 -5
- data/app/assets/config/lesli_shield_manifest.js +0 -48
- data/app/assets/stylesheets/lesli_shield/application.tailwind.css +0 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Lesli
|
|
6
6
|
|
|
7
|
-
Copyright (c)
|
|
7
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
8
8
|
|
|
9
9
|
This program is free software: you can redistribute it and/or modify
|
|
10
10
|
it under the terms of the GNU General Public License as published by
|
|
@@ -33,98 +33,123 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
=end
|
|
34
34
|
|
|
35
35
|
class Users::RegistrationsController < Devise::RegistrationsController
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
REGISTRATION_RATE_LIMIT_WINDOW = 1.hour
|
|
37
|
+
|
|
38
|
+
before_action :ensure_registration_allowed!, only: %i[new create]
|
|
39
|
+
|
|
40
|
+
rate_limit(
|
|
41
|
+
to: 10,
|
|
42
|
+
within: REGISTRATION_RATE_LIMIT_WINDOW,
|
|
43
|
+
by: -> { request.remote_ip },
|
|
44
|
+
with: :respond_to_registration_rate_limit,
|
|
45
|
+
only: :create,
|
|
46
|
+
name: "registration-ip"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
rate_limit(
|
|
50
|
+
to: 3,
|
|
51
|
+
within: REGISTRATION_RATE_LIMIT_WINDOW,
|
|
52
|
+
by: :registration_email_rate_limit_key,
|
|
53
|
+
with: :respond_to_registration_rate_limit,
|
|
54
|
+
only: :create,
|
|
55
|
+
name: "registration-email"
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Devise owns user construction, validation, persistence, confirmation
|
|
59
|
+
# delivery, password cleanup, and the active/inactive response flow.
|
|
60
|
+
def create
|
|
61
|
+
super do |user|
|
|
62
|
+
record_user_creation_safely(user)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
38
65
|
|
|
39
|
-
|
|
40
|
-
# def new
|
|
41
|
-
# super
|
|
42
|
-
# end
|
|
66
|
+
protected
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
68
|
+
# Standalone registrations remain inactive until confirmation creates the
|
|
69
|
+
# account and role. Preserve the existing UX by returning to the form with
|
|
70
|
+
# Devise's success notice instructing the user to check their email.
|
|
71
|
+
def after_inactive_sign_up_path_for(_user)
|
|
72
|
+
new_user_registration_path
|
|
73
|
+
end
|
|
46
74
|
|
|
47
|
-
|
|
48
|
-
if !Lesli.config.security.dig(:allow_registration)
|
|
49
|
-
raise(I18n.t("core.users/registrations.messages_error_registration_not_allowed"))
|
|
50
|
-
end
|
|
75
|
+
private
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
77
|
+
def ensure_registration_allowed!
|
|
78
|
+
return if Lesli.config.security.dig(:allow_registration)
|
|
54
79
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
rescue => exception
|
|
63
|
-
#Honeybadger.notify(exception)
|
|
64
|
-
danger(exception.message)
|
|
65
|
-
end
|
|
66
|
-
redirect_to(new_user_registration_path)
|
|
80
|
+
redirect_to(
|
|
81
|
+
new_user_session_path,
|
|
82
|
+
alert: I18n.t(
|
|
83
|
+
"lesli_shield.devise/registrations.message_registration_not_allowed"
|
|
84
|
+
),
|
|
85
|
+
status: request.post? ? :see_other : :found
|
|
86
|
+
)
|
|
67
87
|
end
|
|
68
88
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
# end
|
|
73
|
-
|
|
74
|
-
# PUT /resource
|
|
75
|
-
# def update
|
|
76
|
-
# super
|
|
77
|
-
# end
|
|
78
|
-
|
|
79
|
-
# DELETE /resource
|
|
80
|
-
# def destroy
|
|
81
|
-
# super
|
|
82
|
-
# end
|
|
83
|
-
|
|
84
|
-
# GET /resource/cancel
|
|
85
|
-
# Forces the session data which is usually expired after sign
|
|
86
|
-
# in to be expired now. This is useful if the user wants to
|
|
87
|
-
# cancel oauth signing in/up in the middle of the process,
|
|
88
|
-
# removing all OAuth session data.
|
|
89
|
-
# def cancel
|
|
90
|
-
# super
|
|
91
|
-
# end
|
|
89
|
+
def registration_email_rate_limit_key
|
|
90
|
+
normalized_email = registration_params[:email].to_s.strip.downcase
|
|
91
|
+
identity = normalized_email.presence || "blank:#{request.remote_ip}"
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
OpenSSL::HMAC.hexdigest(
|
|
94
|
+
"SHA256",
|
|
95
|
+
Rails.application.secret_key_base,
|
|
96
|
+
identity
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def registration_params
|
|
101
|
+
params
|
|
102
|
+
.fetch(resource_name, ActionController::Parameters.new)
|
|
103
|
+
.permit(:email)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def respond_to_registration_rate_limit
|
|
107
|
+
message = I18n.t(
|
|
108
|
+
"lesli_shield.devise/registrations.message_too_many_attempts"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
response.set_header(
|
|
112
|
+
"Retry-After",
|
|
113
|
+
REGISTRATION_RATE_LIMIT_WINDOW.to_i.to_s
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
respond_to do |format|
|
|
117
|
+
format.html { render_registration_rate_limit(message) }
|
|
118
|
+
format.json do
|
|
119
|
+
render json: { error: message }, status: :too_many_requests
|
|
120
|
+
end
|
|
121
|
+
format.any { head :too_many_requests }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
94
124
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
# If you have extra params to permit, append them to the sanitizer.
|
|
101
|
-
def configure_sign_up_params
|
|
102
|
-
devise_parameter_sanitizer.permit(:sign_up, keys: [
|
|
103
|
-
:email,
|
|
104
|
-
:password,
|
|
105
|
-
:password_confirmation
|
|
106
|
-
])
|
|
125
|
+
def render_registration_rate_limit(message)
|
|
126
|
+
flash.now[:alert] = message
|
|
127
|
+
self.resource = resource_class.new(email: registration_params[:email])
|
|
128
|
+
set_minimum_password_length
|
|
129
|
+
render :new, status: :too_many_requests
|
|
107
130
|
end
|
|
108
131
|
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
# A standalone signup has no tenant yet, so its durable audit event belongs
|
|
133
|
+
# to the later confirmation transaction. Keep support for pre-associated
|
|
134
|
+
# resources without allowing an optional audit failure to undo registration.
|
|
135
|
+
def record_user_creation_safely(user)
|
|
136
|
+
return unless user.persisted? && user.account.present?
|
|
137
|
+
|
|
138
|
+
user.log(
|
|
139
|
+
engine: LesliShield,
|
|
140
|
+
source: self.class.name,
|
|
141
|
+
action: action_name,
|
|
142
|
+
operation: "user_creation_succeeded",
|
|
143
|
+
description: "User created successfully"
|
|
144
|
+
)
|
|
145
|
+
rescue StandardError => exception
|
|
146
|
+
Rails.error.report(
|
|
147
|
+
exception,
|
|
148
|
+
handled: true,
|
|
149
|
+
context: {
|
|
150
|
+
controller: self.class.name,
|
|
151
|
+
operation: "user_creation_succeeded"
|
|
152
|
+
}
|
|
153
|
+
)
|
|
129
154
|
end
|
|
130
155
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lesli
|
|
4
4
|
|
|
5
|
-
Copyright (c)
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
6
6
|
|
|
7
7
|
This program is free software: you can redistribute it and/or modify
|
|
8
8
|
it under the terms of the GNU General Public License as published by
|
|
@@ -30,109 +30,147 @@ Building a better future, one line of code at a time.
|
|
|
30
30
|
// ·
|
|
31
31
|
=end
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
require "openssl"
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
35
|
+
class Users::SessionsController < Devise::SessionsController
|
|
36
|
+
LOGIN_RATE_LIMIT_WINDOW = 15.minutes
|
|
37
|
+
|
|
38
|
+
rate_limit(
|
|
39
|
+
to: 20,
|
|
40
|
+
within: LOGIN_RATE_LIMIT_WINDOW,
|
|
41
|
+
by: -> { request.remote_ip },
|
|
42
|
+
with: :respond_to_login_rate_limit,
|
|
43
|
+
only: :create,
|
|
44
|
+
name: "login-ip"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
rate_limit(
|
|
48
|
+
to: 10,
|
|
49
|
+
within: LOGIN_RATE_LIMIT_WINDOW,
|
|
50
|
+
by: :login_email_rate_limit_key,
|
|
51
|
+
with: :respond_to_login_rate_limit,
|
|
52
|
+
only: :create,
|
|
53
|
+
name: "login-email"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Devise owns the authentication pipeline and yields only after credentials
|
|
57
|
+
# and active_for_authentication? have been accepted and the web session has
|
|
58
|
+
# been established. LesliShield adds its application session and audit event
|
|
59
|
+
# without reimplementing Devise's create action.
|
|
51
60
|
def create
|
|
61
|
+
super do |user|
|
|
62
|
+
persist_application_session!(user)
|
|
63
|
+
record_successful_login_analytics
|
|
64
|
+
end
|
|
65
|
+
end
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
self.resource = warden.authenticate(auth_options)
|
|
67
|
+
protected
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
69
|
+
# Devise calls this method only after a successful authentication. url_from
|
|
70
|
+
# accepts local paths and URLs for the current host while rejecting external
|
|
71
|
+
# and protocol-relative URLs.
|
|
72
|
+
def after_sign_in_path_for(_user)
|
|
73
|
+
url_from(requested_redirect) || configured_after_sign_in_path
|
|
74
|
+
end
|
|
62
75
|
|
|
63
|
-
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Devise has already established the web session when its create action
|
|
79
|
+
# yields. If either mandatory write fails, remove that session so the request
|
|
80
|
+
# cannot leave a partially completed login behind.
|
|
81
|
+
def persist_application_session!(user)
|
|
82
|
+
application_session_id = create_application_session_and_audit!(user)
|
|
83
|
+
session[:user_session_id] = application_session_id
|
|
84
|
+
rescue StandardError
|
|
85
|
+
rollback_web_session
|
|
86
|
+
raise
|
|
87
|
+
end
|
|
64
88
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
# The LesliShield session and its single successful-login audit event are
|
|
90
|
+
# committed atomically.
|
|
91
|
+
def create_application_session_and_audit!(user)
|
|
92
|
+
Lesli::User.transaction do
|
|
93
|
+
application_session = LesliShield::UserSessionService
|
|
94
|
+
.new(user)
|
|
95
|
+
.create(request.remote_ip, detected_user_agent)
|
|
96
|
+
.result
|
|
97
|
+
|
|
98
|
+
user.log(
|
|
99
|
+
engine: LesliShield,
|
|
100
|
+
source: self.class.name,
|
|
101
|
+
action: action_name,
|
|
102
|
+
operation: "session_creation_succeeded",
|
|
103
|
+
description: "Session creation successful",
|
|
104
|
+
session_id: application_session.id
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
application_session.id
|
|
69
108
|
end
|
|
109
|
+
end
|
|
70
110
|
|
|
71
|
-
|
|
111
|
+
def rollback_web_session
|
|
112
|
+
session.delete(:user_session_id)
|
|
113
|
+
sign_out(resource_name)
|
|
114
|
+
end
|
|
72
115
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
description: 'Session creation attempt'
|
|
80
|
-
) if defined?(LesliAudit)
|
|
116
|
+
# Request and device records are analytics, not additional authentication
|
|
117
|
+
# audit events.
|
|
118
|
+
def record_successful_login_analytics
|
|
119
|
+
log_requests if respond_to?(:log_requests)
|
|
120
|
+
log_devices if respond_to?(:log_devices)
|
|
121
|
+
end
|
|
81
122
|
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
def detected_user_agent
|
|
124
|
+
get_user_agent if respond_to?(:get_user_agent)
|
|
125
|
+
end
|
|
84
126
|
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
def configured_after_sign_in_path
|
|
128
|
+
Lesli.config.shield.dig(:path_after_login) || root_path
|
|
129
|
+
end
|
|
87
130
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
redirect_to user_session_path(r: sign_in_params[:redirect]) and return
|
|
92
|
-
end
|
|
93
|
-
end
|
|
131
|
+
def requested_redirect
|
|
132
|
+
login_request_params[:redirect]
|
|
133
|
+
end
|
|
94
134
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
135
|
+
def login_request_params
|
|
136
|
+
params.fetch(:user, {}).permit(
|
|
137
|
+
:email,
|
|
138
|
+
:password,
|
|
139
|
+
:remember_me,
|
|
140
|
+
:redirect
|
|
141
|
+
)
|
|
142
|
+
end
|
|
99
143
|
|
|
100
|
-
|
|
101
|
-
|
|
144
|
+
def login_email_rate_limit_key
|
|
145
|
+
normalized_email = login_request_params[:email].to_s.strip.downcase
|
|
146
|
+
identity = normalized_email.presence || "blank:#{request.remote_ip}"
|
|
102
147
|
|
|
103
|
-
|
|
104
|
-
|
|
148
|
+
OpenSSL::HMAC.hexdigest(
|
|
149
|
+
"SHA256",
|
|
150
|
+
Rails.application.secret_key_base,
|
|
151
|
+
identity
|
|
152
|
+
)
|
|
153
|
+
end
|
|
105
154
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
session_id: current_session[:id]
|
|
110
|
-
) if log
|
|
155
|
+
def respond_to_login_rate_limit
|
|
156
|
+
message = I18n.t("lesli_shield.devise/sessions.message_too_many_attempts")
|
|
157
|
+
response.set_header("Retry-After", LOGIN_RATE_LIMIT_WINDOW.to_i.to_s)
|
|
111
158
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
159
|
+
respond_to do |format|
|
|
160
|
+
format.html do
|
|
161
|
+
self.resource = resource_class.new(
|
|
162
|
+
email: login_request_params[:email]
|
|
163
|
+
)
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
165
|
+
flash.now[:alert] = message
|
|
166
|
+
render :new, status: :too_many_requests
|
|
167
|
+
end
|
|
120
168
|
|
|
121
|
-
|
|
169
|
+
format.json do
|
|
170
|
+
render json: { error: message }, status: :too_many_requests
|
|
171
|
+
end
|
|
122
172
|
|
|
123
|
-
|
|
124
|
-
if redirect_path.present?
|
|
125
|
-
uri = URI.parse(redirect_path)
|
|
126
|
-
# Ensures it's a local path
|
|
127
|
-
return redirect_path if uri.relative?
|
|
173
|
+
format.any { head :too_many_requests }
|
|
128
174
|
end
|
|
129
|
-
Lesli.config.shield.dig(:path_after_login) || root_path
|
|
130
|
-
rescue URI::InvalidURIError
|
|
131
|
-
# If the URL is invalid, fallback to root
|
|
132
|
-
root_path
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
def sign_in_params
|
|
136
|
-
params.fetch(:user, {}).permit(:email, :password, :redirect)
|
|
137
175
|
end
|
|
138
176
|
end
|
|
@@ -31,7 +31,7 @@ module LesliShield
|
|
|
31
31
|
:operation => :authorize_request,
|
|
32
32
|
:description => "Privilege not found for: #{request.path}"
|
|
33
33
|
)
|
|
34
|
-
return
|
|
34
|
+
return respond_with_forbidden({ controller: params[:controller], action: params[:action] })
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
unless granted
|
|
@@ -39,7 +39,7 @@ module LesliShield
|
|
|
39
39
|
:operation => :authorize_request,
|
|
40
40
|
:description => "Privilege not granted for: #{request.path}"
|
|
41
41
|
)
|
|
42
|
-
return
|
|
42
|
+
return respond_with_forbidden({ controller: params[:controller], action: params[:action] })
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
end
|
|
@@ -32,34 +32,41 @@ Building a better future, one line of code at a time.
|
|
|
32
32
|
module LesliShield
|
|
33
33
|
class DeviseMailer < Lesli::ApplicationLesliMailer
|
|
34
34
|
|
|
35
|
+
default from: -> { configured_sender }
|
|
36
|
+
|
|
35
37
|
# Sends an email with instructions to allow the user reset the password
|
|
36
38
|
def reset_password_instructions(user, token, opts = {})
|
|
37
39
|
|
|
38
40
|
# defaults for new accounts/users
|
|
39
41
|
email_template = "devise/reset_password_instructions"
|
|
40
|
-
email_subject = I18n.t(
|
|
42
|
+
email_subject = I18n.t(
|
|
43
|
+
"lesli_shield.devise/passwords.mailer_subject"
|
|
44
|
+
)
|
|
41
45
|
|
|
42
46
|
# email parameters
|
|
43
47
|
params = {
|
|
44
|
-
url:
|
|
48
|
+
url: password_reset_url(token),
|
|
45
49
|
full_name: user.full_name
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
# send email
|
|
49
|
-
|
|
50
|
-
params,
|
|
53
|
+
send_email(
|
|
54
|
+
params: params,
|
|
55
|
+
options: opts,
|
|
51
56
|
to: user.email,
|
|
52
57
|
subject: email_subject,
|
|
53
58
|
template_name: email_template
|
|
54
59
|
)
|
|
55
|
-
end
|
|
60
|
+
end
|
|
56
61
|
|
|
57
62
|
# Sends an email to allow the user confirm the email address
|
|
58
63
|
def confirmation_instructions(user, token, opts = {})
|
|
59
64
|
|
|
60
65
|
# defaults for new accounts/users
|
|
61
66
|
email_template = "devise/confirmation_instructions"
|
|
62
|
-
email_subject = I18n.t(
|
|
67
|
+
email_subject = I18n.t(
|
|
68
|
+
"lesli_shield.devise/confirmations.mailer_confirmation_subject"
|
|
69
|
+
)
|
|
63
70
|
|
|
64
71
|
# # custom email and subject if user is changin his email address
|
|
65
72
|
# if !record.unconfirmed_email.blank?
|
|
@@ -69,30 +76,79 @@ module LesliShield
|
|
|
69
76
|
|
|
70
77
|
# Depending on wheter there is a new user or they are changing their email,
|
|
71
78
|
# one or another field will be used
|
|
72
|
-
email_recipient = user.unconfirmed_email || user.email
|
|
79
|
+
email_recipient = user.unconfirmed_email.presence || user.email
|
|
73
80
|
|
|
74
81
|
# email parameters
|
|
75
82
|
params = {
|
|
76
|
-
url:
|
|
83
|
+
url: account_confirmation_url(token)
|
|
77
84
|
}
|
|
78
85
|
|
|
79
86
|
# send email
|
|
80
|
-
|
|
81
|
-
params,
|
|
87
|
+
send_email(
|
|
88
|
+
params: params,
|
|
89
|
+
options: opts,
|
|
82
90
|
to: email_recipient,
|
|
83
91
|
subject: email_subject,
|
|
84
92
|
template_name: email_template
|
|
85
93
|
)
|
|
86
94
|
end
|
|
87
95
|
|
|
88
|
-
def welcome(user)
|
|
89
|
-
email_recipient = user.unconfirmed_email || user.email
|
|
90
|
-
|
|
91
|
-
{ :
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
def welcome(user, opts = {})
|
|
97
|
+
email_recipient = user.unconfirmed_email.presence || user.email
|
|
98
|
+
send_email(
|
|
99
|
+
params: { user: user },
|
|
100
|
+
options: opts,
|
|
101
|
+
to: email_recipient,
|
|
102
|
+
subject: I18n.t(
|
|
103
|
+
"lesli_shield.devise/confirmations.mailer_welcome_subject"
|
|
104
|
+
),
|
|
105
|
+
template_name: "lesli/welcome"
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
private
|
|
110
|
+
|
|
111
|
+
def send_email(params:, options:, to:, subject:, template_name:)
|
|
112
|
+
headers = { to: to, subject: subject }.merge(options)
|
|
113
|
+
headers.delete(:template_name)
|
|
114
|
+
|
|
115
|
+
@params = @params.merge(params)
|
|
116
|
+
build_app_from_params(params)
|
|
117
|
+
|
|
118
|
+
mail(
|
|
119
|
+
**headers,
|
|
120
|
+
template_name: template_name
|
|
95
121
|
)
|
|
96
122
|
end
|
|
123
|
+
|
|
124
|
+
def configured_sender
|
|
125
|
+
sender = Devise.mailer_sender
|
|
126
|
+
sender = sender.call(:user) if sender.respond_to?(:call)
|
|
127
|
+
|
|
128
|
+
sender.presence ||
|
|
129
|
+
Rails.application.config.action_mailer.default_options&.dig(:from).presence ||
|
|
130
|
+
"no-reply@lesli.dev"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Use the host application's named route so password recovery continues
|
|
134
|
+
# to work when authentication is mounted under a custom path.
|
|
135
|
+
def password_reset_url(token)
|
|
136
|
+
Rails.application.routes.url_helpers.edit_user_password_url(
|
|
137
|
+
**action_mailer_url_options,
|
|
138
|
+
reset_password_token: token
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def account_confirmation_url(token)
|
|
143
|
+
Rails.application.routes.url_helpers.user_confirmation_url(
|
|
144
|
+
**action_mailer_url_options,
|
|
145
|
+
confirmation_token: token
|
|
146
|
+
)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def action_mailer_url_options
|
|
150
|
+
options = Rails.application.config.action_mailer.default_url_options
|
|
151
|
+
(options || {}).to_h.symbolize_keys
|
|
152
|
+
end
|
|
97
153
|
end
|
|
98
154
|
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LesliShield
|
|
4
|
+
# Installs Shield's Devise-based authentication implementation on the core
|
|
5
|
+
# user model. Lesli itself only consumes current_user and remains free of a
|
|
6
|
+
# direct dependency on this engine or on Devise.
|
|
7
|
+
module UserAuthentication
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
# This module is prepended after Devise installs its model modules so
|
|
11
|
+
# LesliShield can extend, rather than replace, Devise's eligibility
|
|
12
|
+
# checks. Calling super preserves confirmable, lockable, and any other
|
|
13
|
+
# authentication provider rules.
|
|
14
|
+
module LoginEligibility
|
|
15
|
+
def active_for_authentication?
|
|
16
|
+
super && lesli_shield_login_validation.valid?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def inactive_message
|
|
20
|
+
return super if lesli_shield_login_validation.valid?
|
|
21
|
+
|
|
22
|
+
:invalid
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def lesli_shield_login_validation
|
|
28
|
+
return @lesli_shield_login_validation if defined?(
|
|
29
|
+
@lesli_shield_login_validation
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
@lesli_shield_login_validation = LesliShield::UserValidatorService
|
|
33
|
+
.new(self)
|
|
34
|
+
.validate
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
DEVISE_MODULES = %i[
|
|
39
|
+
database_authenticatable
|
|
40
|
+
registerable
|
|
41
|
+
rememberable
|
|
42
|
+
recoverable
|
|
43
|
+
validatable
|
|
44
|
+
confirmable
|
|
45
|
+
trackable
|
|
46
|
+
lockable
|
|
47
|
+
].freeze
|
|
48
|
+
|
|
49
|
+
included do
|
|
50
|
+
devise(*LesliShield::UserAuthentication::DEVISE_MODULES)
|
|
51
|
+
prepend LesliShield::UserAuthentication::LoginEligibility
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|