searls-auth 0.2.0 → 1.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -1
  3. data/README.md +162 -0
  4. data/app/controllers/searls/auth/base_controller.rb +42 -21
  5. data/app/controllers/searls/auth/email_verifications_controller.rb +57 -0
  6. data/app/controllers/searls/auth/logins_controller.rb +60 -39
  7. data/app/controllers/searls/auth/registrations_controller.rb +84 -32
  8. data/app/controllers/searls/auth/requests_password_resets_controller.rb +55 -0
  9. data/app/controllers/searls/auth/resets_passwords_controller.rb +73 -0
  10. data/app/controllers/searls/auth/settings_controller.rb +83 -0
  11. data/app/controllers/searls/auth/verifications_controller.rb +31 -61
  12. data/app/helpers/searls/auth/application_helper.rb +9 -5
  13. data/app/mailers/searls/auth/base_mailer.rb +1 -1
  14. data/app/mailers/searls/auth/email_verification_mailer.rb +29 -0
  15. data/app/mailers/searls/auth/login_link_mailer.rb +3 -3
  16. data/app/mailers/searls/auth/password_reset_mailer.rb +29 -0
  17. data/app/views/searls/auth/email_verification_mailer/verification_email.html.erb +23 -0
  18. data/app/views/searls/auth/email_verification_mailer/verification_email.text.erb +6 -0
  19. data/app/views/searls/auth/login_link_mailer/login_link.html.erb +5 -5
  20. data/app/views/searls/auth/login_link_mailer/login_link.text.erb +4 -5
  21. data/app/views/searls/auth/logins/show.html.erb +12 -4
  22. data/app/views/searls/auth/password_reset_mailer/password_reset.html.erb +23 -0
  23. data/app/views/searls/auth/password_reset_mailer/password_reset.text.erb +6 -0
  24. data/app/views/searls/auth/registrations/pending_email_verification.html.erb +12 -0
  25. data/app/views/searls/auth/registrations/show.html.erb +1 -2
  26. data/app/views/searls/auth/requests_password_resets/show.html.erb +17 -0
  27. data/app/views/searls/auth/resets_passwords/show.html.erb +26 -0
  28. data/app/views/searls/auth/settings/edit.html.erb +31 -0
  29. data/app/views/searls/auth/shared/_login_fields.html.erb +11 -0
  30. data/app/views/searls/auth/shared/_register_fields.html.erb +15 -0
  31. data/config/routes.rb +11 -0
  32. data/lib/searls/auth/authenticates_user.rb +54 -10
  33. data/lib/searls/auth/builds_target_redirect_url.rb +72 -0
  34. data/lib/searls/auth/config.rb +243 -12
  35. data/lib/searls/auth/creates_user.rb +12 -4
  36. data/lib/searls/auth/delivers_password_reset.rb +18 -0
  37. data/lib/searls/auth/emails_link.rb +2 -2
  38. data/lib/searls/auth/emails_verification.rb +33 -0
  39. data/lib/searls/auth/parses_time_safely.rb +34 -0
  40. data/lib/searls/auth/railtie.rb +0 -1
  41. data/lib/searls/auth/resets_password.rb +41 -0
  42. data/lib/searls/auth/updates_settings.rb +149 -0
  43. data/lib/searls/auth/version.rb +1 -1
  44. data/lib/searls/auth.rb +62 -13
  45. metadata +23 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 608a7738fece38d0fce18f906f68c5d91122aaf2d274cdcf48dba6b770c04069
4
- data.tar.gz: 76d7b5e89479236705a1cb866d7358016510fa9899dffd2474fe9139cfc688bf
3
+ metadata.gz: '09b9515ef89e82e747cc35c415a4de145cf8e61634caccb137ceb8f429a54bdc'
4
+ data.tar.gz: e5b456ac99cc9ad784ed8727266dc3e073e6ee850ad720be0a4a42e894e2bbe8
5
5
  SHA512:
