minimalist_authentication 3.1.0 → 3.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +49 -28
  3. data/app/controllers/email_verifications_controller.rb +4 -3
  4. data/app/controllers/emails_controller.rb +1 -1
  5. data/app/controllers/password_resets_controller.rb +21 -12
  6. data/app/controllers/passwords_controller.rb +13 -8
  7. data/app/helpers/minimalist_authentication/application_helper.rb +68 -0
  8. data/app/mailers/minimalist_authentication_mailer.rb +2 -2
  9. data/app/validators/password_exclusivity_validator.rb +10 -0
  10. data/app/views/email_verifications/show.html.erb +1 -1
  11. data/app/views/emails/edit.html.erb +3 -3
  12. data/app/views/minimalist_authentication_mailer/update_password.html.erb +3 -1
  13. data/app/views/minimalist_authentication_mailer/update_password.text.erb +2 -2
  14. data/app/views/minimalist_authentication_mailer/verify_email.html.erb +9 -3
  15. data/app/views/minimalist_authentication_mailer/verify_email.text.erb +3 -3
  16. data/app/views/password_resets/new.html.erb +5 -6
  17. data/app/views/passwords/edit.html.erb +9 -7
  18. data/app/views/sessions/_form.html.erb +7 -7
  19. data/app/views/sessions/new.html.erb +3 -3
  20. data/config/locales/minimalist_authentication.en.yml +31 -11
  21. data/config/routes.rb +3 -7
  22. data/lib/minimalist_authentication/configuration.rb +7 -3
  23. data/lib/minimalist_authentication/controller.rb +2 -0
  24. data/lib/minimalist_authentication/email_verification.rb +29 -11
  25. data/lib/minimalist_authentication/engine.rb +5 -0
  26. data/lib/minimalist_authentication/user.rb +25 -34
  27. data/lib/minimalist_authentication/verifiable_token.rb +21 -39
  28. data/lib/minimalist_authentication/version.rb +1 -1
  29. data/lib/minimalist_authentication.rb +6 -2
  30. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb8cc19088eea71ab56f803fe72724911988a9dea1eaf2df45ab6eab1127fea9
4
- data.tar.gz: 648dad4be8864624cf3bc9158c9cc207fc19da57256c363415c2c4b3cbe6b3ef
3
+ metadata.gz: 4b81e23298baa784d92be34aa1b6b372f585c5ea8bba51d953a85ee39ec44db5
4
+ data.tar.gz: 2cad820418a3869931fad968063d3f97e7302686b1fd534daf3a1c9f0d5dda15
5
5
  SHA512:
6
- metadata.gz: fdce2aed67b60fb4adc4d6227523001c52099ea9df6458aa420ce89616467986e05e12362afba71811502d25ab406e2356d2857bc62f640ce7942dc00dc408f4
7
- data.tar.gz: a9c0c9ac343602975b60415c0b53c13e50f748e08a63bf37a1ecb6ca64c20eea1e4e6402096c1900b45255cd13ee736a0ef664cce7a25639fccf73486ede9e12
6
+ metadata.gz: 3b5cd0f9f672f3084884b5385d333705d21d9835738e0b79ba8bc10853bc8bf5cbec88451ff07db26844a601fc69e685fe3f5e3961c5dbcd3616033d9cfe781a
7
+ data.tar.gz: 183194ef85c0ed224f9550c95779ecc5d37b6437ea0b05cd2e2cc32c43785d6718639bc1e4b74997df0fedf93432366e7240bc1e1d6b440008210ce64f277134
data/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # MinimalistAuthentication
2
- A Rails authentication gem that takes a minimalist approach. It is designed to be simple to understand, use, and modify for your application.
2
+ A Rails authentication gem that takes a minimalist approach. It is designed to be simple to understand, use, and customize for your application.
3
3
 
4
4
 
5
5
  ## Installation
6
6
  Add this line to your application's Gemfile:
7
7
 
8
8
  ```ruby
9
- gem 'minimalist_authentication'
9
+ gem "minimalist_authentication"
10
10
  ```
11
11
 
12
- And then execute:
12
+ And then run:
13
13
  ```bash
14
14
  $ bundle
15
15
  ```
@@ -61,18 +61,29 @@ class ActiveSupport::TestCase
61
61
  end
62
62
  ```
63
63
 
64
+
64
65
  ## Configuration
65
66
  Customize the configuration with an initializer. Create a **minimalist_authentication.rb** file in config/initializers.
66
67
  ```ruby
67
68
  MinimalistAuthentication.configure do |configuration|
