searls-auth 0.0.1 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b25e2025b4ec1c7eb16c573e4006a079b9073e9837769f1038b62e7b8fc3bfd
4
- data.tar.gz: b09296feab4da00d73216df4a2f5a23b7dd1a97e1a7fd93f824c20acf3fb0379
3
+ metadata.gz: c23b1737c22e177cbc5da56d7aab735c39408b87c6954b59b15b5f6fc3553021
4
+ data.tar.gz: 9906eabdf2bf3593d993bdfc6a6ac5234f346a9fa4f0c26b773fa4291a80fd3a
5
5
  SHA512:
6
- metadata.gz: ac9b6013cec969279330d49c5967de49a79b6c2e5fcab7a8189e5f0e6f9d3aa54b57fc487b59b789f7a03169412c8339ce57e9d4d7314420f8081a0cb5d75be1
7
- data.tar.gz: ff8e2755bac9adc5a6e836b20908a57e5f1a8318aeef1316a30805aef252bc3a103b52729abc87955c5bb62a7cbcfdee843e8308d9406f5e9fa82a89d773f7c5
6
+ metadata.gz: 2cbd987345d290d2648a67817531f571482f56002a71b5a7a0aacae01f89afc93cc799cf248712f3eae72c630623d4915cf0f3d5f8c71e238a740a9701a6bd35
7
+ data.tar.gz: a1e03993409a4306dfe443385c993fa6e9d53f2328914311c46996c263212fcbe0b7d45e93d1a7ed092dc45ea4c2eadb149384d11a97dca097240648ee8f052e
data/.standard.yml CHANGED
@@ -1,3 +1,5 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/standardrb/standard
3
3
  ruby_version: 3.4
4
+ ignore:
5
+ - '**/vendor/**/*'
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.1.1] - 2025-04-27
4
+
5
+ * Improve error message when token generation fails due to a token not being configured on the user model
6
+
7
+ ## [0.1.0] - 2025-04-26
8
+
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)
10
+ * Allow configuration of flash messages
11
+ * Fix a routing error if the user is already registered
12
+
13
+ ## [0.0.2] - 2025-04-16
14
+
15
+ * Little fixes. Renamed `user_name_field` to `user_name_method`
16
+
3
17
  ## [0.0.1] - 2025-04-15
4
18
 
5
- - Initial release
19
+ * Initial release
data/README.md CHANGED
@@ -1 +1,89 @@
1
1
  # searls-auth