6
- metadata.gz: c23b8352aaeec522b1878bf3b32c721cd14d06ef31b306a23fcb7e8b1ca1b3659a39904409e30c4492724289057b1a4594486e7878d01304b2830a7ed39e3ecf
7
- data.tar.gz: 387b365eb74882a67258552f0da17d58913d001ea574fcf4d4c2b0c1487aeac939e6431e210c0b8bbb1471c00bfd01591e3f6aa6969520cee7727850a4d8552f
6
+ metadata.gz: 6c5163daa1551674b1c128ecf8a6d155cde36123780bf2332ef5d9f79166d37a964616f6fda17c97eb2608df9d13ad54f6f25e5c384ef2308b8740f93e326302
7
+ data.tar.gz: 4bfdd8c989591a52aeb52cf91bbf55e189442e57031d877f0e81f88c45d51d8cd22bd8ca21f31be10370c68b3981e410b2a034ed6c69738b28ee10cfcca078d0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0] - 2025-10-04
4
+
5
+ * **BREAKING:** Rename `default_redirect_path_after_register` to `redirect_path_after_register`
6
+ * **BREAKING:** Rename `flash_notice_after_verification` to `flash_notice_after_login`
7
+ * Add password reset flow with default controllers, mailer, views, and configuration hooks
8
+ * Add `before_password_reset` hook to optionally throttle or reject reset requests
9
+ * Add configurable `password_reset_request_view` and `password_reset_edit_view` settings
10
+ * Add `password_reset_enabled` flag to disable the forgot-password link/flow when email delivery is unavailable
11
+ * Add account settings controller/view for password rotation and email changes, plus related configuration hooks
12
+ * Switch from flash[:error] to the conventional flash[:alert] (TIL, 20 years in that :alert is more common)
13
+
3
14
  ## [0.2.0] - 2025-09-11
4
15
 
5
16
  * Add `auth_methods` configuration with default `[:email_link, :email_otp]`
@@ -10,7 +21,7 @@
10
21
 
11
22
  ## [0.1.0] - 2025-04-26
12
23
 
13
- * Add `max_allowed_short_code_attempts` configuration, beyond which the code is erased from the session and the user needs to login again (default: 10)
24
+ * Add `max_allowed_email_otp_attempts` configuration, beyond which the code is erased from the session and the user needs to login again (default: 10)
14
25
  * Allow configuration of flash messages
15
26
  * Fix a routing error if the user is already registered
16
27
 
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # searls-auth
2
2
 