68
- configuration.user_model_name = 'CustomModelName' # default is '::User'
69
+ configuration.login_redirect_path = :custom_path # default is :root_path
70
+ configuration.logout_redirect_path = :custom_path # default is :new_session_path
71
+ configuration.request_email = true # default is true
69
72
  configuration.session_key = :custom_session_key # default is :user_id
73
+ configuration.user_model_name = "CustomModelName" # default is "::User"
70
74
  configuration.validate_email = true # default is true
71
75
  configuration.validate_email_presence = true # default is true
72
- configuration.request_email = true # default is true
73
76
  configuration.verify_email = true # default is true
74
- configuration.login_redirect_path = :custom_path # default is :root_path
75
- configuration.logout_redirect_path = :custom_path # default is :new_session_path
77
+ end
78
+ ```
79
+
80
+ ### Example with a Person Model
81
+ ```ruby
82
+ MinimalistAuthentication.configure do |configuration|
83
+ configuration.login_redirect_path = :dashboard_path
84
+ configuration.session_key = :person_id
85
+ configuration.user_model_name = "Person"
86
+ configuration.validate_email_presence = false
76
87
  end
77
88
  ```
78
89
 
@@ -86,38 +97,44 @@ example_user:
86
97
  ```
87
98
 
88
99
 
89
- ## Verification Tokens
90
- Verification token support is provided by the **MinimalistAuthentication::VerifiableToken**
91
- module. Include the module in your user class and add the verification token columns
92
- to the database.
93
-
94
- Include MinimalistAuthentication::VerifiableToken in your user model (app/models/user.rb)
100
+ ## Email Verification
101
+ Include MinimalistAuthentication::EmailVerification in your user model (app/models/user.rb)
95
102
  ```ruby
96
103
  class User < ApplicationRecord
97
104
  include MinimalistAuthentication::User
98
- include MinimalistAuthentication::VerifiableToken
105
+ include MinimalistAuthentication::EmailVerification
99
106
  end
100
107
  ```
101
108
 
102
- Add the **verification_token** and **verification_token_generated_at** columns:
103
- Create a user model with **email** for an identifier:
109
+ Add the **email_verified_at** column to your user model:
104
110
  ```bash
105
- bin/rails generate migration AddVerificationTokenToUsers verification_token:string:uniq verification_token_generated_at:datetime
111
+ bin/rails generate migration AddEmailVerifiedAtToUsers email_verified_at:datetime
106
112
  ```
107
113
 
108
- ### Email Verification
109
- Include MinimalistAuthentication::EmailVerification in your user model (app/models/user.rb)
114
+
115
+ ## Verification Tokens
116
+ Verification token support is provided by the ```ActiveRecord::TokenFor#generate_token_for``` method. MinimalistAuthentication includes token definitions for **password_reset** and **email_verification**. These tokens are utilized by the **update_password** and **verify_email** email messages respectively, to allow users to update their passwords and verify their email addresses.
117
+
118
+ ### Update Password
119
+ The **update_password** token expires in 1 hour and is invalidated when the user's password is changed.
120
+
121
+ #### Example
110
122
  ```ruby
111
- class User < ApplicationRecord
112
- include MinimalistAuthentication::User
113
- include MinimalistAuthentication::VerifiableToken
114
- include MinimalistAuthentication::EmailVerification
115
- end
123
+ token = user.generate_token_for(:password_reset)
124
+ User.find_by_token_for(:password_reset, token) # => user
125
+ user.update!(password: "new password")
126
+ User.find_by_token_for(:password_reset, token) # => nil
116
127
  ```
117
128
 
118
- Add the **email_verified_at** column to your user model:
119
- ```bash
120
- bin/rails generate migration AddEmailVerifiedAtToUsers email_verified_at:datetime
129
+ ### Email Verification
130
+ The **email_verification** token expires in 1 hour and is invalidated when the user's email is changed.
131
+
132
+ #### Example
133
+ ```ruby
134
+ token = user.generate_token_for(:email_verification)
135
+ User.find_by_token_for(:email_verification, token) # => user
136
+ user.update!(email: "new_email@example.com")
137
+ User.find_by_token_for(:email_verification, token) # => nil
121
138
  ```
122
139
 
123
140
 
@@ -126,7 +143,7 @@ bin/rails generate migration AddEmailVerifiedAtToUsers email_verified_at:datetim
126
143
  ### Upgrading to Version 2.0
127
144
  Pre 2.0 versions of MinimalistAuthentication supported multiple hash algorithms
128
145
  and stored the hashed password and salt as separate fields in the database
129
- (crypted_password and salt). The current version of MinimalistAuthentication
146
+ (crypted_password and salt). The 2.0 version of MinimalistAuthentication
130
147
  uses BCrypt to hash passwords and stores the result in the **password_hash** field.
131
148
 
132
149
  To convert from a pre 2.0 version add the **password_hash** to your user model
@@ -162,5 +179,9 @@ end
162
179
  alias_attribute :password_digest, :password_hash
163
180
  ```
