searls-auth 0.1.1 → 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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/LICENSE.txt +1 -1
  4. data/README.md +185 -1
  5. data/app/controllers/searls/auth/base_controller.rb +42 -21
  6. data/app/controllers/searls/auth/email_verifications_controller.rb +57 -0
  7. data/app/controllers/searls/auth/logins_controller.rb +61 -35
  8. data/app/controllers/searls/auth/registrations_controller.rb +84 -32
  9. data/app/controllers/searls/auth/requests_password_resets_controller.rb +55 -0
  10. data/app/controllers/searls/auth/resets_passwords_controller.rb +73 -0
  11. data/app/controllers/searls/auth/settings_controller.rb +83 -0
  12. data/app/controllers/searls/auth/verifications_controller.rb +37 -53
  13. data/app/helpers/searls/auth/application_helper.rb +9 -5
  14. data/app/mailers/searls/auth/base_mailer.rb +1 -1
  15. data/app/mailers/searls/auth/email_verification_mailer.rb +29 -0
  16. data/app/mailers/searls/auth/login_link_mailer.rb +13 -2
  17. data/app/mailers/searls/auth/password_reset_mailer.rb +29 -0
  18. data/app/views/searls/auth/email_verification_mailer/verification_email.html.erb +23 -0
  19. data/app/views/searls/auth/email_verification_mailer/verification_email.text.erb +6 -0
  20. data/app/views/searls/auth/login_link_mailer/login_link.html.erb +29 -25
  21. data/app/views/searls/auth/login_link_mailer/login_link.text.erb +9 -5
  22. data/app/views/searls/auth/logins/show.html.erb +12 -4
  23. data/app/views/searls/auth/password_reset_mailer/password_reset.html.erb +23 -0
  24. data/app/views/searls/auth/password_reset_mailer/password_reset.text.erb +6 -0
  25. data/app/views/searls/auth/registrations/pending_email_verification.html.erb +12 -0
  26. data/app/views/searls/auth/registrations/show.html.erb +1 -2
  27. data/app/views/searls/auth/requests_password_resets/show.html.erb +17 -0
  28. data/app/views/searls/auth/resets_passwords/show.html.erb +26 -0
  29. data/app/views/searls/auth/settings/edit.html.erb +31 -0
  30. data/app/views/searls/auth/shared/_login_fields.html.erb +11 -0
  31. data/app/views/searls/auth/shared/_register_fields.html.erb +15 -0
  32. data/app/views/searls/auth/verifications/show.html.erb +20 -20
  33. data/config/routes.rb +11 -0
  34. data/lib/searls/auth/authenticates_user.rb +54 -10
  35. data/lib/searls/auth/builds_target_redirect_url.rb +72 -0
  36. data/lib/searls/auth/config.rb +246 -10
  37. data/lib/searls/auth/creates_user.rb +12 -4
  38. data/lib/searls/auth/delivers_password_reset.rb +18 -0
  39. data/lib/searls/auth/emails_link.rb +3 -3
  40. data/lib/searls/auth/emails_verification.rb +33 -0
  41. data/lib/searls/auth/parses_time_safely.rb +34 -0
  42. data/lib/searls/auth/railtie.rb +0 -1
  43. data/lib/searls/auth/resets_password.rb +41 -0
  44. data/lib/searls/auth/updates_settings.rb +149 -0
  45. data/lib/searls/auth/version.rb +1 -1
  46. data/lib/searls/auth.rb +63 -13
  47. data/script/setup +1 -6
  48. data/script/test +1 -1
  49. metadata +24 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c23b1737c22e177cbc5da56d7aab735c39408b87c6954b59b15b5f6fc3553021
4
- data.tar.gz: 9906eabdf2bf3593d993bdfc6a6ac5234f346a9fa4f0c26b773fa4291a80fd3a
3
+ metadata.gz: '09b9515ef89e82e747cc35c415a4de145cf8e61634caccb137ceb8f429a54bdc'
4
+ data.tar.gz: e5b456ac99cc9ad784ed8727266dc3e073e6ee850ad720be0a4a42e894e2bbe8
5
5
  SHA512:
6
- metadata.gz: 2cbd987345d290d2648a67817531f571482f56002a71b5a7a0aacae01f89afc93cc799cf248712f3eae72c630623d4915cf0f3d5f8c71e238a740a9701a6bd35
7
- data.tar.gz: a1e03993409a4306dfe443385c993fa6e9d53f2328914311c46996c263212fcbe0b7d45e93d1a7ed092dc45ea4c2eadb149384d11a97dca097240648ee8f052e
6
+ metadata.gz: 6c5163daa1551674b1c128ecf8a6d155cde36123780bf2332ef5d9f79166d37a964616f6fda17c97eb2608df9d13ad54f6f25e5c384ef2308b8740f93e326302
7
+ data.tar.gz: 4bfdd8c989591a52aeb52cf91bbf55e189442e57031d877f0e81f88c45d51d8cd22bd8ca21f31be10370c68b3981e410b2a034ed6c69738b28ee10cfcca078d0
data/CHANGELOG.md CHANGED
@@ -1,12 +1,27 @@
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
+
14
+ ## [0.2.0] - 2025-09-11
15
+
16
+ * Add `auth_methods` configuration with default `[:email_link, :email_otp]`
17
+
3
18
  ## [0.1.1] - 2025-04-27