3
+ [![Certified Shovelware](https://justin.searls.co/img/shovelware.svg)](https://justin.searls.co/shovelware/)
4
+
3
5
  This gem provides a Ruby on Rails engine that implements a minimal, opinionated, and pleasant email-based authentication system. It has zero other dependencies, which is the correct number of dependencies.
4
6
 
5
7
  For a detailed walk-through with pictures and whatnot, check out this [example app README](/example/simple_app/README.md). Below you'll find the basic steps for getting started.
@@ -93,6 +95,9 @@ Rails.application.config.after_initialize do
93
95
  # Defaults:
94
96
  config.auth_methods = [:email_link, :email_otp]
95
97
 
98
+ # Email OTPs expire after 30 minutes by default.
99
+ # config.email_otp_expiry_minutes = 10
100
+
96
101
  # Link-only (no code in emails, no OTP input shown):
97
102
  # config.auth_methods = :email_link
98
103
 
@@ -104,6 +109,163 @@ end
104
109
 
105
110
  One reason you might want to disable e-mail OTP is that it exposes your users to [a pretty easy-to-implement man-in-the-middle attack](https://blog.danielh.cc/blog/passwords).
106
111
 
112
+ ### Email verification modes
113
+
114
+ Control whether registration triggers verification emails and whether password login requires a verified email.
115
+
116
+ ```ruby
117
+ # config/initializers/searls_auth.rb
118
+ Rails.application.config.after_initialize do
119
+ Searls::Auth.configure do |config|
120
+ # :none (default): No verification emails on registration; password login allowed immediately.
121
+ # :optional: Send a verification email on registration, allow password login, but remind users until verified.
122
+ # :required: Send a verification email on registration and block password login until verified.
123
+ config.email_verification_mode = :none # or :optional, :required
124
+ end
125
+ end
126
+ ```
127
+
128
+ If you enable the built‑in password login (`config.auth_methods` includes `:password`), we assume your `User` model uses `has_secure_password` (or you can provide custom hooks via `password_verifier` and `password_setter`). Verification status is checked via `email_verified_at` by default and can be customized with `email_verified_predicate`/`email_verified_setter`.
129
+
130
+ ### Password login
131
+
132
+ Enabling `:password` adds email+password fields to the login and registration flows. Minimal setup looks like this:
133
+
134
+ ```ruby
135
+ # app/models/user.rb
136
+ class User < ApplicationRecord
137
+ has_secure_password
138
+
139
+ # uncomment if enabling auth_methods :email_link or :email_otp
140
+ # generates_token_for :email_auth, expires_in: 30.minutes
141
+ end
142
+
143
+ # db/migrate/XXXX_add_password_columns.rb
144
+ class AddPasswordColumns < ActiveRecord::Migration[8.0]
145
+ def change
146
+ add_column :users, :password_digest, :string
147
+ add_column :users, :email_verified_at, :datetime
148
+ end
149
+ end
150
+
151
+ # config/initializers/searls_auth.rb
152
+ Rails.application.config.after_initialize do
153
+ Searls::Auth.configure do |config|
154
+ config.auth_methods = [:password] # or any combination like [:password, :email_link, :email_otp]
155
+ config.email_verification_mode = :required # :none and :optional supported too
156
+ end
157
+ end
158
+ ```
159
+
160
+ If you already have legacy password hashing, override `password_verifier`/`password_setter` to wrap it, otherwise we'll use conventional `bcrypt` with `has_secure_password` and `password_digest` comparisons. Likewise, if email verification lives on a different column or association, use `email_verified_predicate`/`email_verified_setter` to adapt.
161
+
162
+ All successful logins still render through the same flows, so make sure your app handles `session[:user_id]` uniformly regardless of which auth method succeeded.
163
+
164
+ If a registration request doesn't supply a `redirect_path` parameter, searls-auth now falls back to `config.redirect_path_after_register` when choosing both the post-registration redirect and the link embedded in verification emails. Override that proc to point brand-new users somewhere more purposeful than the default root path.
165
+
166
+ Likewise, successful logins fall back to `config.redirect_path_after_login` whenever no redirect parameters are supplied. Set it to a dashboard or home screen to spare users from landing on `/`.
167
+
168
+ ### Password reset
169
+
170
+ When `auth_methods` includes `:password`, the engine renders a "Forgot your password?" link beneath the login form. Clicking it walks through a two-step flow: request a reset email and then choose a new password. To enable it, make sure your `User` model issues a token named `:password_reset`. If your app cannot send email, disable the link entirely with `config.password_reset_enabled = false`.
171
+
172
+ ```ruby
173
+ # app/models/user.rb
174
+ class User < ApplicationRecord
175
+ has_secure_password
176
+
177
+ # You can skip this if password reset is not enabled:
178
+ generates_token_for :password_reset, expires_in: 30.minutes
179
+ end
180
+ ```
181
+
182
+ Adjust the expiry window by updating the `expires_in` value above or by providing a custom generator via configuration.
183
+
184
+ Need rate limiting or business rules before delivering reset emails? Return `false` or `:halt` from `before_password_reset` to silently skip sending while preserving the standard response.
185
+
186
+ By default we generate tokens via `generates_token_for`, send mail from `Searls::Auth::PasswordResetMailer`, and log the user in immediately after a successful reset. You can override any piece of that behavior:
187
+
188
+ ```ruby
189
+ Searls::Auth.configure do |config|
190
+ config.password_reset_token_generator = ->(user) { user.generate_token_for(:password_reset) }
191
+ config.password_reset_token_finder = ->(token) { PasswordResetTokenStore.lookup(token) }
192
+ config.auto_login_after_password_reset = false # redirect back to login instead
193
+ config.mail_password_reset_template_path = "my_auth/password_reset_mailer"
194
+ config.mail_password_reset_template_name = "email"
195
+ config.before_password_reset = ->(user, params, controller) do
196
+ PasswordResetThrottle.allow?(user_id: user&.id, ip: controller.request.remote_ip)
197
+ end
198
+ config.password_reset_request_view = "my_auth/password_resets/request"
199
+ config.password_reset_edit_view = "my_auth/password_resets/edit"
200
+ config.password_reset_enabled = false if Rails.env.development? # hide link without SMTP
201
+ end
202
+ ```
203
+
204
+ ### Account settings
205
+
206
+ When password authentication is enabled you can wire any settings UI to the engine by posting to `searls_auth.settings_path`. A simple Rails form looks like:
207
+
208
+ ```erb
209
+ <%= form_with scope: :settings,
210
+ url: searls_auth.settings_path,
211
+ method: :patch,
212
+ local: true do |f| %>
213
+ <%= f.password_field :current_password %>
214
+ <%= f.password_field :password %>
215
+ <%= f.password_field :password_confirmation %>
216
+ <%= f.submit "Save" %>
217
+ <% end %>
218
+ ```
219
+
220
+ The controller will rotate or set passwords, requiring the current password when appropriate.
221
+
222
+ Configure where users land afterwards by overriding `config.redirect_path_after_settings_change` in your initializer so it points back to your own settings page:
223
+
224
+ ```ruby
225
+ Searls::Auth.configure do |config|
226
+ config.redirect_path_after_settings_change = ->(_user, _params, _request, routes) { routes.settings_path }
227
+ end
228
+ ```
229
+
230
+ If you track password state differently, provide your own `config.password_present_predicate`. You can also adjust the flash messages: `flash_notice_after_settings_update`, `flash_error_after_settings_current_password_missing`, and `flash_error_after_settings_current_password_invalid`.
231
+
232
+ Want to tweak copy? Override the flash messages `flash_notice_after_password_reset_email`, `flash_notice_after_password_reset`, `flash_error_after_password_reset_token_invalid`, `flash_error_after_password_reset_password_mismatch`, and `flash_error_after_password_reset_password_blank`, or shadow the mailer templates at `app/views/searls/auth/password_reset_mailer/password_reset.html.erb` and `.text.erb`.
233
+
234
+ #### Triggering a (re)verification email
235
+
236
+ Users can request another verification email. The engine exposes a PATCH endpoint and helper you can call from your app:
237
+
238
+ ```erb
239
+ <%# Anywhere in your app %>
240
+ <%= link_to "Resend verification email",
241
+ searls_auth.resend_email_verification_path,
242
+ data: { turbo_method: :patch } %>
243
+ ```
244
+
245
+ This uses the same mailer and template as login emails. You can override the template in two ways:
246
+
247
+ - Configure the template path/name:
248
+
249
+ ```ruby
250
+ Searls::Auth.configure do |config|
251
+ config.mail_login_template_path = "my_auth/mailer"
252
+ config.mail_login_template_name = "login_link"
253
+ end
254
+ ```
255
+
256
+ - Or create views that shadow the engine’s defaults at `app/views/searls/auth/login_link_mailer/login_link.html.erb` and `.text.erb` in your app.
257
+
258
+ ### Common configurations
259
+
260
+ | `auth_methods` | `email_verification_mode` | Behavior |
261
+ | --- | --- | --- |
262
+ | `[:email_link, :email_otp]` (default) | `:none` | Passwordless magic link + email OTP. Registration links go straight to the verify screen. |
263
+ | `[:password]` | `:none` | Classic email/password. No email is sent; verify routes redirect back to login. |
264
+ | `[: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
+ | `[: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
+
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 (cross-subdomain redirects still work via `redirect_subdomain`).
268
+
107
269
  ## Use it
108
270
 
109
271
  Of course, having a user be "logged in" or not doesn't mean anything if your application doesn't do anything with the knowledge. Users that are logged in will have `session[:user_id]` set to the value of the logged-in user's ID. Logged out users won't have anything set to `session[:user_id]`. What you do with that is your job, not this gem. (Wait, after 20 years does this mean I finally understand the difference between authentication and authorization? Better late than never.)
@@ -2,40 +2,61 @@ require "securerandom"
2
2
 
3
3
  module Searls
4
4
  module Auth
5
- class BaseController < ApplicationController # TODO should this be ActionController::Base? Trade-offs?
5
+ class BaseController < ApplicationController
6
6
  helper Rails.application.helpers
7
7
  helper Rails.application.routes.url_helpers
8
+ helper_method :forwardable_params
9
+
10
+ def forwardable_params
11
+ {redirect_path: params[:redirect_path], redirect_subdomain: params[:redirect_subdomain]}.compact_blank
12
+ end
8
13
 
9
14
  protected
10
15
 
11
- def searls_auth_config
12
- Searls::Auth.config
16
+ def attach_email_otp_to_session!(user)
17
+ session[:searls_auth_email_otp_user_id] = user.id
18
+ session[:searls_auth_email_otp] = SecureRandom.random_number(1000000).to_s.rjust(6, "0")
19
+ session[:searls_auth_email_otp_generated_at] = Time.current
20
+ session[:searls_auth_email_otp_verification_attempts] = 0
13
21
  end
14
22
 
15
- def attach_short_code_to_session!(user)
16
- session[:searls_auth_short_code_user_id] = user.id
17
- session[:searls_auth_short_code] = SecureRandom.random_number(1000000).to_s.rjust(6, "0")
18
- session[:searls_auth_short_code_generated_at] = Time.current
19
- session[:searls_auth_short_code_verification_attempts] = 0
23
+ def reset_expired_email_otp
24
+ generated_at = session[:searls_auth_email_otp_generated_at]
25
+ cutoff = Searls::Auth.config.email_otp_expiry_minutes.minutes.ago
26
+ unless generated_at.present? && (parsed = Searls::Auth::ParsesTimeSafely.new.parse(generated_at)) && parsed > cutoff
27
+ clear_email_otp_from_session!
28
+ end
20
29
  end
21
30
 
22
- def reset_expired_short_code
23
- if session[:searls_auth_short_code_generated_at].present? &&
24
- Time.zone.parse(session[:searls_auth_short_code_generated_at]) < Searls::Auth.config.token_expiry_minutes.minutes.ago
25
- clear_short_code_from_session!
26
- end
31
+ def clear_email_otp_from_session!
32
+ session.delete(:searls_auth_email_otp_user_id)
33
+ session.delete(:searls_auth_email_otp_generated_at)
34
+ session.delete(:searls_auth_email_otp)
35
+ session.delete(:searls_auth_email_otp_verification_attempts)
27
36
  end
28
37
 
29
- def clear_short_code_from_session!
30
- session.delete(:searls_auth_short_code_user_id)
31
- session.delete(:searls_auth_short_code_generated_at)
32
- session.delete(:searls_auth_short_code)
33
- session.delete(:searls_auth_short_code_verification_attempts)
38
+ def log_email_otp_verification_attempt!
39
+ session[:searls_auth_email_otp_verification_attempts] ||= 0
40
+ session[:searls_auth_email_otp_verification_attempts] += 1
34
41
  end
35
42
 
36
- def log_short_code_verification_attempt!
37
- session[:searls_auth_short_code_verification_attempts] ||= 0
38
- session[:searls_auth_short_code_verification_attempts] += 1
43
+ def target_redirect_url
44
+ Searls::Auth::BuildsTargetRedirectUrl.new.build(request, params)
45
+ end
46
+
47
+ def redirect_with_host_awareness(target)
48
+ redirect_to target, allow_other_host: target&.start_with?("http://", "https://")
49
+ end
50
+
51
+ def redirect_after_login(user)
52
+ if (target = target_redirect_url)
53
+ redirect_with_host_awareness(target)
54
+ else
55
+ redirect_to Searls::Auth.config.resolve(
56
+ :redirect_path_after_login,
57
+ user, params, request, main_app
58
+ ) || searls_auth.login_path
59
+ end
39
60
  end
40
61
  end
41
62
  end
@@ -0,0 +1,57 @@
1
+ module Searls
2
+ module Auth
3
+ class EmailVerificationsController < BaseController
4
+ def resend
5
+ user = if session[:user_id].present?
6
+ Searls::Auth.config.user_finder_by_id.call(session[:user_id])
7
+ elsif session[:searls_auth_pending_email].present?
8
+ Searls::Auth.config.user_finder_by_email.call(session[:searls_auth_pending_email])
9
+ end
10
+
11
+ if user.blank? || Searls::Auth.config.email_verification_mode == :none
12
+ flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_verify_attempt_invalid_link, params)
13
+ else
14
+ EmailsVerification.new.email(user: user, **forwardable_params)
15
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_verification_email_resent, params)
16
+ end
17
+
18
+ if session[:user_id].present?
19
+ redirect_to searls_auth.verify_path
20
+ else
21
+ fallback = if session[:searls_auth_pending_email].present?
22
+ searls_auth.pending_email_verification_path({
23
+ email: session[:searls_auth_pending_email],
24
+ redirect_path: session[:searls_auth_pending_redirect_path],
25
+ redirect_subdomain: session[:searls_auth_pending_redirect_subdomain]
26
+ }.compact_blank)
27
+ else
28
+ searls_auth.login_path
29
+ end
30
+ redirect_back fallback_location: fallback
31
+ end
32
+ end
33
+
34
+ def show
35
+ user = Searls::Auth.config.user_finder_by_token.call(params[:token])
36
+
37
+ if user.present?
38
+ unless Searls::Auth.config.email_verified_predicate.call(user)
39
+ Searls::Auth.config.email_verified_setter.call(user)
40
+ end
41
+
42
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_email_verified, user, params)
43
+
44
+ Searls::Auth.config.after_login_success.call(user)
45
+ session[:user_id] = user.id
46
+ session[:has_logged_in_before] = true
47
+
48
+ redirect_after_login(user)
49
+ else
50
+ flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_verify_attempt_invalid_link, params)
51
+
52
+ redirect_to searls_auth.login_path(**forwardable_params)
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,58 +1,79 @@
1
1
  module Searls