164
181
 
182
+ ### Upgrading to Version 3.2
183
+ The **verification_token** and **verification_token_generated_at** database columns are no longer used and can be safely removed from your user model.
184
+
185
+
165
186
  ## License
166
187
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT)..
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class EmailVerificationsController < ApplicationController
4
+ # Verifies the email of the current_user using the provided token
4
5
  def show
5
6
  current_user.verify_email(params[:token])
6
7
  end
7
8
 
9
+ # Form for current_user to request an email verification email
8
10
  def new
9
- # verify email for current_user
11
+ # new.html.erb
10
12
  end
11
13
 
14
+ # Sends an email verification email to the current_user
12
15
  def create
13
- current_user.regenerate_verification_token
14
16
  MinimalistAuthenticationMailer.with(user: current_user).verify_email.deliver_now
15
-
16
17
  redirect_to dashboard_path, notice: t(".notice", email: current_user.email)
17
18
  end
18
19
  end
@@ -7,7 +7,7 @@ class EmailsController < ApplicationController
7
7
  if current_user.update(user_params)
8
8
  redirect_to update_redirect_path, notice: t(".notice")
9
9
  else
10
- render :edit
10
+ render :edit, status: :unprocessable_entity
11
11
  end
12
12
  end
13
13
 
@@ -5,30 +5,39 @@ class PasswordResetsController < ApplicationController
5
5
 
6
6
  layout "sessions"
7
7
 
8
- # Form for user to request a password reset
8
+ # Renders form for user to request a password reset
9
9
  def new
10
- @user = MinimalistAuthentication.configuration.user_model.new
10
+ # new.html.erb
11
11
  end
12
12
 
13
13
  # Send a password update link to users with a verified email
14
14
  def create
15
- if user
16
- user.regenerate_verification_token
17
- MinimalistAuthenticationMailer.with(user:).update_password.deliver_now
15
+ if email_valid?
16
+ send_update_password_email if user
17
+
18
+ # Always display notice to prevent leaking user emails
19
+ redirect_to new_session_path, notice: t(".notice", email:)
20
+ else
21
+ flash.now.alert = t(".alert")
22
+ render :new, status: :unprocessable_entity
18
23
  end
19
- # always display notice even if the user was not found to prevent leaking user emails
20
- redirect_to new_session_path, notice: "Password reset instructions were mailed to #{email}"
21
24
  end
22
25
 
23
26
  private
24
27
 
25
- def user
26
- return unless URI::MailTo::EMAIL_REGEXP.match?(email)
28
+ def email
29
+ params.dig(:user, :email)
30
+ end
27
31
 
28
- @user ||= MinimalistAuthentication.configuration.user_model.active.email_verified.find_by(email:)
32
+ def send_update_password_email
33
+ MinimalistAuthenticationMailer.with(user:).update_password.deliver_now
29
34
  end
30
35
 
31
- def email
32
- params.dig(:user, :email)
36
+ def user
37
+ @user ||= MinimalistAuthentication.user_model.find_by_verified_email(email:)
38
+ end
39
+
40
+ def email_valid?
41
+ URI::MailTo::EMAIL_REGEXP.match?(email)
33
42
  end
34
43
  end
@@ -3,34 +3,39 @@
3
3
  class PasswordsController < ApplicationController
4
4
  skip_before_action :authorization_required
5
5
 
6
+ before_action :validate_token, only: %i[edit update]
7
+
6
8
  layout "sessions"
7
9
 
8
10
  # From for user to update password
9
11
  def edit
10
- redirect_to(new_session_path) unless user.matches_verification_token?(token)
11
- # render passwords/edit.html.erb
12
+ # edit.html.erb
12
13
  end
13
14
 
14
15
  # Update user's password
15
16
  def update
16
- if user.secure_update(token, password_params.merge(password_required: true))
17
+ if user.update(password_params)
17
18
  redirect_to new_session_path, notice: t(".notice")
18
19
  else
19
- render :edit
20
+ render :edit, status: :unprocessable_entity
20
21
  end
21
22
  end
22
23
 
23
24
  private
24
25
 
25
- def user
26
- @user ||= MinimalistAuthentication.configuration.user_model.find(params[:user_id])
26
+ def password_params
27
+ params.require(:user).permit(:password, :password_confirmation)
27
28
  end
28
29
 
29
30
  def token