4
19
 
5
20
  * Improve error message when token generation fails due to a token not being configured on the user model
6
21
 
7
22
  ## [0.1.0] - 2025-04-26
8
23
 
9
- * 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)
10
25
  * Allow configuration of flash messages
11
26
  * Fix a routing error if the user is already registered
12
27
 
data/LICENSE.txt CHANGED
@@ -653,7 +653,7 @@ Also add information on how to contact you by electronic and paper mail.
653
653
  If the program does terminal interaction, make it output a short
654
654
  notice like this when it starts in an interactive mode:
655
655
 
656
- Fine Ants Copyright (C) 2016 Justin Searls
656
+ searls-auth (C) 2025 Searls LLC
657
657
  This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
658
658
  This is free software, and you are welcome to redistribute it
659
659
  under certain conditions; type `show c' for details.
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.
@@ -82,8 +84,190 @@ end
82
84
  ```
83
85
  As stated in the comment above, you can find each configuration and its default value in the code.
84
86
 
87
+ ### Choose your login methods
88
+
89
+ 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:
90
+
91
+ ```ruby
92
+ # config/initializers/searls_auth.rb
93
+ Rails.application.config.after_initialize do
94
+ Searls::Auth.configure do |config|
95
+ # Defaults:
96
+ config.auth_methods = [:email_link, :email_otp]
97
+
98
+ # Email OTPs expire after 30 minutes by default.
99
+ # config.email_otp_expiry_minutes = 10
100
+
101
+ # Link-only (no code in emails, no OTP input shown):
102
+ # config.auth_methods = :email_link
103
+
104
+ # Code-only (no link in emails; OTP input shown):
105
+ # config.auth_methods = :email_otp
106
+ end
107
+ end
108
+ ```
109
+
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).
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
+
85
269
  ## Use it
86
270
 
87
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.)
88
272
 
89
- If this is your first rodeo and you just read the previous paragraph and thought, _yeah, but now what?_, check out the tail end of the [example app README](/example/simple_app/README.md), which shows an approach that a lot of apps use.
273
+ If this is your first rodeo and you just read the previous paragraph and thought, _yeah, but now what?_, check out the tail end of the [example app README](/example/simple_app/README.md#5-require-authentication-for-appropriate-actions), which shows an approach that a lot of apps use.
@@ -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,54 +1,80 @@
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])
12
-
13
- if user.present?
14
- attach_short_code_to_session!(user)
15
- EmailsLink.new.email(
16
- user:,
17
- redirect_path: params[:redirect_path],
18
- redirect_subdomain: params[:redirect_subdomain],
19
- short_code: session[:searls_auth_short_code]
20
- )
21
- flash[:notice] = searls_auth_config.resolve(
22
- :flash_notice_after_login_attempt,
23
- user, params
24
- )
25
- redirect_to searls_auth.verify_path(
26
- redirect_path: params[:redirect_path],
27
- redirect_subdomain: params[:redirect_subdomain]
28
- )
11
+ if Searls::Auth.config.auth_methods.include?(:password) && params[:send_login_email].blank?
12
+ handle_password_login
29
13
  else
30
- flash.now[:error] = searls_auth_config.resolve(
31
- :flash_error_after_login_attempt_unknown_email,
32
- searls_auth.register_path(
33
- email: params[:email],
34
- redirect_path: params[:redirect_path],
35
- redirect_subdomain: params[:redirect_subdomain]
36
- ),
37
- params
38
- )
39
- render searls_auth_config.login_view, layout: searls_auth_config.layout, status: :unprocessable_entity
14
+ handle_email_login
40
15
  end
41
16
  end
42
17
 
43
18
  def destroy
44
19
  ResetsSession.new.reset(self, except_for: [:has_logged_in_before])
45
-
46
- flash[:notice] = searls_auth_config.resolve(
47
- :flash_notice_after_logout,
48
- params
49
- )
20
+ flash[:notice] = Searls::Auth.config.resolve(:flash_notice_after_logout, params)
50
21
  redirect_to searls_auth.login_path
51
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)
37
+ else
38
+ Searls::Auth.config.resolve(:flash_notice_after_login, result.user, params)
39
+ end
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
46
+ else
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
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
58
+ end
59
+
60
+ def handle_email_login
61
+ user = Searls::Auth.config.user_finder_by_email.call(params[:email])
62
+
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
77
+ end
52
78
  end
53
79
  end
54
80
  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