2
2
  module Auth
3
3
  class LoginsController < BaseController
4
- before_action :reset_expired_short_code
4
+ before_action :reset_expired_email_otp
5
5
 
6
6
  def show
7
- render searls_auth_config.login_view, layout: searls_auth_config.layout
7
+ render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout
8
8
  end
9
9
 
10
10
  def create
11
- user = searls_auth_config.user_finder_by_email.call(params[:email])
11
+ if Searls::Auth.config.auth_methods.include?(:password) && params[:send_login_email].blank?
12
+ handle_password_login
13
+ else
14
+ handle_email_login
15
+ end
16
+ end
12
17
 
13
- if user.present?
14
- if searls_auth_config.auth_methods.include?(:email_otp)
15
- attach_short_code_to_session!(user)
18
+ def destroy
19
+ ResetsSession.new.reset(self, except_for: [:has_logged_in_before])
20
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_logout, params)
21
+ redirect_to searls_auth.login_path
22
+ end
23
+
24
+ private
25
+
26
+ def handle_password_login
27
+ authenticator = AuthenticatesUser.new
28
+ result = authenticator.authenticate_by_password(params[:email], params[:password], session)
29
+
30
+ if result.success?
31
+ session[:user_id] = result.user.id
32
+ session[:has_logged_in_before] = true
33
+
34
+ flash[:notice] = if Searls::Auth.config.email_verification_mode != :none && !Searls::Auth.config.email_verified_predicate.call(result.user)
35
+ resend_path = searls_auth.resend_email_verification_path(**forwardable_params)
36
+ Searls::Auth.config.resolve(:flash_notice_after_login_with_unverified_email, resend_path, params)
16
37
  else