30
31
  @token ||= params[:token]
31
32
  end
32
33
 
33
- def password_params
34
- params.require(:user).permit(:password, :password_confirmation)
34
+ def user
35
+ @user ||= MinimalistAuthentication.user_model.active.find_by_token_for(:password_reset, token)
36
+ end
37
+
38
+ def validate_token
39
+ redirect_to(new_session_path, alert: t(".invalid_token")) unless user
35
40
  end
36
41
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MinimalistAuthentication
4
+ module ApplicationHelper
5
+ def ma_confirm_password_field(form, options = {})
6
+ form.password_field(
7
+ :password_confirmation,
8
+ options.reverse_merge(
9
+ autocomplete: "new-password",
10
+ minlength: MinimalistAuthentication.user_model.password_minimum,
11
+ placeholder: t(".password_confirmation.placeholder"),
12
+ required: true
13
+ )
14
+ )
15
+ end
16
+
17
+ def ma_email_field(form, options = {})
18
+ form.email_field(
19
+ :email,
20
+ options.reverse_merge(
21
+ autocomplete: "email",
22
+ autofocus: true,
23
+ placeholder: t(".email.placeholder", default: true),
24
+ required: true
25
+ )
26
+ )
27
+ end
28
+
29
+ def ma_forgot_password_link
30
+ link_to(t(".forgot_password"), new_password_reset_path)
31
+ end
32
+
33
+ def ma_new_password_field(form, options = {})
34
+ form.password_field(
35
+ :password,
36
+ options.reverse_merge(
37
+ autocomplete: "new-password",
38
+ minlength: MinimalistAuthentication.user_model.password_minimum,
39
+ placeholder: t(".password.placeholder"),
40
+ required: true
41
+ )
42
+ )
43
+ end
44
+
45
+ def ma_password_field(form, options = {})
46
+ form.password_field(
47
+ :password,
48
+ options.reverse_merge(
49
+ autocomplete: "current-password",
50
+ placeholder: true,
51
+ required: true
52
+ )
53
+ )
54
+ end
55
+
56
+ def ma_username_field(form, options = {})
57
+ form.text_field(
58
+ :username,
59
+ options.reverse_merge(
60
+ autocomplete: "username",
61
+ autofocus: true,
62
+ placeholder: true,
63
+ required: true
64
+ )
65
+ )
66
+ end
67
+ end
68
+ end
@@ -5,11 +5,11 @@ class MinimalistAuthenticationMailer < ApplicationMailer
5
5
  after_action :mail_user
6
6
 
7
7
  def verify_email
8
- @verify_email_link = email_verification_url(token: @user.verification_token)
8
+ @verify_email_url = email_verification_url(token: @user.generate_token_for(:email_verification))
9
9
  end
10
10
 
11
11
  def update_password
12
- @edit_password_link = edit_user_password_url(@user, token: @user.verification_token)
12
+ @edit_password_url = edit_password_url(token: @user.generate_token_for(:password_reset))
13
13
  end
14
14
 
15
15
  private
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PasswordExclusivityValidator < ActiveModel::EachValidator
4
+ # Ensure password does not match username or email.
5
+ def validate_each(record, attribute, value)
6
+ %w[username email].each do |field|
7
+ record.errors.add(attribute, "can not match #{field}") if value.casecmp?(record.try(field))
8
+ end
9
+ end
10
+ end
@@ -7,4 +7,4 @@
7
7
  <% end %>
8
8
  <h2><%= current_user.email %></h2>
9
9
 
10
- <%= link_to('Go to Dashboard', dashboard_path) %>
10
+ <%= link_to("Go to Dashboard", dashboard_path) %>
@@ -2,11 +2,11 @@
2
2
 
3
3
  <h2>Please update your email address</h2>
4
4
 
5
- <%= form_for(current_user, as: :user, url: email_path) do |form| %>
5
+ <%= form_with(model: current_user, url: email_path) do |form| %>
6
6
  <%= form.text_field :email %>
7
- <%= form.submit 'Update' %>
7
+ <%= form.submit "Update" %>
8
8
  <% end %>
9
9
 
10
10
  <p>Providing your email will allow you to receive confidential messages and reset your password.</p>
11
11
 
12
- <%= link_to('Skip Email Update', dashboard_path) %>
12
+ <%= link_to("Skip Email Update", dashboard_path) %>
@@ -1,3 +1,5 @@
1
1
  <p><%= t('.opening') %></p>
2
2
 
