authentication-zero 2.0.0 → 2.2.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +8 -7
  4. data/lib/authentication_zero/version.rb +1 -1
  5. data/lib/generators/authentication/authentication_generator.rb +7 -6
  6. data/lib/generators/authentication/templates/controllers/api/email_verifications_controller.rb.tt +18 -0
  7. data/lib/generators/authentication/templates/controllers/api/password_resets_controller.rb.tt +6 -6
  8. data/lib/generators/authentication/templates/controllers/api/registrations_controller.rb.tt +5 -1
  9. data/lib/generators/authentication/templates/controllers/api/sessions_controller.rb.tt +4 -4
  10. data/lib/generators/authentication/templates/controllers/html/email_verifications_controller.rb.tt +20 -0
  11. data/lib/generators/authentication/templates/controllers/html/emails_controller.rb.tt +2 -2
  12. data/lib/generators/authentication/templates/controllers/html/password_resets_controller.rb.tt +7 -7
  13. data/lib/generators/authentication/templates/controllers/html/passwords_controller.rb.tt +2 -2
  14. data/lib/generators/authentication/templates/controllers/html/registrations_controller.rb.tt +7 -9
  15. data/lib/generators/authentication/templates/controllers/html/sessions_controller.rb.tt +4 -4
  16. data/lib/generators/authentication/templates/erb/emails/edit.html.erb.tt +8 -2
  17. data/lib/generators/authentication/templates/erb/identity_mailer/email_verify_confirmation.html.erb.tt +11 -0
  18. data/lib/generators/authentication/templates/erb/identity_mailer/email_verify_confirmation.text.erb.tt +9 -0
  19. data/lib/generators/authentication/templates/erb/{password_mailer/reset.html.erb.tt → identity_mailer/password_reset_provision.html.erb.tt} +2 -2
  20. data/lib/generators/authentication/templates/erb/{password_mailer/reset.text.erb.tt → identity_mailer/password_reset_provision.text.erb.tt} +2 -2
  21. data/lib/generators/authentication/templates/erb/password_resets/edit.html.erb.tt +1 -1
  22. data/lib/generators/authentication/templates/erb/password_resets/new.html.erb.tt +1 -1
  23. data/lib/generators/authentication/templates/erb/passwords/edit.html.erb.tt +1 -1
  24. data/lib/generators/authentication/templates/erb/session_mailer/signed_in_notification.html.erb.tt +21 -0
  25. data/lib/generators/authentication/templates/erb/session_mailer/signed_in_notification.text.erb.tt +17 -0
  26. data/lib/generators/authentication/templates/erb/sessions/new.html.erb.tt +1 -1
  27. data/lib/generators/authentication/templates/mailers/identity_mailer.rb.tt +15 -0
  28. data/lib/generators/authentication/templates/mailers/session_mailer.rb.tt +6 -0
  29. data/lib/generators/authentication/templates/migrations/create_sessions_migration.rb.tt +1 -0
  30. data/lib/generators/authentication/templates/migrations/create_table_migration.rb.tt +2 -0
  31. data/lib/generators/authentication/templates/models/current.rb.tt +1 -1
  32. data/lib/generators/authentication/templates/models/model.rb.tt +8 -8
  33. data/lib/generators/authentication/templates/models/session.rb.tt +4 -0
  34. data/lib/generators/authentication/templates/test_unit/controllers/api/email_verifications_controller_test.rb.tt +36 -0
  35. data/lib/generators/authentication/templates/test_unit/controllers/api/emails_controller_test.rb.tt +2 -7
  36. data/lib/generators/authentication/templates/test_unit/controllers/api/password_resets_controller_test.rb.tt +18 -7
  37. data/lib/generators/authentication/templates/test_unit/controllers/api/passwords_controller_test.rb.tt +2 -7
  38. data/lib/generators/authentication/templates/test_unit/controllers/api/registrations_controller_test.rb.tt +15 -0
  39. data/lib/generators/authentication/templates/test_unit/controllers/api/sessions_controller_test.rb.tt +2 -0
  40. data/lib/generators/authentication/templates/test_unit/controllers/html/email_verifications_controller_test.rb.tt +35 -0
  41. data/lib/generators/authentication/templates/test_unit/controllers/html/emails_controller_test.rb.tt +5 -10
  42. data/lib/generators/authentication/templates/test_unit/controllers/html/password_resets_controller_test.rb.tt +22 -11
  43. data/lib/generators/authentication/templates/test_unit/controllers/html/passwords_controller_test.rb.tt +5 -10
  44. data/lib/generators/authentication/templates/test_unit/controllers/html/registrations_controller_test.rb.tt +15 -3
  45. data/lib/generators/authentication/templates/test_unit/controllers/html/sessions_controller_test.rb.tt +4 -2
  46. data/lib/generators/authentication/templates/test_unit/fixtures.yml.tt +1 -0
  47. data/lib/generators/authentication/templates/test_unit/sessions.yml.tt +1 -1
  48. data/lib/generators/authentication/templates/test_unit/system/emails_test.rb.tt +11 -2
  49. data/lib/generators/authentication/templates/test_unit/system/password_resets_test.rb.tt +2 -2
  50. data/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt +2 -2
  51. data/lib/generators/authentication/templates/test_unit/system/registrations_test.rb.tt +20 -0
  52. metadata +14 -16
  53. data/lib/generators/authentication/templates/controllers/api/cancellations_controller.rb.tt +0 -5
  54. data/lib/generators/authentication/templates/controllers/html/cancellations_controller.rb.tt +0 -9
  55. data/lib/generators/authentication/templates/erb/cancellations/new.html.erb.tt +0 -11
  56. data/lib/generators/authentication/templates/erb/email_mailer/changed.html.erb.tt +0 -11
  57. data/lib/generators/authentication/templates/erb/email_mailer/changed.text.erb.tt +0 -9
  58. data/lib/generators/authentication/templates/erb/password_mailer/changed.html.erb.tt +0 -7
  59. data/lib/generators/authentication/templates/erb/password_mailer/changed.text.erb.tt +0 -5
  60. data/lib/generators/authentication/templates/mailers/email_mailer.rb.tt +0 -6
  61. data/lib/generators/authentication/templates/mailers/password_mailer.rb.tt +0 -10
  62. data/lib/generators/authentication/templates/test_unit/controllers/api/cancellations_controller_test.rb.tt +0 -20
  63. data/lib/generators/authentication/templates/test_unit/controllers/html/cancellations_controller_test.rb.tt +0 -24
  64. data/lib/generators/authentication/templates/test_unit/system/cancellations_test.rb.tt +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0359a2a997ced43e3eb97612156d7ffe3206aa8a75ede90ad37dbdf0d0a9269