2
+
3
+ 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
+
5
+ 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.
6
+
7
+ ## Install it
8
+
9
+ Add it to your Gemfile and `bundle` it:
10
+
11
+ ```ruby
12
+ gem "searls-auth"
13
+ ```
14
+
15
+ ## Mount it
16
+
17
+ Next, you need to mount the gem's Engine to host any of the authentication controllers and mailers.
18
+
19
+ You can mount the engine at whatever you path you like (mounting it to "/" can result in some goofy behavior, so maybe not that one). I just do "/auth" because I'm boring:
20
+
21
+ ```ruby
22
+ # config/routes.rb
23
+ Rails.application.routes.draw do
24
+ # …
25
+ mount Searls::Auth::Engine => "/auth"
26
+ # …
27
+ end
28
+ ```
29
+
30
+ If you run your development server and visit [http://localhost:3000/auth/login](http://localhost:3000/auth/login), you should see an ugly login page. (If things look really goofy, it's because the gem defaults to your app's `"application"` layout).
31
+
32
+ ## Secure it
33
+
34
+ If you've got a `User` model with an `email` attribute, you're two-thirds of the way to this thing working. All you need now is to associate [a secure token](https://api.rubyonrails.org/classes/ActiveRecord/TokenFor/ClassMethods.html#method-i-generates_token_for) with the model named `:email_auth`.
35
+
36
+ ```ruby
37
+ # app/models/user.rb
38
+ class User < ApplicationRecord
39
+ # …
40
+ generates_token_for :email_auth, expires_in: 30.minutes
41
+ # …
42
+ end
43
+ ```
44
+
45
+ (You can [name all these things whatever you want](#configure-it), but this is what searls-auth will assume by default.)
46
+
47
+ I'm writing this README as I add searls-auth to my new [POSSE Party](https://posseparty.com) app. As soon as I added the above line I visited [http://localhost:3000/auth/login](http://localhost:3000/auth/login), typed in my email, hit "Log in", and saw this email get sent (thanks to [letter_opener](https://github.com/ryanb/letter_opener)):
48
+
49
+ ![A default searls-auth login email](https://github.com/user-attachments/assets/07114dae-a95b-49bd-ba57-92042c62c1b7)
50
+
51
+ When I pasted in the six-digit code into the (also ugly) default verification page, it auto-submitted the form. That's because my has a vanilla [import maps](https://guides.rubyonrails.org/working_with_javascript_in_rails.html#import-maps) configuration, the least-bad of the various JavaScript ordeals Rails has on offer. (Don't use import maps? Then I leave figuring out how to load the gem's [Stimulus controllers](app/javascript/controllers/searls_auth_login_controller.js) as an exercise to the reader.)
52
+
53
+ I repeated the process to ensure the "magic link" also would have worked by visiting [http://localhost:3000/auth/logout](http://localhost:3000/auth/logout) and then clicking the link.
54
+
55
+ ## Configure it
56
+
57
+ Almost every user-facing thing searls-auth does is configurable, because authentication is an _intimate and precious_ thing that every application must find a way to tweak, brand, and confuse.
58
+
59
+ To configure things, create an initializer:
60
+
61
+ ```
62
+ touch config/initializers/searls_auth.rb
63
+ ```
64
+
65
+ And paste this into it as a starting point:
66
+
67
+ ```ruby
68
+ Rails.application.config.after_initialize do
69
+ Searls::Auth.configure do |config|
70
+ # You can find the defaults here-ish:
71
+ # https://github.com/searlsco/searls-auth/blob/main/lib/searls/auth.rb#L14
72
+ #
73
+ # The expected type of each option is documented inline here-ish:
74
+ # https://github.com/searlsco/searls-auth/blob/main/lib/searls/auth/config.rb#L3
75
+ #
76
+ # (Note that many options can take a proc or a value, which you may want)
77
+ #
78
+ # Override any option like this:
79
+ # config.app_name = "POSSE Party"
80
+ end
81
+ end
82
+ ```
83
+ As stated in the comment above, you can find each configuration and its default value in the code.
84
+
85
+ ## Use it
86
+
87
+ 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
+
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.
@@ -13,22 +13,29 @@ module Searls
13
13
  end
14
14
 
15
15
  def attach_short_code_to_session!(user)
16
- session[:email_auth_short_code_user_id] = user.id
17
- session[:email_auth_short_code] = SecureRandom.random_number(1000000).to_s.rjust(6, "0")
18
- session[:email_auth_short_code_generated_at] = Time.current
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
19
20
  end
20
21
 
21
22
  def reset_expired_short_code
22
- if session[:email_auth_short_code_generated_at].present? &&
23
- Time.zone.parse(session[:email_auth_short_code_generated_at]) < Searls::Auth.config.token_expiry_minutes.minutes.ago
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
24
25
  clear_short_code_from_session!
25
26
  end
26
27
  end
27
28
 
28
29
  def clear_short_code_from_session!
29
- session.delete(:email_auth_short_code_user_id)
30
- session.delete(:email_auth_short_code_generated_at)
31
- session.delete(:email_auth_short_code)
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)
34
+ end
35
+
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
32
39
  end
33
40
  end
34
41
  end
@@ -16,19 +16,26 @@ module Searls
16
16
  user:,
17
17
  redirect_path: params[:redirect_path],
18
18
  redirect_subdomain: params[:redirect_subdomain],
19
- short_code: session[:email_auth_short_code]
19
+ short_code: session[:searls_auth_short_code]
20
20
  )
21
- flash[:notice] = "Login details sent to #{params[:email]}"
22
- redirect_to verify_path(
21
+ flash[:notice] = searls_auth_config.resolve(
22
+ :flash_notice_after_login_attempt,
23
+ user, params
24
+ )
25
+ redirect_to searls_auth.verify_path(
23
26
  redirect_path: params[:redirect_path],
24
27
  redirect_subdomain: params[:redirect_subdomain]
25
28
  )
26
29
  else
27
- flash.now[:error] = "We don't know that email. <a href=\"#{register_path(
28
- email: params[:email],
29
- redirect_path: params[:redirect_path],
30
- redirect_subdomain: params[:redirect_subdomain]
31
- )}\">Sign up</a> instead?".html_safe
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
+ )
32
39
  render searls_auth_config.login_view, layout: searls_auth_config.layout, status: :unprocessable_entity
33
40
  end
34
41
  end
@@ -36,8 +43,11 @@ module Searls
36
43
  def destroy
37
44
  ResetsSession.new.reset(self, except_for: [:has_logged_in_before])
38
45
 
39
- flash[:notice] = "You've been logged out."
40
- redirect_to login_path
46
+ flash[:notice] = searls_auth_config.resolve(
47
+ :flash_notice_after_logout,
48
+ params
49
+ )
50
+ redirect_to searls_auth.login_path
41
51
  end
42
52
  end
43
53
  end
@@ -20,13 +20,26 @@ module Searls
20
20
 
21
21
  EmailsLink.new.email(
22
22
  user: result.user,
23
- short_code: session[:email_auth_short_code],
23
+ short_code: session[:searls_auth_short_code],
24
24
  **redirect_params
25
25
  )
26
- flash[:notice] = "Verification email sent to #{params[:email]}"
27
- redirect_to verify_path(**redirect_params)
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)
28
32
  else
29
- flash.now[:error] = result.error_messages
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
+ )
30
43
  render searls_auth_config.register_view, layout: searls_auth_config.layout, status: :unprocessable_entity