17
- clear_short_code_from_session!
38
+ Searls::Auth.config.resolve(:flash_notice_after_login, result.user, params)
18
39
  end
19
-
20
- EmailsLink.new.email(
21
- user:,
22
- redirect_path: params[:redirect_path],
23
- redirect_subdomain: params[:redirect_subdomain],
24
- short_code: session[:searls_auth_short_code]
25
- )
26
- flash[:notice] = searls_auth_config.resolve(
27
- :flash_notice_after_login_attempt,
28
- user, params
29
- )
30
- redirect_to searls_auth.verify_path(
31
- redirect_path: params[:redirect_path],
32
- redirect_subdomain: params[:redirect_subdomain]
33
- )
40
+ redirect_after_login(result.user)
41
+ elsif result.email_unverified?
42
+ session[:searls_auth_pending_email] = params[:email]
43
+ resend_path = searls_auth.resend_email_verification_path(**forwardable_params)
44
+ flash.now[:alert] = Searls::Auth.config.resolve(:flash_error_after_login_attempt_unverified_email, resend_path, params)
45
+ render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout, status: :unprocessable_content
34
46
  else
35
- flash.now[:error] = searls_auth_config.resolve(
36
- :flash_error_after_login_attempt_unknown_email,
37
- searls_auth.register_path(
38
- email: params[:email],
39
- redirect_path: params[:redirect_path],
40
- redirect_subdomain: params[:redirect_subdomain]
41
- ),
42
- params
43
- )
44
- render searls_auth_config.login_view, layout: searls_auth_config.layout, status: :unprocessable_entity
47
+ user = Searls::Auth.config.user_finder_by_email.call(params[:email])
48
+ flash.now[:alert] = if user.blank?
49
+ Searls::Auth.config.resolve(:flash_error_after_login_attempt_unknown_email, searls_auth.register_path(email: params[:email], **forwardable_params), params)
50
+ else
51
+ Searls::Auth.config.resolve(:flash_error_after_login_attempt_invalid_password, params)
52
+ end
53
+ render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout, status: :unprocessable_content
45
54
  end
