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.
- 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 -770
- 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/lesli_shield/users_controller.rb +3 -7
- 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/authentication_interface.rb +3 -3
- 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 +35 -39
- 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-card.html.erb +35 -45
- 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 +44 -4
- data/lib/lesli_shield/router.rb +52 -18
- data/lib/lesli_shield/version.rb +2 -2
- data/readme.md +6 -4
- metadata +30 -5
- data/app/assets/config/lesli_shield_manifest.js +0 -48
- data/app/assets/stylesheets/lesli_shield/application.tailwind.css +0 -0
|
@@ -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
|
|
@@ -39,13 +39,13 @@ module LesliShield
|
|
|
39
39
|
|
|
40
40
|
# # check if user has an active session
|
|
41
41
|
# if current_session.equal? nil or !current_session.active?
|
|
42
|
-
# current_user.
|
|
42
|
+
# current_user.log(operation: "system_session_logout", description: "session finished by the system")
|
|
43
43
|
# sign_out current_user
|
|
44
44
|
# redirect_to "#{engine_path}/logout" and return
|
|
45
45
|
# end
|
|
46
46
|
|
|
47
47
|
# if !current_session.expiration_at.blank? && current_session.expiration_at < Time.current
|
|
48
|
-
# current_user.
|
|
48
|
+
# current_user.log(operation: "system_session_logout", description: "session expired by the system")
|
|
49
49
|
# sign_out current_user
|
|
50
50
|
# redirect_to "#{engine_path}/logout" and return
|
|
51
51
|
# end
|
|
@@ -53,7 +53,7 @@ module LesliShield
|
|
|
53
53
|
# # check password expiration date
|
|
54
54
|
# if current_user.has_expired_password?
|
|
55
55
|
# unless controller_name == "profiles"
|
|
56
|
-
# current_user.
|
|
56
|
+
# current_user.log(operation: "redirect_due_to_expired_password")
|
|
57
57
|
# redirect_to "/administration/profile#force-password-reset", notice: I18n.t("core.users/sessions.messages_danger_password_expired")
|
|
58
58
|
# return
|
|
59
59
|
# 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
|
|
@@ -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,
|
|
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,5 +1,8 @@
|
|
|
1
1
|
module LesliShield
|
|
2
|
-
|
|
3
|
-
|
|
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
|