31
44
  end
32
45
  end
@@ -12,6 +12,7 @@ module Searls
12
12
  authenticator = AuthenticatesUser.new
13
13
  result = case auth_method
14
14
  when :short_code
15
+ log_short_code_verification_attempt!
15
16
  authenticator.authenticate_by_short_code(params[:short_code], session)
16
17
  when :token
17
18
  authenticator.authenticate_by_token(params[:token])
@@ -20,6 +21,10 @@ module Searls
20
21
  if result.success?
21
22
  session[:user_id] = result.user.id
22
23
  session[:has_logged_in_before] = true
24
+ flash[:notice] = searls_auth_config.resolve(
25
+ :flash_notice_after_verification,
26
+ result.user, params
27
+ )
23
28
  if params[:redirect_subdomain].present? && params[:redirect_subdomain] != request.subdomain
24
29
  redirect_to generate_full_url(
25
30
  params[:redirect_path],
@@ -32,25 +37,35 @@ module Searls
32
37
  result.user, params, request, main_app)
33
38
  end
34
39
  elsif auth_method == :short_code
35
- flash[:error] = "We weren't able to log you in with that code. Try again?"
36
- render searls_auth_config.verify_view, layout: searls_auth_config.layout, status: :unprocessable_entity
40
+ if result.exceeded_short_code_attempt_limit?
41
+ clear_short_code_from_session!
42
+ flash[:error] = searls_auth_config.resolve(
43
+ :flash_error_after_verify_attempt_exceeds_limit,
44
+ params
45
+ )
46
+ redirect_to searls_auth.login_path(
47
+ redirect_path: params[:redirect_path],
48
+ redirect_subdomain: params[:redirect_subdomain]
49
+ )
50
+ else
51
+ flash[:error] = searls_auth_config.resolve(
52
+ :flash_error_after_verify_attempt_incorrect_short_code,
53
+ params
54
+ )
55
+ render searls_auth_config.verify_view, layout: searls_auth_config.layout, status: :unprocessable_entity
56
+ end
37
57
  else