3
- <%= link_to @edit_password_link, @edit_password_link %>
3
+ <p>
4
+ <%= link_to("Update Password", @edit_password_url, rel: "noopener noreferrer", target: "_blank") %>
5
+ </p>
@@ -1,3 +1,3 @@
1
- <%= t('.opening') %>
1
+ <%= t(".opening") %>
2
2
 
3
- <%= @edit_password_link %>
3
+ <%= @edit_password_url %>
@@ -1,5 +1,11 @@
1
- <p><%= t('.opening') %></p>
1
+ <p>
2
+ <%= t(".opening") %>
3
+ </p>
2
4
 
3
- <%= link_to @verify_email_link, @verify_email_link %>
5
+ <p>
6
+ <%= link_to("Verify Email Address", @verify_email_url, rel: "noopener noreferrer", target: "_blank") %>
7
+ </p>
4
8
 
5
- <p><%= t('.closing') %></p>
9
+ <p>
10
+ <%= t(".closing") %>
11
+ </p>
@@ -1,5 +1,5 @@
1
- <%= t('.opening') %>
1
+ <%= t(".opening") %>
2
2
 
3
- <%= @verify_email_link %>
3
+ <%= @verify_email_url %>
4
4
 
5
- <%= t('.closing') %>
5
+ <%= t(".closing") %>
@@ -1,13 +1,12 @@
1
- <h1>Request Password Reset</h1>
1
+ <h1><%= t(".title") %></h1>
2
2
 
3
3
  <p>
4
- Enter your email address we'll send you a link to reset your password.
4
+ <%= t(".instructions") %>
5
5
  </p>
6
6
 
7
- <%= form_for @user, as: :user, url: password_reset_path do |form| %>
7
+ <%= form_with scope: :user, url: password_reset_path do |form| %>
8
8
  <div>
9
- <%= form.label :email %>
10
- <%= form.text_field :email %>
9
+ <%= ma_email_field(form) %>
11
10
  </div>
12
- <%= form.submit 'Reset Password' %>
11
+ <%= form.submit t(".submit") %>
13
12
  <% end %>
@@ -1,15 +1,17 @@
1
- <h1>Edit Password</h1>
1
+ <h1><%= t(".title") %></h1>
2
+
3
+ <p><%= t(".instructions") %></p>
2
4
 
3
5
  <%= @user.errors.full_messages if @user.errors.any? %>
4
6
 
5
- <%= form_for @user, as: :user, url: user_password_path(@user, token: @token), html: { method: :put } do |form| %>
7
+ <%= form_with model: @user, url: password_path(token: @token) do |form| %>
6
8
  <div>
7
- <%= form.label :password %>
8
- <%= form.password_field :password %>
9
+ <%= form.label :password %>
10
+ <%= ma_new_password_field(form) %>
9
11
  </div>
10
12
  <div>
11
- <%= form.label :password_confirmation %>
12
- <%= form.password_field :password_confirmation %>
13
+ <%= form.label :password_confirmation %>
14
+ <%= ma_confirm_password_field(form) %>
13
15
  </div>
14
- <%= form.submit 'Update Password' %>
16
+ <%= form.submit t(".submit") %>
15
17
  <% end %>
@@ -1,11 +1,11 @@
1
- <%= form_for @user, url: session_path do |form| %>
1
+ <%= form_with model: @user, url: session_path do |form| %>
2
2
  <div>
3
- <%= form.label :email %>
4
- <%= form.text_field :email %>
3
+ <%= form.label :email %>
4
+ <%= ma_email_field(form) %>
5
5
  </div>
6
- <div>
7
- <%= form.label :password %>
8
- <%= form.password_field :password %>
6
+ <div style="margin-top: 8px;">
7
+ <%= form.label :password %>
8
+ <%= ma_password_field(form) %>
9
9
  </div>
10
- <%= form.submit 'Log in' %>
10
+ <%= form.submit t("sessions.new.submit") %>
11
11
  <% end %>
@@ -1,3 +1,3 @@
1
- <h1>Login</h1>
2
- <%= render partial: 'form' %>
3
- <%= link_to 'I forgot', new_password_reset_path %>
1
+ <h1><%= t(".title") %></h1>
2
+ <%= render partial: "form" %>
3
+ <%= ma_forgot_password_link %>
@@ -1,12 +1,38 @@
1
1
  en:
2
- # controllers
3
2
  email_verifications:
4
3
  create:
5
4
  notice: Verification email sent to %{email}, follow the instructions to complete verification. Thank you!
6
5
  emails:
7
6
  update:
8
7
  notice: Email successfully updated