4
- data.tar.gz: 9a50df4bf9804a3da3bb1ee753b7ecd027a336f0624de5a9c5d1cfec1b5354b1
3
+ metadata.gz: ed9892e61478e60ca189e6e835b59c9fd2ab63e2c342e1d67bbdfb57f60dd9ba
4
+ data.tar.gz: d890089e13104ec641c5d26ec5541e3ce9a4cb64cbe2d2276347f0c328efc713
5
5
  SHA512:
6
- metadata.gz: 45b51a6f24b135e1273d076683c1d406755e30ec012c4e5ec632a158ef0bb8432cb9c461254b044ba7c491717eaa95571e1cce85bb6560b19f35cf1ac03a7000
7
- data.tar.gz: 2dd271538d229d7a88df027a44f06e59a4f0092cdc6bb8de214596dfde3cb52c2339e5659c6a8331ba69440a25030ec6c2620255f5d483e0f980a042277a28ae
6
+ metadata.gz: f050cf8b766989090c9a4c415bdc7f91eebb2ff63b3d7632a5bdfa1923d68ebf6f110d8a2eba71af7387c708c6398ea365746d51fdd45903b1362fd5551588b7
7
+ data.tar.gz: 6ec0286f6ea33a9f95551e8808297ab4ffc4d7e609317e5a90c3442c36d7d4c4845bb9746c0e92bea5383ac0ff92f64f652c9453c5b16311ea1eda17b2b96642
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- authentication-zero (2.0.0)
4
+ authentication-zero (2.2.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -4,14 +4,16 @@ The purpose of authentication zero is to generate a pre-built authentication sys
4
4
 
5
5
  ## Features
6
6
 
7
+ - **Simplest code ever**
7
8
  - Sign up
8
9
  - Email and password validations
9
10
  - Reset the user password and send reset instructions
11
+ - Reset the user password only from verified emails
10
12
  - Authentication by cookie (html)
11
13
  - Authentication by token (api)
12
- - Manage sessions
13
- - Send e-mail when email is changed
14
- - Send e-mail when password is changed
14
+ - Send e-mail verification when change your email
15
+ - Send e-mail when sign-in to your account
16
+ - Manage multiple sessions
15
17
  - Cancel my account
16
18
  - Log out
17
19
 
@@ -22,7 +24,6 @@ The purpose of authentication zero is to generate a pre-built authentication sys
22
24
  - [httponly cookies](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html): A cookie with the httponly attribute is inaccessible to the JavaScript, this precaution helps mitigate cross-site scripting (XSS) attacks.
23
25
  - [signed_id](https://api.rubyonrails.org/classes/ActiveRecord/SignedId.html): Returns a signed id that is tamper proof, so it's safe to send in an email or otherwise share with the outside world.
24
26
  - [Current attributes](https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html): Abstract super class that provides a thread-isolated attributes singleton, which resets automatically before and after each request.
25
- - [Callbacks](https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html): We use callbacks to send emails after changing an email or password.
26
27
  - [Action mailer](https://api.rubyonrails.org/classes/ActionMailer/Base.html): Action Mailer allows you to send email from your application using a mailer model and views.
27
28
  - [Log filtering](https://guides.rubyonrails.org/action_controller_overview.html#log-filtering): Parameters 'token' and 'password' are marked [FILTERED] in the log.
28
29
  - [Functional Tests](https://guides.rubyonrails.org/testing.html#functional-tests-for-your-controllers): In Rails, testing the various actions of a controller is a form of writing functional tests.
@@ -56,11 +57,11 @@ Add these lines to your `app/views/home/index.html.erb`:
56
57
  <p>Signed as <%= Current.user.email %></p>
57
58
 
58
59
  <div>
59
- <%= link_to "Change password", edit_passwords_path %>
60
+ <%= link_to "Change password", edit_password_path %>
60
61
  </div>
61
62
 
62
63
  <div>
63
- <%= link_to "Change email", edit_emails_path %>
64
+ <%= link_to "Change email", edit_email_path %>
64
65
  </div>
65
66
 
66
67
  <div>
@@ -68,7 +69,7 @@ Add these lines to your `app/views/home/index.html.erb`:
68
69
  </div>
69
70
 
70
71
  <div>
71
- <%= link_to "Cancel my account & delete my data", new_cancellations_path %>
72
+ <%= button_to "Cancel my account", registration_path, method: :delete %>
72
73
  </div>
73
74
 
74
75
  <%= button_to "Log out", Current.session, method: :delete %>
@@ -1,3 +1,3 @@
1
1
  module AuthenticationZero
2
- VERSION = "2.0.0"
2
+ VERSION = "2.2.1"
3
3
  end
@@ -81,8 +81,8 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
81
81
 
82
82
  def create_views
83
83
  if options.api
84
- directory "erb/email_mailer", "app/views/email_mailer"
85
- directory "erb/password_mailer", "app/views/password_mailer"
84
+ directory "erb/identity_mailer", "app/views/identity_mailer"
85
+ directory "erb/session_mailer", "app/views/session_mailer"
86
86
  else
87
87
  directory "#{template_engine}", "app/views"
88
88
  end
@@ -94,10 +94,11 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
94
94
 
95
95
  def add_routes
96
96
  unless options.skip_routes
97
- route "resource :password_resets, only: [:new, :edit, :create, :update]"
98
- route "resource :cancellations, only: [:new, :create]"
99
- route "resource :passwords, only: [:edit, :update]"
100
- route "resource :emails, only: [:edit, :update]"
97
+ route "resource :registration, only: :destroy"
98
+ route "resource :password_reset, only: [:new, :edit, :create, :update]"
99
+ route "resource :password, only: [:edit, :update]"
100
+ route "resource :email_verification, only: [:new, :edit, :create, :update]"
101
+ route "resource :email, only: [:edit, :update]"
101
102
  route "resources :sessions, only: [:index, :show, :destroy]"
102
103
  route "post 'sign_up', to: 'registrations#create'"
103
104
  route "get 'sign_up', to: 'registrations#new'" unless options.api?
@@ -0,0 +1,18 @@
1
+ class EmailVerificationsController < ApplicationController
2
+ before_action :set_<%= singular_table_name %>, only: :update
3
+
4
+ def create
5
+ IdentityMailer.with(<%= singular_table_name %>: Current.<%= singular_table_name %>).email_verify_confirmation.deliver_later
6
+ end
7
+
8
+ def update
9
+ @<%= singular_table_name %>.update! verified: true
10
+ end
11
+
12
+ private
13
+ def set_<%= singular_table_name %>
14
+ @<%= singular_table_name %> = <%= class_name %>.find_signed!(params[:token], purpose: "verify_#{params[:email]}")
15
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
16
+ render json: { error: "That email verification link is invalid" }, status: :bad_request
17
+ end
18
+ end
@@ -1,13 +1,13 @@
1
1
  class PasswordResetsController < ApplicationController
2
- skip_before_action :authenticate
3
-
4
2
  before_action :set_<%= singular_table_name %>, only: :update
5
3
 
4
+ skip_before_action :authenticate
5
+
6
6
  def create
7
- if @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
8
- PasswordMailer.with(<%= singular_table_name %>: @<%= singular_table_name %>).reset.deliver_later
7
+ if @<%= singular_table_name %> = <%= class_name %>.find_by(email: params[:email], verified: true)
8
+ IdentityMailer.with(<%= singular_table_name %>: @<%= singular_table_name %>).password_reset_provision.deliver_later
9
9
  else
10
- render json: { error: "Sorry, we didn't recognize that email address" }, status: :not_found
10
+ render json: { error: "You can't reset your password until you verify your email" }, status: :not_found
11
11
  end
12
12
  end
13
13
 
@@ -23,7 +23,7 @@ class PasswordResetsController < ApplicationController
23
23
  def set_<%= singular_table_name %>
24
24
  @<%= singular_table_name %> = <%= class_name %>.find_signed!(params[:token], purpose: :password_reset)
25
25
  rescue ActiveSupport::MessageVerifier::InvalidSignature
26
- render json: { error: "Your token has expired, please request a new one" }, status: :bad_request
26
+ render json: { error: "That password reset link is invalid" }, status: :bad_request
27
27
  end
28
28
 
29
29
  def <%= "#{singular_table_name}_params" %>
@@ -1,5 +1,5 @@
1
1
  class RegistrationsController < ApplicationController
2
- skip_before_action :authenticate
2
+ skip_before_action :authenticate, only: :create
3
3
 
4
4
  def create
5
5
  @<%= singular_table_name %> = <%= class_name %>.new(<%= "#{singular_table_name}_params" %>)
@@ -11,6 +11,10 @@ class RegistrationsController < ApplicationController
11
11
  end
12
12
  end
13
13
 
14
+ def destroy
15
+ Current.<%= singular_table_name %>.destroy
16
+ end
17
+
14
18
  private
15
19
  def <%= "#{singular_table_name}_params" %>
16
20
  params.permit(:email, :password, :password_confirmation)
@@ -1,8 +1,8 @@
1
1
  class SessionsController < ApplicationController
2
- skip_before_action :authenticate, only: :create
3
-
4
2
  before_action :set_session, only: %i[ show destroy ]
5
3
 
4
+ skip_before_action :authenticate, only: :create
5
+
6
6
  def index
7
7
  render json: Current.<%= singular_table_name %>.sessions.order(created_at: :desc)
8
8
  end
@@ -20,7 +20,7 @@ class SessionsController < ApplicationController
20
20
 
21
21
  render json: session, status: :created
22
22
  else
23
- render json: { error: "Invalid email or password" }, status: :unauthorized
23
+ render json: { error: "That email or password is incorrect" }, status: :unauthorized
24
24
  end
25
25
  end
26
26
 
@@ -30,7 +30,7 @@ class SessionsController < ApplicationController
30
30
 
31
31
  private
32
32
  def set_session
33
- @session = Current.user.sessions.find(params[:id])
33
+ @session = Current.<%= singular_table_name %>.sessions.find(params[:id])
34
34
  end
35
35
 
36
36
  def session_params
@@ -0,0 +1,20 @@
1
+ class EmailVerificationsController < ApplicationController
2
+ before_action :set_<%= singular_table_name %>, only: :edit
3
+
4
+ def edit
5
+ @<%= singular_table_name %>.update! verified: true
6
+ redirect_to root_path, notice: "Thank you for verifying your email address"
7
+ end
8
+
9
+ def create
10
+ IdentityMailer.with(<%= singular_table_name %>: Current.<%= singular_table_name %>).email_verify_confirmation.deliver_later
11
+ redirect_to root_path, notice: "We sent a verification email to your email address"
12
+ end
13
+
14
+ private
15
+ def set_<%= singular_table_name %>
16
+ @<%= singular_table_name %> = <%= class_name %>.find_signed!(params[:token], purpose: "verify_#{params[:email]}")
17
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
18
+ redirect_to edit_email_path, alert: "That email verification link is invalid"
19
+ end
20
+ end
@@ -6,9 +6,9 @@ class EmailsController < ApplicationController
6
6
 
7
7
  def update
8
8
  if !@<%= singular_table_name %>.authenticate(params[:current_password])
9
- redirect_to edit_emails_path, alert: "The current password you entered is incorrect"
9
+ redirect_to edit_email_path, alert: "The current password you entered is incorrect"
10
10
  elsif @<%= singular_table_name %>.update(<%= "#{singular_table_name}_params" %>)
11
- redirect_to root_path, notice: "Your email has been changed successfully"
11
+ redirect_to root_path, notice: "Your email has been changed"
12
12
  else
13
13
  render :edit, status: :unprocessable_entity
14
14
  end
@@ -1,8 +1,8 @@
1
1
  class PasswordResetsController < ApplicationController
2
- skip_before_action :authenticate
3
-
4
2
  before_action :set_<%= singular_table_name %>, only: %i[ edit update ]
5
3
 
4
+ skip_before_action :authenticate
5
+
6
6
  def new
7
7
  end
8
8
 
@@ -10,11 +10,11 @@ class PasswordResetsController < ApplicationController
10
10
  end
11
11
 
12
12
  def create
13
- if @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
14
- PasswordMailer.with(<%= singular_table_name %>: @<%= singular_table_name %>).reset.deliver_later
15
- redirect_to sign_in_path, notice: "You will receive an email with instructions on how to reset your password in a few minutes"
13
+ if @<%= singular_table_name %> = <%= class_name %>.find_by(email: params[:email], verified: true)
14
+ IdentityMailer.with(<%= singular_table_name %>: @<%= singular_table_name %>).password_reset_provision.deliver_later
15
+ redirect_to sign_in_path, notice: "Check your email for reset instructions"
16
16
  else
17
- redirect_to new_password_resets_path, alert: "Sorry, we didn't recognize that email address"
17
+ redirect_to new_password_reset_path, alert: "You can't reset your password until you verify your email"
18
18
  end
19
19
  end
20
20
 
@@ -30,7 +30,7 @@ class PasswordResetsController < ApplicationController
30
30
  def set_<%= singular_table_name %>
31
31
  @<%= singular_table_name %> = <%= class_name %>.find_signed!(params[:token], purpose: :password_reset)
32
32
  rescue ActiveSupport::MessageVerifier::InvalidSignature
33
- redirect_to new_password_resets_path, alert: "Your token has expired, please request a new one"
33
+ redirect_to new_password_reset_path, alert: "That password reset link is invalid"
34
34
  end
35
35
 
36
36
  def <%= "#{singular_table_name}_params" %>
@@ -6,9 +6,9 @@ class PasswordsController < ApplicationController
6
6
 
7
7
  def update
8
8
  if !@<%= singular_table_name %>.authenticate(params[:current_password])
9
- redirect_to edit_passwords_path, alert: "The current password you entered is incorrect"
9
+ redirect_to edit_password_path, alert: "The current password you entered is incorrect"
10
10
  elsif @<%= singular_table_name %>.update(<%= "#{singular_table_name}_params" %>)
11
- redirect_to root_path, notice: "Your password has been changed successfully"
11
+ redirect_to root_path, notice: "Your password has been changed"
12
12
  else
13
13
  render :edit, status: :unprocessable_entity
14
14
  end
@@ -1,5 +1,5 @@
1
1
  class RegistrationsController < ApplicationController
2
- skip_before_action :authenticate
2
+ skip_before_action :authenticate, only: %i[ new create ]
3
3
 
4
4
  def new
5
5
  @<%= singular_table_name %> = <%= class_name %>.new
@@ -9,21 +9,19 @@ class RegistrationsController < ApplicationController
9
9
  @<%= singular_table_name %> = <%= class_name %>.new(<%= "#{singular_table_name}_params" %>)
10
10
 
11
11
  if @<%= singular_table_name %>.save
12
- @session = @user.sessions.create!(session_params)
13
- cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
14
-
15
- redirect_to root_path, notice: "Welcome! You have signed up successfully"
12
+ redirect_to sign_in_path, notice: "Welcome! You have signed up successfully"
16
13
  else
17
14
  render :new, status: :unprocessable_entity
18
15
  end
19
16
  end
20
17
 
18
+ def destroy
19
+ Current.<%= singular_table_name %>.destroy
20
+ redirect_to sign_in_path, notice: "Your account is closed"
21
+ end
22
+
21
23
  private
22
24
  def <%= "#{singular_table_name}_params" %>
23
25
  params.require(:<%= singular_table_name %>).permit(:email, :password, :password_confirmation)
24
26
  end
25
-
26
- def session_params
27
- { user_agent: request.user_agent, ip_address: request.remote_ip }
28
- end
29
27
  end
@@ -1,8 +1,8 @@
1
1
  class SessionsController < ApplicationController
2
- skip_before_action :authenticate, only: %i[ new create ]
3
-
4
2
  before_action :set_session, only: :destroy
5
3
 
4
+ skip_before_action :authenticate, only: %i[ new create ]
5
+
6
6
  def index
7
7
  @sessions = Current.<%= singular_table_name %>.sessions.order(created_at: :desc)
8
8
  end
@@ -20,7 +20,7 @@ class SessionsController < ApplicationController
20
20
 
21
21
  redirect_to root_path, notice: "Signed in successfully"
22
22
  else
23
- redirect_to sign_in_path(email_hint: params[:email]), alert: "Invalid email or password"
23
+ redirect_to sign_in_path(email_hint: params[:email]), alert: "That email or password is incorrect"
24
24
  end
25
25
  end
26
26
 
@@ -31,7 +31,7 @@ class SessionsController < ApplicationController
31
31
 
32
32
  private
33
33
  def set_session
34
- @session = Current.user.sessions.find(params[:id])
34
+ @session = Current.<%= singular_table_name %>.sessions.find(params[:id])
35
35
  end
36
36
 
37
37
  def session_params
@@ -1,8 +1,14 @@
1
1
  <p style="color: red"><%%= alert %></p>
2
2
 
3
- <h1>Change your email</h1>
3
+ <%% if Current.<%= singular_table_name %>.verified? %>
4
+ <h1>Change your email</h1>
5
+ <%% else %>
6
+ <h1>Verify your email </h1>
7
+ <p>We sent a verification email to the address below. Check that email and follow those instructions to confirm it's your email address.</p>
8
+ <p><%%= button_to "Re-send verification email", email_verification_path %></p>
9
+ <%% end %>
4
10
 
5
- <%%= form_with(model: @<%= model_resource_name %>, url: emails_path) do |form| %>
11
+ <%%= form_with(model: @<%= model_resource_name %>, url: email_path) do |form| %>
6
12
  <%% if @<%= singular_table_name %>.errors.any? %>
7
13
  <div style="color: red">
8
14
  <h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
@@ -0,0 +1,11 @@
1
+ <p>Hey there,</p>
2
+
3
+ <p>This is to confirm that <%%= @<%= singular_table_name %>.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.</p>
4
+
5
+ <p><strong>You must hit the link below to confirm that you received this email.</strong></p>
6
+
7
+ <%%= link_to "Yes, use this email for my account", edit_email_verification_url(token: @signed_id, email: @<%= singular_table_name %>.email) %>
8
+
9
+ <hr>
10
+
11
+ <p>Have questions or need help? Just reply to this email and our support team will help you sort it out.</p>
@@ -0,0 +1,9 @@
1
+ Hey there,
2
+
3
+ This is to confirm that <%%= @<%= singular_table_name %>.email %> is the email you want to use on your account. If you ever lose your password, that's where we'll email a reset link.
4
+
5
+ You must hit the link below to confirm that you received this email.
6
+
7
+ [Yes, use this email for my account]<%%= edit_email_verification_url(token: @signed_id, email: @<%= singular_table_name %>.email) %>
8
+
9
+ Have questions or need help? Just reply to this email and our support team will help you sort it out.
@@ -1,8 +1,8 @@
1
1
  <p>Hey there,</p>
2
2
 
3
- <p>Can't remember your password for <strong><%%= params[:<%= singular_table_name %>].email %></strong>? That's OK, it happens. Just hit the link below to set a new one.</p>
3
+ <p>Can't remember your password for <strong><%%= @session.<%= singular_table_name %>.email %></strong>? That's OK, it happens. Just hit the link below to set a new one.</p>
4
4
 
5
- <p><%%= link_to "Reset my password", edit_password_resets_url(token: @signed_id) %></p>
5
+ <p><%%= link_to "Reset my password", edit_password_reset_url(token: @signed_id) %></p>
6
6
 
7
7
  <p>If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.</p>
8
8
 
@@ -1,8 +1,8 @@
1
1
  Hey there,
2
2
 
3
- Can't remember your password for <%%= params[:<%= singular_table_name %>].email %>? That's OK, it happens. Just hit the link below to set a new one.
3
+ Can't remember your password for <%%= @session.<%= singular_table_name %>.email %>? That's OK, it happens. Just hit the link below to set a new one.
4
4
 
5
- [Reset my password]<%%= edit_password_resets_url(token: @signed_id) %>
5
+ [Reset my password]<%%= edit_password_reset_url(token: @signed_id) %>
6
6
 
7
7
  If you did not request a password reset you can safely ignore this email, it expires in 20 minutes. Only someone with access to this email account can reset your password.
8
8
 
@@ -1,6 +1,6 @@
1
1
  <h1>Reset your password</h1>
2
2
 
3
- <%%= form_with(model: @<%= model_resource_name %>, url: password_resets_path) do |form| %>
3
+ <%%= form_with(model: @<%= model_resource_name %>, url: password_reset_path) do |form| %>
4
4
  <%% if @<%= singular_table_name %>.errors.any? %>
5
5
  <div style="color: red">
6
6
  <h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <h1>Forgot your password?</h1>
4
4
 
5
- <%%= form_with(url: password_resets_path) do |form| %>
5
+ <%%= form_with(url: password_reset_path) do |form| %>
6
6
  <div>
7
7
  <%%= form.label :email, style: "display: block" %>
8
8
  <%%= form.email_field :email, autofocus: true, required: true %>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <h1>Change your password</h1>
4
4
 
5
- <%%= form_with(model: @<%= model_resource_name %>, url: passwords_path) do |form| %>
5
+ <%%= form_with(model: @<%= model_resource_name %>, url: password_path) do |form| %>
6
6
  <%% if @<%= singular_table_name %>.errors.any? %>
7
7
  <div style="color: red">
8
8
  <h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
@@ -0,0 +1,21 @@
1
+ <p>Hey there,</p>
2
+
3
+ <p>A new device just signed in to your account (<%%= @session.<%= singular_table_name %>.email %>).</p>
4
+
5
+ <p>
6
+ <strong><%%= @session.user_agent %></strong>
7
+ <br>
8
+ <%%= @session.created_at %>
9
+ <br>
10
+ IP address: <%%= @session.ip_address %>
11
+ </p>
12
+
13
+ <p><strong>If this was you, carry on.</strong> We could notify you about sign-ins from this device again.</p>
14
+
15
+ <p><strong>If you don't recognize this device</strong>, someone else may have accessed your account. You should immediately <%%= link_to "change your password", new_password_reset_url %>.</p>
16
+
17
+ <p><strong>Tip:</strong> It's a good idea to periodically review all of the <%%= link_to "devices and sessions", sessions_url %> in your account for suspicious activity.</p>
18
+
19
+ <hr>
20
+
21
+ <p>Have questions or need help? Just reply to this email and our support team will help you sort it out.</p>
@@ -0,0 +1,17 @@
1
+ Hey there,
2
+
3
+ A new device just signed in to your account (<%%= @session.<%= singular_table_name %>.email %>).
4
+
5
+ <%%= @session.user_agent %>
6
+
7
+ <%%= @session.created_at %>
8
+
9
+ <%%= @session.ip_address %>
10
+
11
+ If this was you, carry on. We could notify you about sign-ins from this device again.
12
+
13
+ If you don't recognize this device, someone else may have accessed your account. You should immediately [change your password]<%%= new_password_reset_url %>.
14
+
15
+ Tip: It's a good idea to periodically review all of the [devices and sessions]<%%= sessions_url %> in your account for suspicious activity.
16
+
17
+ <p>Have questions or need help? Just reply to this email and our support team will help you sort it out.
@@ -23,5 +23,5 @@
23
23
 
24
24
  <div>
25
25
  <%%= link_to "Sign up", sign_up_path %> |
26
- <%%= link_to "Forgot your password?", new_password_resets_path %>
26
+ <%%= link_to "Forgot your password?", new_password_reset_path %>
27
27
  </div>
@@ -0,0 +1,15 @@
1
+ class IdentityMailer < ApplicationMailer
2
+ def password_reset_provision
3
+ @<%= singular_table_name %> = params[:<%= singular_table_name %>]
4
+ @signed_id = params[:<%= singular_table_name %>].signed_id(purpose: :password_reset, expires_in: 20.minutes)
5
+
6
+ mail to: @<%= singular_table_name %>.email, subject: "Reset your password"
7
+ end
8
+
9
+ def email_verify_confirmation
10
+ @<%= singular_table_name %> = params[:<%= singular_table_name %>]
11
+ @signed_id = params[:<%= singular_table_name %>].signed_id(purpose: "verify_#{@<%= singular_table_name %>.email}", expires_in: 20.minutes)
12
+
13
+ mail to: @<%= singular_table_name %>.email, subject: "Verify your email"
14
+ end
15
+ end
@@ -0,0 +1,6 @@
1
+ class SessionMailer < ApplicationMailer
2
+ def signed_in_notification
3
+ @session = params[:session]
4
+ mail to: @session.<%= singular_table_name %>.email, subject: "New sign-in to your account"
5
+ end
6
+ end
@@ -2,6 +2,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
2
2
  def change
3
3
  create_table :sessions do |t|
4
4
  t.references :<%= singular_table_name %>, null: false, foreign_key: true
5
+
5
6
  t.string :user_agent
6
7
  t.string :ip_address
7
8
 
@@ -4,6 +4,8 @@ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Mi
4
4
  t.string :email, null: false
5
5
  t.string :password_digest, null: false
6
6
 
7
+ t.boolean :verified, default: false
8
+
7
9
  t.timestamps
8
10
  end
9
11
 
@@ -2,6 +2,6 @@ class Current < ActiveSupport::CurrentAttributes
2
2
  attribute :session, :<%= singular_table_name %>
3
3
 
4
4
  def session=(session)
5
- super; Current.<%= singular_table_name %> = session.<%= singular_table_name %>
5
+ super; self.<%= singular_table_name %> = session.<%= singular_table_name %>
6
6
  end
7
7
  end
@@ -11,15 +11,15 @@ class <%= class_name %> < ApplicationRecord
11
11
  self.email = email.downcase.strip
12
12
  end
13
13
 
14
- after_update_commit do
15
- if self.email_previously_changed?
16
- EmailMailer.with(change: self.email_previous_change).changed.deliver_later
17
- end
14
+ after_create_commit do
15
+ IdentityMailer.with(<%= singular_table_name %>: self).email_verify_confirmation.deliver_later
18
16
  end
19
17
 
20
- after_update_commit do
21
- if self.password_digest_previously_changed?
22
- PasswordMailer.with(<%= singular_table_name %>: self).changed.deliver_later
23
- end
18
+ after_update_commit if: :email_previously_changed? do
19
+ update_columns verified: false
20
+ end
21
+
22
+ after_update_commit if: :email_previously_changed? do
23
+ IdentityMailer.with(<%= singular_table_name %>: self).email_verify_confirmation.deliver_later
24
24
  end
25
25
  end
@@ -1,3 +1,7 @@
1
1
  class Session < ApplicationRecord
2
2
  belongs_to :<%= singular_table_name %>
3
+
4
+ after_create_commit do
5
+ SessionMailer.with(session: self).signed_in_notification.deliver_later
6
+ end
3
7
  end
@@ -0,0 +1,36 @@
1
+ require "test_helper"
2
+
3
+ class EmailVerificationsControllerTest < ActionDispatch::IntegrationTest
4
+ setup do
5
+ @<%= singular_table_name %>, @token = sign_in_as(<%= table_name %>(:lazaro_nixon))
6
+ @sid = @<%= singular_table_name %>.signed_id(purpose: "verify_#{@<%= singular_table_name %>.email}", expires_in: 20.minutes)
7
+ @sid_exp = @<%= singular_table_name %>.signed_id(purpose: "verify_#{@<%= singular_table_name %>.email}", expires_in: 0.minutes)
8
+
9
+ @<%= singular_table_name %>.update! verified: false
10
+ end
11
+
12
+ test "should send a verification email" do
13
+ assert_enqueued_email_with IdentityMailer, :email_verify_confirmation, args: { <%= singular_table_name %>: @<%= singular_table_name %> } do
14
+ post email_verification_url, headers: { "Authorization" => "Bearer #{@token}" }
15
+ end
16
+
17
+ assert_response :no_content
18
+ end
19
+
20
+ test "should verify email" do
21
+ patch email_verification_url, params: { token: @sid, email: @<%= singular_table_name %>.email }, headers: { "Authorization" => "Bearer #{@token}" }
22
+ assert_response :no_content
23
+ end
24
+
25
+ test "should not verify email with expired token" do
26
+ patch email_verification_url, params: { token: @sid_exp, email: @<%= singular_table_name %>.email }, headers: { "Authorization" => "Bearer #{@token}" }
27
+
28
+ assert_response :bad_request
29
+ assert_equal "That email verification link is invalid", response.parsed_body["error"]
30
+ end
31
+
32
+ def sign_in_as(<%= singular_table_name %>)
33
+ post(sign_in_url, params: { email: <%= singular_table_name %>.email, password: "secret123" })
34
+ [<%= singular_table_name %>, response.headers["X-Session-Token"]]
35
+ end
36
+ end