55
+ rescue NameError
56
+ flash.now[:alert] = Searls::Auth.config.resolve(:flash_error_after_password_misconfigured, params)
57
+ render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout, status: :unprocessable_content
46
58
  end
47
59
 
48
- def destroy
49
- ResetsSession.new.reset(self, except_for: [:has_logged_in_before])
60
+ def handle_email_login
61
+ user = Searls::Auth.config.user_finder_by_email.call(params[:email])
50
62
 
51
- flash[:notice] = searls_auth_config.resolve(
52
- :flash_notice_after_logout,
53
- params
54
- )
55
- redirect_to searls_auth.login_path
63
+ if user.present?
64
+ if Searls::Auth.config.auth_methods.include?(:email_otp)
65
+ attach_email_otp_to_session!(user)
66
+ else
67
+ clear_email_otp_from_session!
68
+ end
69
+
70
+ EmailsLink.new.email(user:, email_otp: session[:searls_auth_email_otp], **forwardable_params)
71
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_login_attempt, user, params)
72
+ redirect_to searls_auth.verify_path(**forwardable_params)
73
+ else
74
+ flash.now[:alert] = Searls::Auth.config.resolve(:flash_error_after_login_attempt_unknown_email, searls_auth.register_path(email: params[:email], **forwardable_params), params)
75
+ render Searls::Auth.config.login_view, layout: Searls::Auth.config.layout, status: :unprocessable_content
76
+ end
56
77
  end