8
+ minimalist_authentication_mailer:
9
+ update_password:
10
+ opening: Please click the link below to update your password.
11
+ subject: Update Password
12
+ verify_email:
13
+ closing: If you did not request email verification you can safely ignore this message.
14
+ opening: Please click the link below to complete your email verification.
15
+ subject: Email Address Verification
16
+ password_resets:
17
+ new:
18
+ email:
19
+ placeholder: Enter your email address
20
+ instructions: Enter your verified email address, and we'll send you a link to reset your password.
21
+ submit: Send Reset Link
22
+ title: Reset Your Password
23
+ create:
24
+ alert: Please provide a valid email address.
25
+ notice: We've sent password reset instructions to %{email}. If you don't receive the email within a few minutes, please check your spam folder.
9
26
  passwords:
27
+ edit:
28
+ instructions: Please enter your new password.
29
+ password:
30
+ placeholder: New password
31
+ password_confirmation:
32
+ placeholder: Confirm password
33
+ submit: Reset Password
34
+ title: Reset Password
35
+ invalid_token: Your password reset link has expired. Please request a new one to continue.
10
36
  update:
11
37
  notice: Password successfully updated
12
38
  sessions:
@@ -14,13 +40,7 @@ en:
14
40
  alert: Couldn't log you in as %{identifier}
15
41
  destroy:
16
42
  notice: You have been logged out.
17
-
18
- # mailers
19
- minimalist_authentication_mailer:
20
- update_password:
21
- opening: Please click the link below to update your password.
22
- subject: Update Password
23
- verify_email:
24
- closing: If you did not request email verification you can safely ignore this message.
25
- opening: Please click the link below to complete your email verification.
26
- subject: "Email Address Verification"
43
+ new:
44
+ forgot_password: Forgot password?
45
+ title: Sign In
46
+ submit: Sign In
data/config/routes.rb CHANGED
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Rails.application.routes.draw do
4
- resources :user, only: [] do
5
- resource :password, only: %i[edit update]
6
- end
7
-
8
- resource :password_reset, only: %i[new create]
9
-
4
+ resource :email_verification, only: %i[show new create]
10
5
  resource :email, only: %i[edit update]
11
- resource :email_verification, only: %i[new create show]
6
+ resource :password_reset, only: %i[new create]
7
+ resource :password, only: %i[edit update]
12
8
  end
@@ -62,15 +62,19 @@ module MinimalistAuthentication
62
62
  self.logout_redirect_path = :new_session_path
63
63
  end
64
64
 
65
+ # Clear the user_model class
66
+ def clear_user_model
67
+ @user_model = nil
68
+ end
69
+
70
+ # Display deprecation warning for email_prefix
65
71
  def email_prefix=(_)
66
72
  MinimalistAuthentication.deprecator.warn("The #email_prefix configuration setting is no longer supported.")
67
73
  end
68
74
 
69
75
  # Returns the user_model class
70
- # Calling constantize on a string makes this work correctly with
71
- # the Spring application preloader gem.
72
76
  def user_model
73
- user_model_name.constantize
77
+ @user_model ||= user_model_name.constantize
74
78
  end
75
79
  end
76
80
  end
@@ -9,6 +9,8 @@ module MinimalistAuthentication
9
9
  # use skip_before_action to open up specific actions
10
10
  before_action :authorization_required
11
11
 
12
+ helper MinimalistAuthentication::ApplicationHelper
13
+
12
14
  helper_method :current_user, :logged_in?, :authorized?
13
15
  end
14
16
 
@@ -5,9 +5,31 @@ module MinimalistAuthentication
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- before_save :clear_email_verification, if: ->(user) { user.email_changed? }
8
+ generates_token_for :email_verification, expires_in: 1.hour do
9
+ email
10
+ end
9
11
 
10
- scope :email_verified, -> { where("LENGTH(email) > 2").where.not(email_verified_at: nil) }
12
+ before_save :clear_email_verification, if: :email_changed?
13
+
14
+ scope :with_verified_email, -> { where.not(email_verified_at: nil) }
15
+ end
16
+
17
+ module ClassMethods
18
+ def email_verified
19
+ MinimalistAuthentication.deprecator.warn(<<-MSG.squish)
20
+ Calling #email_verified is deprecated.
21
+ Call #with_verified_email instead.
22
+ MSG
23
+ with_verified_email
24
+ end
25
+
26
+ def find_by_verified_email(email:)
27
+ active.with_verified_email.find_by(email:)
28
+ end
29
+ end
30
+
31
+ def email_verified?
32
+ email.present? && email_verified_at.present?
11
33
  end
12
34
 
13
35
  def needs_email_set?
@@ -18,26 +40,22 @@ module MinimalistAuthentication
18
40
  email_verification_enabled? && email.present? && email_verified_at.blank?
19
41
  end
20
42
 