38
- flash[:error] = "We weren't able to log you in with that link. Try again?"
39
- redirect_to login_path(
58
+ flash[:error] = searls_auth_config.resolve(
59
+ :flash_error_after_verify_attempt_invalid_link,
60
+ params
61
+ )
62
+ redirect_to searls_auth.login_path(
40
63
  redirect_path: params[:redirect_path],
41
64
  redirect_subdomain: params[:redirect_subdomain]
42
65
  )
43
66
  end
44
67
  end
45
68
 
46
- def destroy
47
- ResetsSession.new.reset(self,
48
- except_for: searls_auth_config.preserve_session_keys_after_logout)
49
-
50
- flash[:notice] = "You've been logged out."
51
- redirect_to login_path
52
- end
53
-
54
69
  private
55
70
 
56
71
  def generate_full_url(path, subdomain)
@@ -51,10 +51,13 @@ module Searls
51
51
  end
52
52
 
53
53
  def attr_for(model, field_name)
54
- model.attributes[field_name.to_s]
54
+ if model.respond_to?(field_name)
55
+ model.send(field_name)
56
+ end
55
57
  end
56
58
 
57
59
  def rpad(s, spacer = " ", times = 1)
60
+ return "" if s.blank?
58
61
  "#{s}#{spacer * times}"
59
62
  end
60
63
 
@@ -23,7 +23,6 @@ export default class SearlsAuthLoginController extends Controller {
23
23
  }
24
24
 
25
25
  updateEmail (e) {
26
- debugger
27
26
  this.emailValue = e.currentTarget.value
28
27
  window.sessionStorage.setItem('__searls_auth_email', this.emailValue)
29
28
  }
@@ -3,6 +3,17 @@ module Searls
3
3
  class BaseMailer < ApplicationMailer # TODO should this be ActionMailer::Base? Trade-offs?
4
4
  helper Searls::Auth::ApplicationHelper
5
5
  include Searls::Auth::ApplicationHelper
6
+
7
+ protected
8
+
9
+ def format_to(user)
10
+ name_field = searls_auth_helper.attr_for(user, Searls::Auth.config.user_name_method)
11
+ if name_field.present?
12
+ "#{user.name} <#{user.email}>"
13
+ else
14
+ user.email
15
+ end
16
+ end
6
17
  end
7
18
  end
8
19
  end
@@ -11,7 +11,7 @@ module Searls
11
11
 
12
12
  mail(
13
13
  to: format_to(@user),
14
- subject: "Your #{searls_auth_helper.rpad(@config.app_name)} login code is #{@short_code}",
14
+ subject: "Your #{searls_auth_helper.rpad(@config.app_name)}login code is #{@short_code}",
15
15
  template_path: @config.mail_login_template_path,
16
16
  template_name: @config.mail_login_template_name
17
17
  ) do |format|
@@ -1,6 +1,6 @@
1
1
  <%= content_for :width, 480 %>
2
2
  <p>
3
- <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_field)).present? %>
3
+ <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_method)).present? %>
4
4
  Hello, <strong><%= user_name %></strong>!
5
5
  <% else %>
6
6
  Hello!
@@ -1,4 +1,4 @@
1
- <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_field)).present? %>
1
+ <% if (user_name = searls_auth_helper.attr_for(@user, @config.user_name_method)).present? %>
2
2
  Hi, <%= user_name %>!
3
3
  <% else %>
4
4
  Hello!
@@ -6,7 +6,7 @@ Hello!
6
6
 
7
7
  You can log in to your <%= searls_auth_helper.rpad(@config.app_name) %>account at this URL:
8
8
 