57
78
  end
58
79
  end
@@ -2,47 +2,99 @@ module Searls
2
2
  module Auth
3
3
  class RegistrationsController < BaseController
4
4
  def show
5
- render searls_auth_config.register_view, layout: searls_auth_config.layout
5
+ render Searls::Auth.config.register_view, layout: Searls::Auth.config.layout
6
6
  end
7
7
 
8
8
  def create
9
9
  result = CreatesUser.new.call(params)
10
10
 
11
11
  if result.success?
12
- attach_short_code_to_session!(result.user)
13
-
14
- redirect_params = {
15
- redirect_path: searls_auth_config.resolve(
16
- :redirect_path_after_register,
17
- result.user, params, request, main_app
18
- )
19
- }
20
-
21
- EmailsLink.new.email(
22
- user: result.user,
23
- short_code: session[:searls_auth_short_code],
24
- **redirect_params
25
- )
26
- flash[:notice] = searls_auth_config.resolve(
27
- :flash_notice_after_registration,
28
- result.user, params
29
- )
30
-
31
- redirect_to searls_auth.verify_path(**redirect_params)
12
+ user = result.user
13
+ if password_registration?
14
+ handle_password_registration(user)
15
+ else
16
+ handle_email_registration(user)
17
+ end
32
18
  else
33
- flash.now[:error] = searls_auth_config.resolve(
34
- :flash_error_after_register_attempt,
35
- result.error_messages,
36
- searls_auth.login_path(
37
- email: params[:email],
38
- redirect_path: params[:redirect_path],
39
- redirect_subdomain: params[:redirect_subdomain]
40
- ),
41
- params
42
- )
43
- render searls_auth_config.register_view, layout: searls_auth_config.layout, status: :unprocessable_entity
19
+ flash.now[:alert] = Searls::Auth.config.resolve(:flash_error_after_register_attempt, result.error_messages, searls_auth.login_path(email: params[:email], **forwardable_params), params)
20
+ render Searls::Auth.config.register_view, layout: Searls::Auth.config.layout, status: :unprocessable_content
44
21
  end
45
22
  end