21
- def email_verified?
22
- email.present? && email_verified_at.present?
23
- end
24
-
25
43
  def verify_email(token)
26
- secure_update(token, email_verified_at: Time.zone.now)
44
+ touch(:email_verified_at) if token_owner?(:email_verification, token)
27
45
  end
28
46
 
29
47
  private
30
48
 
31
- def request_email_enabled?
32
- MinimalistAuthentication.configuration.request_email
49
+ def clear_email_verification
50
+ self.email_verified_at = nil
33
51
  end
34
52
 
35
53
  def email_verification_enabled?
36
54
  MinimalistAuthentication.configuration.verify_email
37
55
  end
38
56
 
39
- def clear_email_verification
40
- self.email_verified_at = nil
57
+ def request_email_enabled?
58
+ MinimalistAuthentication.configuration.request_email
41
59
  end
42
60
  end
43
61
  end
@@ -2,5 +2,10 @@
2
2
 
3
3
  module MinimalistAuthentication
4
4
  class Engine < ::Rails::Engine
5
+ isolate_namespace MinimalistAuthentication
6
+
7
+ config.to_prepare do
8
+ MinimalistAuthentication.configuration.clear_user_model
9
+ end
5
10
  end
6
11
  end
@@ -11,8 +11,9 @@ module MinimalistAuthentication
11
11
  included do
12
12
  has_secure_password
13
13
 
14
- # Force validations for a blank password.
15
- attribute :password_required, :boolean, default: false
14
+ generates_token_for :password_reset, expires_in: 1.hour do
15
+ password_salt.last(10)
16
+ end
16
17
 
17
18
  # Email validations
