searls-auth 1.0.2 → 2.0.0
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/CHANGELOG.md +6 -1
- data/README.md +22 -1
- data/TODO.md +21 -0
- data/app/controllers/searls/auth/base_controller.rb +4 -4
- data/app/controllers/searls/auth/email_verifications_controller.rb +1 -1
- data/app/controllers/searls/auth/logins_controller.rb +6 -2
- data/app/controllers/searls/auth/registrations_controller.rb +11 -11
- data/app/controllers/searls/auth/requests_password_resets_controller.rb +3 -3
- data/app/controllers/searls/auth/resets_passwords_controller.rb +3 -3
- data/app/controllers/searls/auth/settings_controller.rb +2 -2
- data/app/controllers/searls/auth/verifications_controller.rb +1 -1
- data/app/helpers/searls/auth/application_helper.rb +11 -1
- data/app/mailers/searls/auth/email_verification_mailer.rb +1 -1
- data/app/mailers/searls/auth/login_link_mailer.rb +1 -1
- data/app/mailers/searls/auth/password_reset_mailer.rb +1 -1
- data/app/views/searls/auth/email_verification_mailer/verification_email.html.erb +1 -1
- data/app/views/searls/auth/email_verification_mailer/verification_email.text.erb +1 -1
- data/app/views/searls/auth/login_link_mailer/login_link.html.erb +1 -1
- data/app/views/searls/auth/login_link_mailer/login_link.text.erb +1 -1
- data/app/views/searls/auth/logins/show.html.erb +1 -1
- data/app/views/searls/auth/password_reset_mailer/password_reset.html.erb +1 -1
- data/app/views/searls/auth/password_reset_mailer/password_reset.text.erb +1 -1
- data/app/views/searls/auth/registrations/show.html.erb +1 -1
- data/app/views/searls/auth/requests_password_resets/show.html.erb +1 -1
- data/app/views/searls/auth/resets_passwords/show.html.erb +1 -1
- data/app/views/searls/auth/settings/edit.html.erb +1 -1
- data/app/views/searls/auth/verifications/show.html.erb +1 -1
- data/lib/searls/auth/builds_target_redirect_url.rb +39 -30
- data/lib/searls/auth/config.rb +28 -0
- data/lib/searls/auth/creates_user.rb +5 -5
- data/lib/searls/auth/delivers_password_reset.rb +2 -2
- data/lib/searls/auth/emails_link.rb +2 -2
- data/lib/searls/auth/emails_verification.rb +2 -2
- data/lib/searls/auth/sanitizes_flash_html.rb +18 -0
- data/lib/searls/auth/version.rb +1 -1
- data/lib/searls/auth.rb +4 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b89495d0211c7f68d45f32b331fa2aba422d39a829b3ab7569bbe66901c17de3
|
|
4
|
+
data.tar.gz: 89eb9084ad27d89d1bb4ca328a157ca89578a0bfe57c53359f92b30421a2ec33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a06c5572753eb9718b4da43e297b8fcc19b20d7b3e19bdb8bd13ab400b289179e3b3f1a2e92fca5ce1203064b5445f2a7868cde2a8c3ee42c7c560d301659aaa
|
|
7
|
+
data.tar.gz: 2b9a204452996e4d25df5abbc1608667f59ab6f791138918ca99c8dd0c24cf09173617fa1d2e3839bc3263b491970a627ac72336d90eca653f18c8b5acc770c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
## [
|
|
1
|
+
## [2.0.0] - 2026-01-22
|
|
2
|
+
|
|
3
|
+
* **BREAKING:** Remove `redirect_subdomain` support in favor of `redirect_host`
|
|
4
|
+
* **BREAKING:** Replace `redirect_host_allowed_predicate`, `cross_domain_sso_token_generator`, and `cross_cookie_domain_predicate` with `sso_token_for_cross_domain_redirects`
|
|
5
|
+
* Security: Sanitize user-controlled `notice` and `alert` query params rendered on the login page to mitigate reflected XSS, while preserving safe formatting tags and links (`<a href=...>` and Turbo data attributes)
|
|
6
|
+
* Behavior: Disallowed tags/attributes/protocols are stripped from login `notice`/`alert` params (e.g. `<script>`, event handler attributes, `javascript:` links)
|
|
2
7
|
|
|
3
8
|
## [1.0.2] - 2025-10-13
|
|
4
9
|
|
data/README.md
CHANGED
|
@@ -84,6 +84,15 @@ end
|
|
|
84
84
|
```
|
|
85
85
|
As stated in the comment above, you can find each configuration and its default value in the code.
|
|
86
86
|
|
|
87
|
+
### Customize user lookup
|
|
88
|
+
|
|
89
|
+
searls-auth uses separate hooks to find users by email for login vs registration:
|
|
90
|
+
|
|
91
|
+
- `config.user_finder_by_email` is used by login.
|
|
92
|
+
- `config.user_finder_by_email_for_registration` is used by registration to find an existing row to update (e.g., “upgrade” an existing user record).
|
|
93
|
+
|
|
94
|
+
If `user_finder_by_email_for_registration` returns a persisted user, searls-auth will block the registration attempt unless `config.existing_user_registration_blocked_predicate.call(user, params)` returns `false`.
|
|
95
|
+
|
|
87
96
|
### Choose your login methods
|
|
88
97
|
|
|
89
98
|
By default, users can log in either by clicking a magic link or by entering a 6‑digit code they receive via email. This is controlled by the `auth_methods` configuration:
|
|
@@ -264,7 +273,19 @@ end
|
|
|
264
273
|
| `[:password, :email_link, :email_otp]` | `:optional` | Users can log in with either password or email. Registration logs the user in immediately and also emails a verification link. |
|
|
265
274
|
| `[:password, :email_link]` | `:required` | Registration emails a link and blocks password login until verified. Resend verification is exposed at `searls_auth.resend_email_verification_path`. |
|
|
266
275
|
|
|
267
|
-
In every case, `redirect_path` values are normalized to on-site URLs, so forwarding someone to login with `redirect_path: some_path` keeps the eventual redirect on your domain
|
|
276
|
+
In every case, `redirect_path` values are normalized to on-site URLs, so forwarding someone to login with `redirect_path: some_path` keeps the eventual redirect on your domain.
|
|
277
|
+
|
|
278
|
+
You can optionally pass `redirect_host` to redirect to another host (e.g., an admin subdomain or another app).
|
|
279
|
+
|
|
280
|
+
When redirecting to another host, you can append an SSO token by setting `config.sso_token_for_cross_domain_redirects` (defaults to `nil`). It is called with `(user, request, target_host)` and should return a token string or `nil`.
|
|
281
|
+
|
|
282
|
+
### Multi-domain logout bounce
|
|
283
|
+
|
|
284
|
+
The engine provides `searls_auth.logout_path`, which clears the current session on the current host.
|
|
285
|
+
|
|
286
|
+
If your app uses multiple cookie domains, the engine cannot clear the other domain’s session cookie. The simplest approach is to implement logout in your host app and “bounce” through the other domain(s), clearing each cookie in turn.
|
|
287
|
+
|
|
288
|
+
If you mount the engine at `/`, you can shadow its `/logout` route by defining your own `get "/logout"` route before the `mount` line. Your controller can call `Searls::Auth::ResetsSession.new.reset(self, except_for: [...])` and then redirect to your other domain’s logout endpoint with a `return_to` back to your primary domain.
|
|
268
289
|
|
|
269
290
|
## Use it
|
|
270
291
|
|
data/TODO.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# TODO (bwb-adoption)
|
|
2
|
+
|
|
3
|
+
This is the remaining `searls-auth` gem work needed for grog/BWB adoption.
|
|
4
|
+
|
|
5
|
+
- [x] Add **cross-host** redirect support (not just path-only redirects)
|
|
6
|
+
- [x] Accept a `redirect_host` param (optional), and reject/ignore it by default
|
|
7
|
+
- [x] Add a config hook to validate hosts (e.g. `redirect_host_allowed_predicate`)
|
|
8
|
+
- [x] Extend `Searls::Auth::BuildsTargetRedirectUrl` to build absolute URLs using `redirect_host` when allowed
|
|
9
|
+
- [x] Preserve existing path normalization and avoid open redirects
|
|
10
|
+
- [x] Add **cross-cookie-domain SSO token forwarding** hook
|
|
11
|
+
- [x] Provide a config hook (e.g. `cross_domain_sso_token_generator`) that can append `sso_token=...` when redirecting to another cookie domain
|
|
12
|
+
- [x] Make token param name configurable (default `sso_token`)
|
|
13
|
+
- [x] Provide an overridable way to decide “cross-cookie-domain” (default: registrable domain differs)
|
|
14
|
+
- [x] Add (or document) **logout bounce** support for multi-domain apps
|
|
15
|
+
- [x] Either: first-class config for “also clear session on another domain”, or explicit docs showing how host apps can implement it
|
|
16
|
+
- [x] Add example app coverage
|
|
17
|
+
- [x] Add integration/system tests proving `redirect_host` is rejected by default and allowed when configured
|
|
18
|
+
- [x] Add a test proving SSO token appending works when enabled
|
|
19
|
+
- [x] Update docs
|
|
20
|
+
- [x] Document `user_finder_by_email_for_registration` and the “upgrade existing non-registered user row” pattern
|
|
21
|
+
- [x] Document the new cross-domain hooks and security expectations
|
|
@@ -8,7 +8,7 @@ module Searls
|
|
|
8
8
|
helper_method :forwardable_params
|
|
9
9
|
|
|
10
10
|
def forwardable_params
|
|
11
|
-
{redirect_path: params[:redirect_path],
|
|
11
|
+
{redirect_path: params[:redirect_path], redirect_host: params[:redirect_host]}.compact_blank
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
protected
|
|
@@ -40,8 +40,8 @@ module Searls
|
|
|
40
40
|
session[:searls_auth_email_otp_verification_attempts] += 1
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def target_redirect_url
|
|
44
|
-
Searls::Auth::BuildsTargetRedirectUrl.new.build(request, params)
|
|
43
|
+
def target_redirect_url(user: nil)
|
|
44
|
+
Searls::Auth::BuildsTargetRedirectUrl.new.build(request, params, user: user)
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def redirect_with_host_awareness(target)
|
|
@@ -49,7 +49,7 @@ module Searls
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def redirect_after_login(user)
|
|
52
|
-
if (target = target_redirect_url)
|
|
52
|
+
if (target = target_redirect_url(user: user))
|
|
53
53
|
redirect_with_host_awareness(target)
|
|
54
54
|
else
|
|
55
55
|
redirect_to Searls::Auth.config.resolve(
|
|
@@ -22,7 +22,7 @@ module Searls
|
|
|
22
22
|
searls_auth.pending_email_verification_path({
|
|
23
23
|
email: session[:searls_auth_pending_email],
|
|
24
24
|
redirect_path: session[:searls_auth_pending_redirect_path],
|
|
25
|
-
|
|
25
|
+
redirect_host: session[:searls_auth_pending_redirect_host]
|
|
26
26
|
}.compact_blank)
|
|
27
27
|
else
|
|
28
28
|
searls_auth.login_path
|
|
@@ -4,6 +4,9 @@ module Searls
|
|
|
4
4
|
before_action :reset_expired_email_otp
|
|
5
5
|
|
|
6
6
|
def show
|
|
7
|
+
sanitizes_flash_html = SanitizesFlashHtml.new
|
|
8
|
+
flash.now[:notice] ||= sanitizes_flash_html.sanitize(params[:notice]) if params[:notice].present?
|
|
9
|
+
flash.now[:alert] ||= sanitizes_flash_html.sanitize(params[:alert]) if params[:alert].present?
|
|
7
10
|
render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout
|
|
8
11
|
end
|
|
9
12
|
|
|
@@ -16,9 +19,10 @@ module Searls
|
|
|
16
19
|
end
|
|
17
20
|
|
|
18
21
|
def destroy
|
|
19
|
-
ResetsSession.new.reset(self, except_for:
|
|
22
|
+
ResetsSession.new.reset(self, except_for: Searls::Auth.config.preserve_session_keys_after_logout)
|
|
20
23
|
flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_logout, params)
|
|
21
|
-
|
|
24
|
+
target = Searls::Auth.config.resolve(:redirect_url_after_logout, flash[:notice], params, request, main_app)
|
|
25
|
+
redirect_to(target.presence || searls_auth.login_path, allow_other_host: true)
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
private
|
|
@@ -36,22 +36,22 @@ module Searls
|
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def handle_password_registration(user)
|
|
39
|
-
target_path,
|
|
39
|
+
target_path, target_host = registration_redirect_destination(user)
|
|
40
40
|
if Searls::Auth.config.email_verification_mode != :none
|
|
41
|
-
EmailsVerification.new.email(user: user, redirect_path: target_path,
|
|
41
|
+
EmailsVerification.new.email(user: user, redirect_path: target_path, redirect_host: target_host)
|
|
42
42
|
session[:searls_auth_pending_email] = user.email
|
|
43
43
|
session[:searls_auth_pending_redirect_path] = target_path
|
|
44
|
-
session[:
|
|
44
|
+
session[:searls_auth_pending_redirect_host] = target_host
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
if Searls::Auth.config.email_verification_mode == :required
|
|
48
48
|
flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_registration, user, params)
|
|
49
|
-
redirect_to searls_auth.pending_email_verification_path({email: user.email, redirect_path: target_path,
|
|
49
|
+
redirect_to searls_auth.pending_email_verification_path({email: user.email, redirect_path: target_path, redirect_host: target_host}.compact_blank)
|
|
50
50
|
else
|
|
51
51
|
session[:user_id] = user.id
|
|
52
52
|
session[:has_logged_in_before] = true
|
|
53
53
|
flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_login, user, params)
|
|
54
|
-
if (target = target_redirect_url)
|
|
54
|
+
if (target = target_redirect_url(user: user))
|
|
55
55
|
redirect_with_host_awareness(target)
|
|
56
56
|
else
|
|
57
57
|
fallback = Searls::Auth.config.resolve(:redirect_path_after_login, user, params, request, main_app)
|
|
@@ -62,25 +62,25 @@ module Searls
|
|
|
62
62
|
|
|
63
63
|
def handle_email_registration(user)
|
|
64
64
|
return unless email_methods_enabled?
|
|
65
|
-
target_path,
|
|
66
|
-
enqueue_login_verification_email(user, target_path:,
|
|
65
|
+
target_path, target_host = registration_redirect_destination(user)
|
|
66
|
+
enqueue_login_verification_email(user, target_path:, target_host:)
|
|
67
67
|
flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_registration, user, params)
|
|
68
68
|
redirect_to searls_auth.verify_path(**forwardable_params)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def registration_redirect_destination(user)
|
|
72
72
|
if redirect_params_supplied?
|
|
73
|
-
[params[:redirect_path], params[:
|
|
73
|
+
[params[:redirect_path], params[:redirect_host]]
|
|
74
74
|
else
|
|
75
75
|
[Searls::Auth.config.resolve(:redirect_path_after_register, user, params, request, main_app), nil]
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
79
|
def redirect_params_supplied?
|
|
80
|
-
params[:redirect_path].present? || params[:
|
|
80
|
+
params[:redirect_path].present? || params[:redirect_host].present?
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
-
def enqueue_login_verification_email(user, target_path:,
|
|
83
|
+
def enqueue_login_verification_email(user, target_path:, target_host:)
|
|
84
84
|
email_otp = nil
|
|
85
85
|
if Searls::Auth.config.auth_methods.include?(:email_otp)
|
|
86
86
|
attach_email_otp_to_session!(user)
|
|
@@ -92,7 +92,7 @@ module Searls
|
|
|
92
92
|
user: user,
|
|
93
93
|
email_otp: email_otp,
|
|
94
94
|
redirect_path: target_path,
|
|
95
|
-
|
|
95
|
+
redirect_host: target_host
|
|
96
96
|
)
|
|
97
97
|
end
|
|
98
98
|
end
|
|
@@ -16,7 +16,7 @@ module Searls
|
|
|
16
16
|
Searls::Auth::DeliversPasswordReset.new.deliver(
|
|
17
17
|
user:,
|
|
18
18
|
redirect_path: params[:redirect_path],
|
|
19
|
-
|
|
19
|
+
redirect_host: params[:redirect_host]
|
|
20
20
|
)
|
|
21
21
|
end
|
|
22
22
|
|
|
@@ -24,7 +24,7 @@ module Searls
|
|
|
24
24
|
redirect_to searls_auth.password_reset_request_path(
|
|
25
25
|
email: email,
|
|
26
26
|
redirect_path: params[:redirect_path],
|
|
27
|
-
|
|
27
|
+
redirect_host: params[:redirect_host]
|
|
28
28
|
)
|
|
29
29
|
end
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@ module Searls
|
|
|
36
36
|
flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_password_reset_not_enabled, params)
|
|
37
37
|
redirect_to searls_auth.login_path(
|
|
38
38
|
redirect_path: params[:redirect_path],
|
|
39
|
-
|
|
39
|
+
redirect_host: params[:redirect_host]
|
|
40
40
|
)
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
@@ -36,7 +36,7 @@ module Searls
|
|
|
36
36
|
flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_password_reset_not_enabled, params)
|
|
37
37
|
redirect_to searls_auth.login_path(
|
|
38
38
|
redirect_path: params[:redirect_path],
|
|
39
|
-
|
|
39
|
+
redirect_host: params[:redirect_host]
|
|
40
40
|
)
|
|
41
41
|
nil
|
|
42
42
|
end
|
|
@@ -49,7 +49,7 @@ module Searls
|
|
|
49
49
|
flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_password_reset_token_invalid, params)
|
|
50
50
|
redirect_to searls_auth.password_reset_request_path(
|
|
51
51
|
redirect_path: params[:redirect_path],
|
|
52
|
-
|
|
52
|
+
redirect_host: params[:redirect_host]
|
|
53
53
|
)
|
|
54
54
|
nil
|
|
55
55
|
end
|
|
@@ -64,7 +64,7 @@ module Searls
|
|
|
64
64
|
else
|
|
65
65
|
redirect_to searls_auth.login_path(
|
|
66
66
|
redirect_path: params[:redirect_path],
|
|
67
|
-
|
|
67
|
+
redirect_host: params[:redirect_host]
|
|
68
68
|
)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
@@ -50,7 +50,7 @@ module Searls
|
|
|
50
50
|
|
|
51
51
|
redirect_to searls_auth.login_path(
|
|
52
52
|
redirect_path: request.original_fullpath,
|
|
53
|
-
|
|
53
|
+
redirect_host: request.host
|
|
54
54
|
)
|
|
55
55
|
nil
|
|
56
56
|
end
|
|
@@ -62,7 +62,7 @@ module Searls
|
|
|
62
62
|
session.delete(:user_id)
|
|
63
63
|
redirect_to searls_auth.login_path(
|
|
64
64
|
redirect_path: request.original_fullpath,
|
|
65
|
-
|
|
65
|
+
redirect_host: request.host
|
|
66
66
|
)
|
|
67
67
|
nil
|
|
68
68
|
end
|
|
@@ -39,7 +39,7 @@ module Searls
|
|
|
39
39
|
session[:user_id] = result.user.id
|
|
40
40
|
session[:has_logged_in_before] = true
|
|
41
41
|
flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_login, result.user, params)
|
|
42
|
-
if (target = target_redirect_url)
|
|
42
|
+
if (target = target_redirect_url(user: result.user))
|
|
43
43
|
redirect_with_host_awareness(target)
|
|
44
44
|
else
|
|
45
45
|
redirect_to Searls::Auth.config.resolve(:redirect_path_after_login, result.user, params, request, main_app)
|
|
@@ -55,7 +55,7 @@ module Searls
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def enable_turbo?
|
|
58
|
-
|
|
58
|
+
!redirect_host_crosses_host?
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def attr_for(model, field_name)
|
|
@@ -78,6 +78,16 @@ module Searls
|
|
|
78
78
|
def params
|
|
79
79
|
@view_context.params
|
|
80
80
|
end
|
|
81
|
+
|
|
82
|
+
def request
|
|
83
|
+
@view_context.request
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def redirect_host_crosses_host?
|
|
87
|
+
host = params[:redirect_host].to_s.strip.downcase
|
|
88
|
+
return false if host.blank?
|
|
89
|
+
host != request.host
|
|
90
|
+
end
|
|
81
91
|
end
|
|
82
92
|
|
|
83
93
|
module ApplicationHelper
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<%= link_to searls_auth.verify_email_url({
|
|
13
13
|
token: @token,
|
|
14
14
|
redirect_path: @redirect_path,
|
|
15
|
-
|
|
15
|
+
redirect_host: @redirect_host
|
|
16
16
|
}.compact_blank) do %>
|
|
17
17
|
<span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Verify email address</span>
|
|
18
18
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Hello<% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_method)).present? %>, <%= user_name %><% end %>!
|
|
2
2
|
|
|
3
3
|
Verify your email using this link:
|
|
4
|
-
<%= searls_auth.verify_email_url({ token: @token, redirect_path: @redirect_path,
|
|
4
|
+
<%= searls_auth.verify_email_url({ token: @token, redirect_path: @redirect_path, redirect_host: @redirect_host }.compact_blank) %>
|
|
5
5
|
|
|
6
6
|
If you didn't ask for this, you can ignore this email.
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<%= link_to verify_token_url({
|
|
14
14
|
token: @token,
|
|
15
15
|
redirect_path: @redirect_path,
|
|
16
|
-
|
|
16
|
+
redirect_host: @redirect_host
|
|
17
17
|
}.compact_blank) do %>
|
|
18
18
|
<span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Log in</span>
|
|
19
19
|
<% end %>
|
|
@@ -10,7 +10,7 @@ You can log in by visiting this URL for your <%= searls_auth_helper.rpad(@config
|
|
|
10
10
|
<%= searls_auth.verify_token_url({
|
|
11
11
|
token: @token,
|
|
12
12
|
redirect_path: @redirect_path,
|
|
13
|
-
|
|
13
|
+
redirect_host: @redirect_host
|
|
14
14
|
}.compact_blank) %>
|
|
15
15
|
<% end %>
|
|
16
16
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with url: searls_auth.login_path, method: :post, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
|
|
4
4
|
<%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
|
|
5
|
-
<%= f.hidden_field :
|
|
5
|
+
<%= f.hidden_field :redirect_host, value: params[:redirect_host] %>
|
|
6
6
|
<%= render "searls/auth/shared/login_fields", f: f %>
|
|
7
7
|
<div>
|
|
8
8
|
<%= f.submit "Log in" %>
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<%= link_to password_reset_edit_url({
|
|
13
13
|
token: @token,
|
|
14
14
|
redirect_path: @redirect_path,
|
|
15
|
-
|
|
15
|
+
redirect_host: @redirect_host
|
|
16
16
|
}.compact_blank) do %>
|
|
17
17
|
<span style="width: 70%; box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); display: inline-block; padding-left: 2rem; padding-right: 2rem; padding-top: 1rem; padding-bottom: 1rem; font-size: 1.5rem; font-weight: 700; border-radius: 0.75rem; background-color: <%= @config.email_button_color %>; color: white;">Reset password</span>
|
|
18
18
|
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Hello<% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_method)).present? %>, <%= user_name %><% end %>!
|
|
2
2
|
|
|
3
3
|
Reset your password using this link:
|
|
4
|
-
<%= password_reset_edit_url({ token: @token, redirect_path: @redirect_path,
|
|
4
|
+
<%= password_reset_edit_url({ token: @token, redirect_path: @redirect_path, redirect_host: @redirect_host }.compact_blank) %>
|
|
5
5
|
|
|
6
6
|
If you didn't ask for this, you can ignore this email.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with url: searls_auth.register_path, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
|
|
4
4
|
<%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
|
|
5
|
-
<%= f.hidden_field :
|
|
5
|
+
<%= f.hidden_field :redirect_host, value: params[:redirect_host] %>
|
|
6
6
|
<%= render "searls/auth/shared/register_fields", f: f %>
|
|
7
7
|
<%= f.submit "Register" %>
|
|
8
8
|
<% end %>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with url: searls_auth.password_reset_request_path, method: :post, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
|
|
4
4
|
<%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
|
|
5
|
-
<%= f.hidden_field :
|
|
5
|
+
<%= f.hidden_field :redirect_host, value: params[:redirect_host] %>
|
|
6
6
|
<div>
|
|
7
7
|
<%= f.label :email, "Email address" %>
|
|
8
8
|
<%= f.email_field :email, required: true, value: params[:email], autocomplete: "email" %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<%= form_with url: searls_auth.password_reset_update_path, method: :patch do |f| %>
|
|
4
4
|
<%= f.hidden_field :token, value: @token %>
|
|
5
5
|
<%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
|
|
6
|
-
<%= f.hidden_field :
|
|
6
|
+
<%= f.hidden_field :redirect_host, value: params[:redirect_host] %>
|
|
7
7
|
<div>
|
|
8
8
|
<%= label_tag :email, "Email address" %>
|
|
9
9
|
<%= email_field_tag :email, @user_email, disabled: true %>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<%= form_with scope: :settings, url: searls_auth.settings_path, method: :patch do |f| %>
|
|
4
4
|
<%= hidden_field_tag :redirect_path, params[:redirect_path] %>
|
|
5
|
-
<%= hidden_field_tag :
|
|
5
|
+
<%= hidden_field_tag :redirect_host, params[:redirect_host] %>
|
|
6
6
|
|
|
7
7
|
<fieldset>
|
|
8
8
|
<legend>Password</legend>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<% if Searls::Auth.config.auth_methods.include?(:email_otp) %>
|
|
8
8
|
<%= form_with(url: searls_auth.verify_path, method: :post, data: { turbo: searls_auth_helper.enable_turbo? }) do |f| %>
|
|
9
9
|
<%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
|
|
10
|
-
<%= f.hidden_field :
|
|
10
|
+
<%= f.hidden_field :redirect_host, value: params[:redirect_host] %>
|
|
11
11
|
<div data-controller="<%= searls_auth_helper.otp_stimulus_controller %>">
|
|
12
12
|
<%= f.label :short_code, "Code" %>
|
|
13
13
|
<%= f.text_field :short_code,
|
|
@@ -1,16 +1,22 @@
|
|
|
1
1
|
require "uri"
|
|
2
|
+
require "rack/utils"
|
|
2
3
|
|
|
3
4
|
module Searls
|
|
4
5
|
module Auth
|
|
5
6
|
class BuildsTargetRedirectUrl
|
|
6
|
-
def build(request, params)
|
|
7
|
+
def build(request, params, user: nil)
|
|
7
8
|
path = normalize_path(params[:redirect_path])
|
|
8
|
-
host =
|
|
9
|
+
host = normalize_redirect_host(params[:redirect_host])
|
|
9
10
|
|
|
10
|
-
if host == request.host && path.present?
|
|
11
|
+
if (host.blank? || host == request.host) && path.present?
|
|
11
12
|
path
|
|
12
|
-
elsif host != request.host
|
|
13
|
-
absolute_url(request, host, path)
|
|
13
|
+
elsif host.present? && host != request.host
|
|
14
|
+
url = absolute_url(request, host, path)
|
|
15
|
+
if same_cookie_domain?(request, host)
|
|
16
|
+
url
|
|
17
|
+
else
|
|
18
|
+
append_cross_domain_sso_token(url, request, user, host) || path
|
|
19
|
+
end
|
|
14
20
|
end
|
|
15
21
|
end
|
|
16
22
|
|
|
@@ -23,32 +29,12 @@ module Searls
|
|
|
23
29
|
end
|
|
24
30
|
end
|
|
25
31
|
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return request.host if s.nil? || s == cur || (s == "" && cur.nil?)
|
|
30
|
-
return root_host(request) || request.host if s == "" && cur
|
|
31
|
-
base = root_host(request) || request.host
|
|
32
|
-
"#{s}.#{base}"
|
|
33
|
-
end
|
|
32
|
+
def normalize_redirect_host(raw)
|
|
33
|
+
v = raw.to_s.strip.downcase
|
|
34
|
+
return if v.blank?
|
|
34
35
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (v = raw.to_s.downcase).present?
|
|
38
|
-
v if /\A[a-z0-9-]+\z/.match?(v)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def root_host(request)
|
|
43
|
-
request.domain.presence || begin
|
|
44
|
-
host = URI.parse(request.base_url).host
|
|
45
|
-
sub = request.subdomain.to_s
|
|
46
|
-
if sub.empty?
|
|
47
|
-
host
|
|
48
|
-
else
|
|
49
|
-
pref = "#{sub}."
|
|
50
|
-
host.start_with?(pref) ? host.delete_prefix(pref) : host
|
|
51
|
-
end
|
|
36
|
+
if /\A[a-z0-9.-]+\z/.match?(v)
|
|
37
|
+
v
|
|
52
38
|
end
|
|
53
39
|
end
|
|
54
40
|
|
|
@@ -67,6 +53,29 @@ module Searls
|
|
|
67
53
|
end
|
|
68
54
|
uri.to_s
|
|
69
55
|
end
|
|
56
|
+
|
|
57
|
+
def append_cross_domain_sso_token(url, request, user, host)
|
|
58
|
+
return if user.blank?
|
|
59
|
+
|
|
60
|
+
token = begin
|
|
61
|
+
provider = Searls::Auth.config.sso_token_for_cross_domain_redirects
|
|
62
|
+
provider&.call(user, request, host)
|
|
63
|
+
end
|
|
64
|
+
return if token.blank?
|
|
65
|
+
|
|
66
|
+
uri = URI.parse(url)
|
|
67
|
+
query = Rack::Utils.parse_nested_query(uri.query)
|
|
68
|
+
query["sso_token"] = token
|
|
69
|
+
uri.query = Rack::Utils.build_nested_query(query)
|
|
70
|
+
uri.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def same_cookie_domain?(request, host)
|
|
74
|
+
domain = request.domain
|
|
75
|
+
return false if domain.blank?
|
|
76
|
+
|
|
77
|
+
host == domain || host.end_with?(".#{domain}")
|
|
78
|
+
end
|
|
70
79
|
end
|
|
71
80
|
end
|
|
72
81
|
end
|
data/lib/searls/auth/config.rb
CHANGED
|
@@ -9,9 +9,11 @@ module Searls
|
|
|
9
9
|
# Core hooks that must always be callable
|
|
10
10
|
HOOK_FIELDS = [
|
|
11
11
|
:user_finder_by_email,
|
|
12
|
+
:user_finder_by_email_for_registration,
|
|
12
13
|
:user_finder_by_id,
|
|
13
14
|
:user_finder_by_token,
|
|
14
15
|
:user_initializer,
|
|
16
|
+
:existing_user_registration_blocked_predicate,
|
|
15
17
|
:token_generator,
|
|
16
18
|
:email_verified_predicate,
|
|
17
19
|
:email_verified_setter,
|
|
@@ -23,9 +25,11 @@ module Searls
|
|
|
23
25
|
:email_verification_mode, # :none, :optional, :required
|
|
24
26
|
# Data setup
|
|
25
27
|
:user_finder_by_email, # proc(email)
|
|
28
|
+
:user_finder_by_email_for_registration, # proc(email)
|
|
26
29
|
:user_finder_by_id, # proc(id)
|
|
27
30
|
:user_finder_by_token, # proc(token)
|
|
28
31
|
:user_initializer, # proc(params)
|
|
32
|
+
:existing_user_registration_blocked_predicate, # proc(user, params)
|
|
29
33
|
:user_name_method, # string
|
|
30
34
|
:token_generator, # proc()
|
|
31
35
|
:password_verifier, # proc(user, password)
|
|
@@ -60,12 +64,15 @@ module Searls
|
|
|
60
64
|
:redirect_path_after_register, # string or proc(user, params, request, routes), all new registrations redirect here
|
|
61
65
|
:redirect_path_after_login, # string or proc(user, params, request, routes), only redirected here if redirect_path param not set
|
|
62
66
|
:redirect_path_after_settings_change, # string or proc(user, params, request, routes), post-settings updates redirect here
|
|
67
|
+
:redirect_url_after_logout, # string or proc(notice, params, request, routes)
|
|
68
|
+
:sso_token_for_cross_domain_redirects, # proc(user, request, target_host)
|
|
63
69
|
# Hook setup
|
|
64
70
|
:validate_registration, # proc(user, params, errors = []), must return an array of error messages where empty means valid
|
|
65
71
|
:after_login_success, # proc(user)
|
|
66
72
|
# Branding setup
|
|
67
73
|
:app_name, # string
|
|
68
74
|
:app_url, # string
|
|
75
|
+
:support_email_address, # string
|
|
69
76
|
:email_banner_image_path, # string
|
|
70
77
|
:email_background_color, # string
|
|
71
78
|
:email_button_color, # string
|
|
@@ -108,6 +115,14 @@ module Searls
|
|
|
108
115
|
auth_methods.include?(:password) && password_reset_enabled
|
|
109
116
|
end
|
|
110
117
|
|
|
118
|
+
def default_redirect_path_after_login
|
|
119
|
+
redirect_path_after_login
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def default_redirect_path_after_login=(value)
|
|
123
|
+
self.redirect_path_after_login = value
|
|
124
|
+
end
|
|
125
|
+
|
|
111
126
|
def password_present?(user)
|
|
112
127
|
predicate = password_present_predicate
|
|
113
128
|
return false if predicate.nil?
|
|
@@ -120,6 +135,7 @@ module Searls
|
|
|
120
135
|
validate_email_verification_mode!
|
|
121
136
|
validate_numeric_options!
|
|
122
137
|
validate_core_hooks!
|
|
138
|
+
ensure_callable_optional!(:sso_token_for_cross_domain_redirects)
|
|
123
139
|
validate_password_settings!
|
|
124
140
|
validate_default_user_hooks!
|
|
125
141
|
rescue => e
|
|
@@ -224,6 +240,7 @@ module Searls
|
|
|
224
240
|
def validate_default_user_hooks!
|
|
225
241
|
hooks_pointing_at_user = [
|
|
226
242
|
:user_finder_by_email,
|
|
243
|
+
:user_finder_by_email_for_registration,
|
|
227
244
|
:user_finder_by_id,
|
|
228
245
|
:user_finder_by_token,
|
|
229
246
|
:user_initializer,
|
|
@@ -263,6 +280,17 @@ module Searls
|
|
|
263
280
|
end
|
|
264
281
|
end
|
|
265
282
|
|
|
283
|
+
if public_send(:user_finder_by_email_for_registration).equal?(Searls::Auth::DEFAULT_CONFIG[:user_finder_by_email_for_registration])
|
|
284
|
+
unless ::User.respond_to?(:find_by)
|
|
285
|
+
raise Searls::Auth::Error, "Default :user_finder_by_email_for_registration expects User.find_by(email: ...) to exist."
|
|
286
|
+
end
|
|
287
|
+
has_email_method = ::User.method_defined?(:email)
|
|
288
|
+
has_email_column = ::User.respond_to?(:column_names) && ::User.column_names.include?("email")
|
|
289
|
+
unless schema_checks_blocked? || has_email_method || has_email_column
|
|
290
|
+
raise Searls::Auth::Error, "Default :user_finder_by_email_for_registration expects a `users.email` attribute."
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
266
294
|
if public_send(:user_finder_by_token).equal?(Searls::Auth::DEFAULT_CONFIG[:user_finder_by_token])
|
|
267
295
|
unless ::User.respond_to?(:find_by_token_for)
|
|
268
296
|
raise Searls::Auth::Error, "Default :user_finder_by_token expects User.find_by_token_for(:email_auth, token) (Rails signed_id API)."
|
|
@@ -7,10 +7,10 @@ module Searls
|
|
|
7
7
|
Result = Struct.new(:user, :success?, :error_messages)
|
|
8
8
|
|
|
9
9
|
def call(params)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
found = Searls::Auth.config.user_finder_by_email_for_registration.call(params[:email])
|
|
11
|
+
user = found || Searls::Auth.config.user_initializer.call(params)
|
|
12
12
|
|
|
13
|
-
if user.persisted?
|
|
13
|
+
if user.persisted? && Searls::Auth.config.existing_user_registration_blocked_predicate.call(user, params)
|
|
14
14
|
Result.new(nil, false, ["An account already exists for that email address. <a href=\"#{login_path(**forwardable_params(params))}\">Log in</a> instead?"])
|
|
15
15
|
elsif (errors = Searls::Auth.config.validate_registration.call(user, params, [])).any?
|
|
16
16
|
Result.new(nil, false, errors)
|
|
@@ -32,13 +32,13 @@ module Searls
|
|
|
32
32
|
private
|
|
33
33
|
|
|
34
34
|
def forwardable_params(params)
|
|
35
|
-
params.permit(:redirect_path, :
|
|
35
|
+
params.permit(:redirect_path, :redirect_host, :email)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def simplified_error_messages(model)
|
|
39
39
|
model.errors.details.keys.map { |attr|
|
|
40
40
|
model.errors.full_messages_for(attr).first
|
|
41
|
-
}.
|
|
41
|
+
}.compact
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -3,13 +3,13 @@ module Searls
|
|
|
3
3
|
class DeliversPasswordReset
|
|
4
4
|
Result = Struct.new(:success?, keyword_init: true)
|
|
5
5
|
|
|
6
|
-
def deliver(user:, redirect_path: nil,
|
|
6
|
+
def deliver(user:, redirect_path: nil, redirect_host: nil)
|
|
7
7
|
token = Searls::Auth.config.password_reset_token_generator.call(user)
|
|
8
8
|
PasswordResetMailer.with(
|
|
9
9
|
user:,
|
|
10
10
|
token:,
|
|
11
11
|
redirect_path:,
|
|
12
|
-
|
|
12
|
+
redirect_host:
|
|
13
13
|
).password_reset.deliver_later
|
|
14
14
|
Result.new(success?: true)
|
|
15
15
|
end
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
module Searls
|
|
2
2
|
module Auth
|
|
3
3
|
class EmailsLink
|
|
4
|
-
def email(user:, email_otp:, redirect_path: nil,
|
|
4
|
+
def email(user:, email_otp:, redirect_path: nil, redirect_host: nil)
|
|
5
5
|
LoginLinkMailer.with(
|
|
6
6
|
user:,
|
|
7
7
|
token: (Searls::Auth.config.auth_methods.include?(:email_link) ? generate_token!(user) : nil),
|
|
8
8
|
email_otp: (Searls::Auth.config.auth_methods.include?(:email_otp) ? email_otp : nil),
|
|
9
9
|
redirect_path:,
|
|
10
|
-
|
|
10
|
+
redirect_host:
|
|
11
11
|
).login_link.deliver_later
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
module Searls
|
|
2
2
|
module Auth
|
|
3
3
|
class EmailsVerification
|
|
4
|
-
def email(user:, redirect_path: nil,
|
|
4
|
+
def email(user:, redirect_path: nil, redirect_host: nil)
|
|
5
5
|
EmailVerificationMailer.with(
|
|
6
6
|
user:,
|
|
7
7
|
token: generate_token!(user),
|
|
8
8
|
redirect_path:,
|
|
9
|
-
|
|
9
|
+
redirect_host:
|
|
10
10
|
).verification_email.deliver_later
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module Searls
|
|
2
|
+
module Auth
|
|
3
|
+
class SanitizesFlashHtml
|
|
4
|
+
def initialize
|
|
5
|
+
require "rails-html-sanitizer"
|
|
6
|
+
@safe_list_sanitizer = Rails::HTML::SafeListSanitizer.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def sanitize(html)
|
|
10
|
+
@safe_list_sanitizer.sanitize(
|
|
11
|
+
html,
|
|
12
|
+
tags: %w[a b br code em i li ol p strong u ul],
|
|
13
|
+
attributes: %w[data-turbo-confirm data-turbo-method href rel target title]
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/searls/auth/version.rb
CHANGED
data/lib/searls/auth.rb
CHANGED
|
@@ -3,6 +3,7 @@ require_relative "auth/parses_time_safely"
|
|
|
3
3
|
require_relative "auth/config"
|
|
4
4
|
require_relative "auth/builds_target_redirect_url" if defined?(Rails)
|
|
5
5
|
require_relative "auth/creates_user" if defined?(Rails)
|
|
6
|
+
require_relative "auth/sanitizes_flash_html" if defined?(Rails)
|
|
6
7
|
require_relative "auth/emails_link"
|
|
7
8
|
require_relative "auth/emails_verification"
|
|
8
9
|
require_relative "auth/engine" if defined?(Rails)
|
|
@@ -22,9 +23,11 @@ module Searls
|
|
|
22
23
|
email_verification_mode: :none,
|
|
23
24
|
# Data setup
|
|
24
25
|
user_finder_by_email: ->(email) { User.find_by(email:) },
|
|
26
|
+
user_finder_by_email_for_registration: ->(email) { User.find_by(email:) },
|
|
25
27
|
user_finder_by_id: ->(id) { User.find_by(id:) },
|
|
26
28
|
user_finder_by_token: ->(token) { User.find_by_token_for(:email_auth, token) },
|
|
27
29
|
user_initializer: ->(params) { User.new(email: params[:email]) },
|
|
30
|
+
existing_user_registration_blocked_predicate: ->(_user, _params) { true },
|
|
28
31
|
user_name_method: "name",
|
|
29
32
|
token_generator: ->(user) { user.generate_token_for(:email_auth) },
|
|
30
33
|
email_otp_expiry_minutes: 30,
|
|
@@ -67,6 +70,7 @@ module Searls
|
|
|
67
70
|
redirect_path_after_settings_change: ->(user, params, request, routes) {
|
|
68
71
|
routes.respond_to?(:edit_settings_path) ? routes.edit_settings_path : "/settings"
|
|
69
72
|
},
|
|
73
|
+
sso_token_for_cross_domain_redirects: nil,
|
|
70
74
|
# Hook setup
|
|
71
75
|
validate_registration: ->(user, params, errors) { errors },
|
|
72
76
|
after_login_success: ->(user) {},
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: searls-auth
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Justin Searls
|
|
@@ -34,6 +34,7 @@ files:
|
|
|
34
34
|
- LICENSE.txt
|
|
35
35
|
- README.md
|
|
36
36
|
- Rakefile
|
|
37
|
+
- TODO.md
|
|
37
38
|
- app/controllers/searls/auth/base_controller.rb
|
|
38
39
|
- app/controllers/searls/auth/email_verifications_controller.rb
|
|
39
40
|
- app/controllers/searls/auth/logins_controller.rb
|
|
@@ -80,6 +81,7 @@ files:
|
|
|
80
81
|
- lib/searls/auth/railtie.rb
|
|
81
82
|
- lib/searls/auth/resets_password.rb
|
|
82
83
|
- lib/searls/auth/resets_session.rb
|
|
84
|
+
- lib/searls/auth/sanitizes_flash_html.rb
|
|
83
85
|
- lib/searls/auth/updates_settings.rb
|
|
84
86
|
- lib/searls/auth/version.rb
|
|
85
87
|
- script/setup
|