9
- <%= verify_token_url({
9
+ <%= searls_auth.verify_token_url({
10
10
  token: @token,
11
11
  redirect_path: @redirect_path,
12
12
  redirect_subdomain: @redirect_subdomain
@@ -1,15 +1,9 @@
1
1
  <h1>Log In</h1>
2
2
 
3
- <div style="background: rgba(255,0,0, 0.1)">
4
- <%= flash[:error] %>
5
- </div>
6
- <div style="background: rgba(0,0,255, 0.1)">
7
- <%= flash[:notice] %>
8
- </div>
9
-
10
- <%= form_with url: login_path, method: :post, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
3
+ <%= form_with url: searls_auth.login_path, method: :post, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
11
4
  <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
12
5
  <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
6
+ <%= f.label :email %>
13
7
  <%= f.email_field :email, required: true, value: params[:email], data: searls_auth_helper.email_field_stimulus_data %>
14
8
  <%= f.submit "Log in" %>
15
9
  <% end %>
@@ -1,15 +1,9 @@
1
1
  <h1>Create your account</h1>
2
2
 
3
- <div style="background: rgba(255,0,0, 0.1)">
4
- <%= flash[:error] %>
5
- </div>
6
- <div style="background: rgba(0,0,255, 0.1)">
7
- <%= flash[:notice] %>
8
- </div>
9
-
10
- <%= form_with url: register_path, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
3
+ <%= form_with url: searls_auth.register_path, data: {controller: searls_auth_helper.login_stimulus_controller} do |f| %>
11
4
  <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
12
5
  <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
6
+ <%= f.label :email %>
13
7
  <%= f.email_field :email, value: params[:email], required: true, data: searls_auth_helper.email_field_stimulus_data %>
14
8
  <%= f.submit "Register" %>
15
9
  <% end %>
@@ -3,13 +3,14 @@
3
3
  In the next few moments, you should receive an email that will provide you
4
4
  two ways to log in: a link and a six-digit code that you can enter below.
5
5
  </p>
6
- <%= form_with(url: verify_path, method: :post, data: {
6
+ <%= form_with(url: searls_auth.verify_path, method: :post, data: {
7
7
  # Don't use turbo on cross-domain redirects
8
8
  turbo: searls_auth_helper.enable_turbo?
9
9
  }) do |f| %>
10
10
  <%= f.hidden_field :redirect_path, value: params[:redirect_path] %>
11
11
  <%= f.hidden_field :redirect_subdomain, value: params[:redirect_subdomain] %>
12
12
  <div data-controller="<%= searls_auth_helper.otp_stimulus_controller %>">
13
+ <%= f.label :short_code, "Code" %>
13
14
  <%= f.text_field :short_code,
14
15
  maxlength: 6,
15
16
  inputmode: "numeric",
@@ -1,15 +1,17 @@
1
1
  module Searls
2
2
  module Auth
3
3
  class AuthenticatesUser
4
- Result = Struct.new(:success?, :user, keyword_init: true)
4
+ Result = Struct.new(:success?, :user, :exceeded_short_code_attempt_limit?, keyword_init: true)
5
5
 
6
6
  def authenticate_by_short_code(short_code, session)
7
- user = Searls::Auth.config.user_finder_by_id.call(session[:email_auth_short_code_user_id])
7
+ if session[:searls_auth_short_code_verification_attempts] > Searls::Auth.config.max_allowed_short_code_attempts
8
+ return Result.new(success?: false, exceeded_short_code_attempt_limit?: true)
9
+ end
8
10
 
9
- if session[:email_auth_short_code_generated_at].present? &&
10
- Time.zone.parse(session[:email_auth_short_code_generated_at]) > Searls::Auth.config.token_expiry_minutes.minutes.ago &&
11
- user.present? &&
12
- short_code == session[:email_auth_short_code]
11
+ if session[:searls_auth_short_code_generated_at].present? &&
12
+ Time.zone.parse(session[:searls_auth_short_code_generated_at]) > Searls::Auth.config.token_expiry_minutes.minutes.ago &&
13
+ short_code == session[:searls_auth_short_code] &&
14
+ (user = Searls::Auth.config.user_finder_by_id.call(session[:searls_auth_short_code_user_id])).present?
13
15
  Searls::Auth.config.after_login_success&.call(user)
14
16
  Result.new(success?: true, user: user)
15
17
  else
@@ -2,15 +2,16 @@ module Searls
2
2
  module Auth
3
3
  Config = Struct.new(
4
4
  # Data setup
5
- :user_finder_by_email, # proc (email)
6
- :user_finder_by_id, # proc (id)
7
- :user_finder_by_token, # proc (token)
8
- :user_initializer, # proc (params)
9
- :user_name_field, # string
10
- :token_generator, # proc ()
5
+ :user_finder_by_email, # proc(email)
6
+ :user_finder_by_id, # proc(id)
7
+ :user_finder_by_token, # proc(token)
8
+ :user_initializer, # proc(params)
9
+ :user_name_method, # string
10
+ :token_generator, # proc()
11
11
  :token_expiry_minutes, # integer
12
12
  # Controller setup
13
13
  :preserve_session_keys_after_logout, # array of symbols
14
+ :max_allowed_short_code_attempts, # integer
14
15
  # View setup
15
16
  :layout, # string
16
17
  :login_view, # string
@@ -20,11 +21,11 @@ module Searls
20
21
  :mail_login_template_path, # string
21
22
  :mail_login_template_name, # string
22
23
  # Routing setup
23
- :redirect_path_after_register, # string or proc, all new registrations redirect here
24
- :default_redirect_path_after_login, # string or proc, only redirected here if redirect_path param not set
24
+ :redirect_path_after_register, # string or proc(user, params, request, routes), all new registrations redirect here
25
+ :default_redirect_path_after_login, # string or proc(user, params, request, routes), only redirected here if redirect_path param not set
25
26
  # Hook setup
26
- :validate_registration, # proc (user, params, errors = []), must return an array of error messages where empty means valid
27
- :after_login_success, # proc (user)
27
+ :validate_registration, # proc(user, params, errors = []), must return an array of error messages where empty means valid
28
+ :after_login_success, # proc(user)
28
29
  # Branding setup
29
30
  :app_name, # string
30
31
  :app_url, # string
@@ -32,6 +33,16 @@ module Searls
32
33
  :email_banner_image_path, # string
33
34
  :email_background_color, # string
34
35
  :email_button_color, # string
36
+ # Messages setup
37
+ :flash_notice_after_registration, # string or proc(user, params)
38
+ :flash_error_after_register_attempt, # string or proc(error_messages, login_path, params)
39
+ :flash_notice_after_login_attempt, # string or proc(user, params)
40
+ :flash_error_after_login_attempt_unknown_email, # string or proc(register_path, params)
41
+ :flash_notice_after_logout, # string or proc(params)
42
+ :flash_notice_after_verification, # string or proc(user, params)
43
+ :flash_error_after_verify_attempt_exceeds_limit, # string or proc(params)
44
+ :flash_error_after_verify_attempt_incorrect_short_code, # string or proc(params)
45
+ :flash_error_after_verify_attempt_invalid_link, # string or proc(params)
35
46
  keyword_init: true
36
47
  ) do
37
48
  # Get values from values that might be procs
@@ -4,12 +4,31 @@ module Searls
4
4
  def email(user:, short_code:, redirect_path: nil, redirect_subdomain: nil)
5
5
  LoginLinkMailer.with(
6
6
  user:,
7
- token: Searls::Auth.config.token_generator.call(user),
7
+ token: generate_token!(user),
8
8
  short_code:,
9
9
  redirect_path:,
10
10
  redirect_subdomain:
11
11
  ).login_link.deliver_later
12
12
  end
13
+
14
+ private
15
+
16
+ def generate_token!(user)
17
+ Searls::Auth.config.token_generator.call(user)
18
+ rescue KeyError => e
19
+ raise Error, <<~MSG
20
+ Secure token generation for user failed!
21
+
22
+ Message: #{e.message}
23
+ User: #{user.inspect}
24
+
25
+ This can probably be fixed by adding a line like this to your #{user.class.name} class:
26
+
27
+ generates_token_for :email_auth, expires_in: 30.minutes
28
+
29
+ Otherwise, you may want to override searls-auth's "token_generator" setting with a proc of your own.
30
+ MSG
31
+ end
13
32
  end
14
33
  end
15
34
  end
@@ -1,5 +1,5 @@
1
1
  module Searls
2
2
  module Auth
3
- VERSION = "0.0.1"
3
+ VERSION = "0.1.1"
4
4
  end
5
5
  end
data/lib/searls/auth.rb CHANGED
@@ -16,12 +16,13 @@ module Searls
16
16
  user_finder_by_email: ->(email) { User.find_by(email:) },
17
17
  user_finder_by_id: ->(id) { User.find_by(id:) },
18
18
  user_finder_by_token: ->(token) { User.find_by_token_for(:email_auth, token) },
19
- user_initializer: ->(params) { User.new(params[:email]) },
20
- user_name_field: "name",
19
+ user_initializer: ->(params) { User.new(email: params[:email]) },
20
+ user_name_method: "name",
21
21
  token_generator: ->(user) { user.generate_token_for(:email_auth) },
22
22
  token_expiry_minutes: 30,
23
23
  # Controller setup
24
24
  preserve_session_keys_after_logout: [],
25
+ max_allowed_short_code_attempts: 10,
25
26
  # View setup
26
27
  layout: "application",
27
28
  register_view: "searls/auth/registrations/show",
@@ -48,7 +49,20 @@ module Searls
48
49
  support_email_address: nil,
49
50
  email_background_color: "#d8d7ed",
50
51
  email_button_color: "#c664f3",
51
- email_banner_image_path: nil
52
+ email_banner_image_path: nil,
53
+ # Messages setup
54
+ flash_notice_after_registration: ->(user, params) { "Verification email sent to #{params[:email]}" },
55
+ flash_error_after_register_attempt: ->(error_messages, login_path, params) { error_messages },
56
+ flash_notice_after_login_attempt: ->(user, params) { "Login details sent to #{params[:email]}" },
57
+ flash_error_after_login_attempt_unknown_email: ->(register_path, params) {
58
+ "We don't know that email. <a href=\"#{register_path}\">Sign up</a> instead?".html_safe
59
+ },
60
+ flash_notice_after_logout: "You've been logged out",
61
+ flash_notice_after_verification: "You are now logged in",
62
+ flash_error_after_verify_attempt_exceeds_limit: "Too many verification attempts. Please login again to generate a new code",
63
+ flash_error_after_verify_attempt_incorrect_short_code: "We weren't able to log you in with that code. Try again?",
64
+ flash_error_after_verify_attempt_invalid_link: "We weren't able to log you in with that link. Try again?"
65
+
52
66
  }.freeze
53
67
 
54
68
  CONFIG = Config.new(**DEFAULT_CONFIG)
data/script/setup ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ bundle
6
+
7
+ cd example/simple_app
8
+ bundle
9
+ bin/rake db:setup
10
+ export PLAYWRIGHT_CLI_VERSION=$(bundle exec ruby -e 'require "playwright"; puts Playwright::COMPATIBLE_PLAYWRIGHT_VERSION.strip')
11
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn add -D "playwright@$PLAYWRIGHT_CLI_VERSION"
12
+ yarn run playwright install chromium
13
+
14
+ cd ../..
data/script/setup_ci ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ cd example/simple_app
6
+ bin/rake db:setup
7
+ cd ../..
data/script/test ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ echo "-----> Testing searls-auth gem"
6
+ bundle exec rake
7
+
8
+ echo "-----> Testing example/simple_app"
9
+ cd example/simple_app
10
+ bin/rake
11
+ cd ../..
12
+
13
+ echo "-----> Looks good! 🪩"
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: 0.0.1
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
@@ -60,6 +60,9 @@ files:
60
60
  - lib/searls/auth/railtie.rb
61
61
  - lib/searls/auth/resets_session.rb
62
62
  - lib/searls/auth/version.rb
63
+ - script/setup
64
+ - script/setup_ci
65
+ - script/test
63
66
  homepage: https://github.com/searls/searls-auth
64
67
  licenses:
65
68
  - GPL-3.0-only