18
19
  validates(
@@ -25,13 +26,18 @@ module MinimalistAuthentication
25
26
 
26
27
  # Password validations
27
28
  # Adds validations for minimum password length and exclusivity.
28
- # has_secure_password adds validations for presence, maximum length, confirmation,
29
- # and password_challenge.
30
- validates :password, length: { minimum: :password_minimum }, if: :validate_password?
31
- validate :password_exclusivity, if: :password?
29
+ # has_secure_password includes validations for presence, maximum length, confirmation, and password_challenge.
30
+ validates(
31
+ :password,
32
+ password_exclusivity: true,
33
+ length: { minimum: :password_minimum },
34
+ allow_blank: true
35
+ )
32
36
 
33
37
  # Active scope
34
38
  scope :active, ->(state = true) { where(active: state) }
39
+
40
+ delegate :password_minimum, to: :class
35
41
  end
36
42
 
37
43
  module ClassMethods
@@ -52,6 +58,9 @@ module MinimalistAuthentication
52
58
  def guest
53
59
  new(email: GUEST_USER_EMAIL).freeze
54
60
  end
61
+
62
+ # Minimum password length
63
+ def password_minimum = 12
55
64
  end
56
65
 
57
66
  # Called after a user is authenticated to determine if the user object should be returned.
@@ -65,15 +74,9 @@ module MinimalistAuthentication
65
74
  active?
66
75
  end
67
76
 
68
- # Remove the has_secure_password password blank error if password is not required.
77
+ # Remove the has_secure_password password blank error if user is inactive.
69
78
  def errors
70
- super.tap { |errors| errors.delete(:password, :blank) unless validate_password? }
71
- end
72
-
73
- # Returns true if the user is not active.
74
- def inactive?
75
- MinimalistAuthentication.deprecator.warn("Calling #inactive? is deprecated.")
76
- !active?
79
+ super.tap { |errors| errors.delete(:password, :blank) if inactive? }
77
80
  end
78
81
 
79
82
  # Returns true if password matches the hashed_password, otherwise returns false.
@@ -89,33 +92,21 @@ module MinimalistAuthentication
89
92
  email == GUEST_USER_EMAIL
90
93
  end
91
94
 
95
+ # Returns true if the user is not active.
96
+ def inactive?
97
+ !active?
98
+ end
99
+
92
100
  # Sets #last_logged_in_at to the current time without updating the updated_at timestamp.
93
101
  def logged_in
94
102
  update_column(:last_logged_in_at, Time.current)
95
103
  end
96
104
 
97
- # Minimum password length
98
- def password_minimum = 12
99
-
100
- # Checks for password presence
101
- def password?
102
- password.present?
103
- end
104
-
105
105
  private
106
106
 
107
- # Ensure password does not match username or email.
108
- def password_exclusivity
109
- %w[username email].each do |field|
110
- errors.add(:password, "can not match #{field}") if password.casecmp?(try(field))
111
- end
112
- end
113
-
114
- # Require password for active users that either do no have a password hash
115
- # stored OR are attempting to set a new password. Set **password_required**
116
- # to true to force validations even when the password field is blank.
117
- def validate_password?
118
- active? && (password_digest.blank? || password? || password_required?)
107
+ # Return true if the user matches the owner of the provided token.
108
+ def token_owner?(purpose, token)
109
+ self.class.find_by_token_for(purpose, token) == self
119
110
  end
120
111
 
121
112
  # Validate email for all users.
@@ -4,51 +4,33 @@ module MinimalistAuthentication
4
4
  module VerifiableToken
5
5
  extend ActiveSupport::Concern
6
6
 
7
- TOKEN_EXPIRATION_HOURS = 6
8
-
9
- # generate secure verification_token and record generation time
10
- def regenerate_verification_token
11
- update_token
12
- end
13
-
14
- def secure_update(token, attributes)
15
- if matches_verification_token?(token)
16
- update(attributes) && clear_token
17
- else
18
- errors.add(:base, "Verification token check failed")
19
- false
20
- end
7
+ included do
8
+ MinimalistAuthentication.deprecator.warn(<<-MSG.squish)
9
+ MinimalistAuthentication::VerifiableToken is no longer required.
10
+ You can safely remove the include from your user model.
11
+ MSG
21
12
  end
22
13
 
23
- def matches_verification_token?(token)
24
- token.present? && verification_token_valid? && secure_match?(token)
14
+ def matches_verification_token?(_token)
15
+ MinimalistAuthentication.deprecator.warn(<<-MSG.squish)
16
+ Calling #matches_verification_token? is deprecated.
17
+ MSG
25
18
  end
26
19
 
27
- def verification_token_valid?
28
- return false if verification_token.blank? || verification_token_generated_at.blank?
29
-
30
- verification_token_generated_at > TOKEN_EXPIRATION_HOURS.hours.ago
31
- end
32
-
33
- private
34
-
35
- def clear_token
36
- update_token(token: nil, time: nil)
37
- end
38
-
39
- def update_token(token: self.class.generate_unique_secure_token, time: Time.now.utc)
40
- update!(
41
- verification_token: token,
42
- verification_token_generated_at: time
43
- )
20
+ def regenerate_verification_token
21
+ MinimalistAuthentication.deprecator.warn(<<-MSG.squish)
22
+ Calling #regenerate_verification_token is deprecated and no longer generates tokens.
23
+ Call #generate_token_for with an argument of :password_reset or
24
+ :email_verification instead.
25
+ MSG
44
26
  end
45
27
 
46
- # Compare the tokens in a time-constant manner, to mitigate timing attacks.
47
- def secure_match?(token)
48
- ActiveSupport::SecurityUtils.secure_compare(
49
- ::Digest::SHA256.hexdigest(token),
50
- ::Digest::SHA256.hexdigest(verification_token)
51
- )
28
+ def verification_token
29
+ MinimalistAuthentication.deprecator.warn(<<-MSG.squish)
30
+ Calling #verification_token is deprecated and no longer returns a valid token.
31
+ Call #generate_token_for with an argument of :password_reset or
32
+ :email_verification instead.
33
+ MSG
52
34
  end
53
35
  end
54
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MinimalistAuthentication
4
- VERSION = "3.1.0"
4
+ VERSION = "3.2.1"
5
5
  end
@@ -11,7 +11,11 @@ require "minimalist_authentication/sessions"
11
11
  require "minimalist_authentication/test_helper"
12
12
 
13
13
  module MinimalistAuthentication
14
- def self.deprecator
15
- @deprecator ||= ActiveSupport::Deprecation.new("4.0", name)
14
+ class << self
15
+ delegate :user_model, to: :configuration
16
+
17
+ def deprecator
18
+ @deprecator ||= ActiveSupport::Deprecation.new("4.0", name)
19
+ end
16
20
  end
17
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minimalist_authentication
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 3.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Baldwin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-11-10 00:00:00.000000000 Z
12
+ date: 2024-11-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bcrypt
@@ -60,8 +60,10 @@ files:
60
60
  - app/controllers/emails_controller.rb
61
61
  - app/controllers/password_resets_controller.rb
62
62
  - app/controllers/passwords_controller.rb
63
+ - app/helpers/minimalist_authentication/application_helper.rb
63
64
  - app/mailers/application_mailer.rb
64
65
  - app/mailers/minimalist_authentication_mailer.rb
66
+ - app/validators/password_exclusivity_validator.rb
65
67
  - app/views/email_verifications/new.html.erb
66
68
  - app/views/email_verifications/show.html.erb
67
69
  - app/views/emails/edit.html.erb