23
+
24
+ def pending_email_verification
25
+ render Searls::Auth.config.pending_email_verification_view, layout: Searls::Auth.config.layout
26
+ end
27
+
28
+ private
29
+
30
+ def password_registration?
31
+ Searls::Auth.config.auth_methods.include?(:password) && params[:password].present?
32
+ end
33
+
34
+ def email_methods_enabled?
35
+ (Searls::Auth.config.auth_methods & [:email_link, :email_otp]).any?
36
+ end
37
+
38
+ def handle_password_registration(user)
39
+ target_path, target_subdomain = registration_redirect_destination(user)
40
+ if Searls::Auth.config.email_verification_mode != :none
41
+ EmailsVerification.new.email(user: user, redirect_path: target_path, redirect_subdomain: target_subdomain)
42
+ session[:searls_auth_pending_email] = user.email
43
+ session[:searls_auth_pending_redirect_path] = target_path
44
+ session[:searls_auth_pending_redirect_subdomain] = target_subdomain
45
+ end
46
+
47
+ if Searls::Auth.config.email_verification_mode == :required
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, redirect_subdomain: target_subdomain}.compact_blank)
50
+ else
51
+ session[:user_id] = user.id
52
+ session[:has_logged_in_before] = true
53
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_login, user, params)
54
+ if (target = target_redirect_url)
55
+ redirect_with_host_awareness(target)
56
+ else
57
+ fallback = Searls::Auth.config.resolve(:redirect_path_after_login, user, params, request, main_app)
58
+ redirect_to(fallback || searls_auth.login_path)
59
+ end
60
+ end
61
+ end
62
+
63
+ def handle_email_registration(user)
64
+ return unless email_methods_enabled?
65
+ target_path, target_subdomain = registration_redirect_destination(user)
66
+ enqueue_login_verification_email(user, target_path:, target_subdomain:)
67
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_registration, user, params)
68
+ redirect_to searls_auth.verify_path(**forwardable_params)
69
+ end
70
+
71
+ def registration_redirect_destination(user)
72
+ if redirect_params_supplied?
73
+ [params[:redirect_path], params[:redirect_subdomain]]
74
+ else
75
+ [Searls::Auth.config.resolve(:redirect_path_after_register, user, params, request, main_app), nil]
76
+ end
77
+ end
78
+
79
+ def redirect_params_supplied?
80
+ params[:redirect_path].present? || params[:redirect_subdomain].present?
81
+ end
82
+
83
+ def enqueue_login_verification_email(user, target_path:, target_subdomain:)
84
+ email_otp = nil
85
+ if Searls::Auth.config.auth_methods.include?(:email_otp)
86
+ attach_email_otp_to_session!(user)
87
+ email_otp = session[:searls_auth_email_otp]
88
+ else
89
+ clear_email_otp_from_session!
90
+ end
91
+ EmailsLink.new.email(
92
+ user: user,
93
+ email_otp: email_otp,
94
+ redirect_path: target_path,
95
+ redirect_subdomain: target_subdomain
96
+ )
97
+ end
46
98
  end
47
99
  end
48
100
  end
@@ -0,0 +1,55 @@
1
+ module Searls
2
+ module Auth
3
+ class RequestsPasswordResetsController < BaseController
4
+ before_action :ensure_password_reset_enabled
5
+ before_action :clear_email_otp_from_session!, only: [:show, :create]
6
+
7
+ def show
8
+ render Searls::Auth.config.password_reset_request_view, layout: Searls::Auth.config.layout
9
+ end
10
+
11
+ def create
12
+ email = params[:email].to_s.strip
13
+ user = Searls::Auth.config.user_finder_by_email.call(email)
14
+
15
+ if proceed_with_password_reset_request?(user) && deliverable_user?(user)
16
+ Searls::Auth::DeliversPasswordReset.new.deliver(
17
+ user:,
18
+ redirect_path: params[:redirect_path],
19
+ redirect_subdomain: params[:redirect_subdomain]
20
+ )
21
+ end
22
+
23
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_password_reset_email, params)
24
+ redirect_to searls_auth.password_reset_request_path(
25
+ email: email,
26
+ redirect_path: params[:redirect_path],
27
+ redirect_subdomain: params[:redirect_subdomain]
28
+ )
29
+ end
30
+
31
+ private
32
+
33
+ def ensure_password_reset_enabled
34
+ return if Searls::Auth.config.password_reset_enabled?
35
+
36
+ flash[:alert] = Searls::Auth.config.resolve(:flash_error_after_password_reset_not_enabled, params)
37
+ redirect_to searls_auth.login_path(
38
+ redirect_path: params[:redirect_path],
39
+ redirect_subdomain: params[:redirect_subdomain]
40
+ )
41
+ nil
42
+ end
43
+
44
+ def deliverable_user?(user)
45
+ return false if user.blank?
46
+ Searls::Auth.config.password_present?(user)
47
+ end
48
+
49
+ def proceed_with_password_reset_request?(user)
50
+ result = Searls::Auth.config.before_password_reset.call(user, params, self)
51
+ !(result == false || result == :halt)
52
+ end
53
+ end
54
+ end
55
+ end