authentication-zero 1.0.2 → 2.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +9 -7
  4. data/lib/authentication_zero/version.rb +1 -1
  5. data/lib/generators/authentication/authentication_generator.rb +20 -13
  6. data/lib/generators/authentication/templates/controllers/api/password_resets_controller.rb.tt +1 -5
  7. data/lib/generators/authentication/templates/controllers/api/sessions_controller.rb.tt +26 -4
  8. data/lib/generators/authentication/templates/controllers/html/cancellations_controller.rb.tt +1 -1
  9. data/lib/generators/authentication/templates/controllers/html/emails_controller.rb.tt +2 -2
  10. data/lib/generators/authentication/templates/controllers/html/password_resets_controller.rb.tt +3 -3
  11. data/lib/generators/authentication/templates/controllers/html/passwords_controller.rb.tt +2 -2
  12. data/lib/generators/authentication/templates/controllers/html/registrations_controller.rb.tt +1 -2
  13. data/lib/generators/authentication/templates/controllers/html/sessions_controller.rb.tt +21 -9
  14. data/lib/generators/authentication/templates/erb/cancellations/new.html.erb.tt +1 -1
  15. data/lib/generators/authentication/templates/erb/emails/edit.html.erb.tt +1 -1
  16. data/lib/generators/authentication/templates/erb/password_mailer/reset.html.erb.tt +1 -1
  17. data/lib/generators/authentication/templates/erb/password_mailer/reset.text.erb.tt +1 -1
  18. data/lib/generators/authentication/templates/erb/password_resets/edit.html.erb.tt +1 -1
  19. data/lib/generators/authentication/templates/erb/password_resets/new.html.erb.tt +1 -1
  20. data/lib/generators/authentication/templates/erb/passwords/edit.html.erb.tt +1 -1
  21. data/lib/generators/authentication/templates/erb/session_mailer/signed_in.html.erb.tt +21 -0
  22. data/lib/generators/authentication/templates/erb/session_mailer/signed_in.text.erb.tt +17 -0
  23. data/lib/generators/authentication/templates/erb/sessions/index.html.erb.tt +34 -0
  24. data/lib/generators/authentication/templates/erb/sessions/new.html.erb.tt +1 -6
  25. data/lib/generators/authentication/templates/mailers/password_mailer.rb.tt +1 -5
  26. data/lib/generators/authentication/templates/mailers/session_mailer.rb.tt +6 -0
  27. data/lib/generators/authentication/templates/migrations/create_sessions_migration.rb.tt +11 -0
  28. data/lib/generators/authentication/templates/migrations/create_table_migration.rb.tt +12 -0
  29. data/lib/generators/authentication/templates/models/current.rb.tt +5 -1
  30. data/lib/generators/authentication/templates/models/model.rb.tt +2 -28
  31. data/lib/generators/authentication/templates/models/session.rb.tt +7 -0
  32. data/lib/generators/authentication/templates/test_unit/controllers/api/cancellations_controller_test.rb.tt +3 -3
  33. data/lib/generators/authentication/templates/test_unit/controllers/api/emails_controller_test.rb.tt +4 -9
  34. data/lib/generators/authentication/templates/test_unit/controllers/api/password_resets_controller_test.rb.tt +4 -11
  35. data/lib/generators/authentication/templates/test_unit/controllers/api/passwords_controller_test.rb.tt +4 -9
  36. data/lib/generators/authentication/templates/test_unit/controllers/api/sessions_controller_test.rb.tt +16 -6
  37. data/lib/generators/authentication/templates/test_unit/controllers/html/cancellations_controller_test.rb.tt +3 -3
  38. data/lib/generators/authentication/templates/test_unit/controllers/html/emails_controller_test.rb.tt +5 -10
  39. data/lib/generators/authentication/templates/test_unit/controllers/html/password_resets_controller_test.rb.tt +8 -8
  40. data/lib/generators/authentication/templates/test_unit/controllers/html/passwords_controller_test.rb.tt +5 -10
  41. data/lib/generators/authentication/templates/test_unit/controllers/html/registrations_controller_test.rb.tt +1 -3
  42. data/lib/generators/authentication/templates/test_unit/controllers/html/sessions_controller_test.rb.tt +14 -7
  43. data/lib/generators/authentication/templates/test_unit/fixtures.yml.tt +0 -1
  44. data/lib/generators/authentication/templates/test_unit/sessions.yml.tt +6 -0
  45. data/lib/generators/authentication/templates/test_unit/system/cancellations_test.rb.tt +2 -2
  46. data/lib/generators/authentication/templates/test_unit/system/emails_test.rb.tt +2 -2
  47. data/lib/generators/authentication/templates/test_unit/system/password_resets_test.rb.tt +2 -2
  48. data/lib/generators/authentication/templates/test_unit/system/passwords_test.rb.tt +2 -2
  49. data/lib/generators/authentication/templates/test_unit/system/sessions_test.rb.tt +8 -1
  50. metadata +10 -7
  51. data/lib/generators/authentication/templates/erb/email_mailer/changed.html.erb.tt +0 -11
  52. data/lib/generators/authentication/templates/erb/email_mailer/changed.text.erb.tt +0 -9
  53. data/lib/generators/authentication/templates/erb/password_mailer/changed.html.erb.tt +0 -7
  54. data/lib/generators/authentication/templates/erb/password_mailer/changed.text.erb.tt +0 -5
  55. data/lib/generators/authentication/templates/mailers/email_mailer.rb.tt +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c038ff0229826dcb0ef7416e95f41ef7e18a01a7e5993a3995eb992c2a21a44a
4
- data.tar.gz: 6295fb855b41a0ea8242916c18c40686f41d6f32e83fe66ed8922fe3b2c24ee7
3
+ metadata.gz: 44c36959694f55d09ae33d82d3dbe2091a49372c4792eda84bfddf94c62b636c
4
+ data.tar.gz: 9d4b642209fe681865973ad1a5b13bd8bba1bf63140ae49acfe25923ce085514
5
5
  SHA512:
6
- metadata.gz: fbbbb3287c24e23260ca3ad7adcc206c864f02843c255c111b6c1f06fd822581db35c522358dfa60a626e83bb2eb0f8494274847185a122f9cd90726c94ae2cf
7
- data.tar.gz: fb427adb9595073f03b76bba0683cafd5ce9a699b25d79bbd4df2902abc7f4643f326a4b25c017ce6ab98b7e21d69d0532539aaeec8a018c3d69b7fd597f4d81
6
+ metadata.gz: cbc64b8248e0bf392983bb19a3034b76471dc54511cb7ffee7ed122bb3c7f1424ec4211b11b5d0026395d622c1e6d6b35c3be09710f5617f45db2daece00ec49
7
+ data.tar.gz: f3ccd41d8f2c417918cf65c7124fc597355d07bc96ba509bba12174d5161df33b2434a3af76a67f2e23de020f7cec549fc8a326619643373e4c05eb1b799c4bd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- authentication-zero (1.0.2)
4
+ authentication-zero (2.1.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -4,26 +4,24 @@ 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
10
11
  - Authentication by cookie (html)
11
12
  - Authentication by token (api)
12
- - Remember me (html)
13
- - Send e-mail when email is changed
14
- - Send e-mail when password is changed
13
+ - Send e-mail when sign-in to your account
14
+ - Manage multiple sessions
15
15
  - Cancel my account
16
16
  - Log out
17
17
 
18
18
  ## Security and best practices
19
19
 
20
20
  - [has_secure_password](https://api.rubyonrails.org/classes/ActiveModel/SecurePassword/ClassMethods.html#method-i-has_secure_password): Adds methods to set and authenticate against a BCrypt password.
21
- - [has_secure_token](https://api.rubyonrails.org/classes/ActiveRecord/SecureToken/ClassMethods.html#method-i-has_secure_token): Adds methods to generate unique tokens.
22
21
  - [signed cookies](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html): Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from the cookie again.
23
22
  - [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.
24
23
  - [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.
25
24
  - [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.
26
- - [Callbacks](https://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html): We use callbacks to send emails after changing an email or password.
27
25
  - [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.
28
26
  - [Log filtering](https://guides.rubyonrails.org/action_controller_overview.html#log-filtering): Parameters 'token' and 'password' are marked [FILTERED] in the log.
29
27
  - [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,19 +54,23 @@ Add these lines to your `app/views/home/index.html.erb`:
56
54
 
57
55
  <p>Signed as <%= Current.user.email %></p>
58
56
 
57
+ <div>
58
+ <%= link_to "Change password", edit_passwords_path %>
59
+ </div>
60
+
59
61
  <div>
60
62
  <%= link_to "Change email", edit_emails_path %>
61
63
  </div>
62
64
 
63
65
  <div>
64
- <%= link_to "Change password", edit_passwords_path %>
66
+ <%= link_to "Manage Sessions", sessions_path %>
65
67
  </div>
66
68
 
67
69
  <div>
68
70
  <%= link_to "Cancel my account & delete my data", new_cancellations_path %>
69
71
  </div>
70
72
 
71
- <%= button_to "Log out", sign_out_path, method: :delete %>
73
+ <%= button_to "Log out", Current.session, method: :delete %>
72
74
  ```
73
75
 
74
76
  And you'll need to set up the default URL options for the mailer in each environment. Here is a possible configuration for `config/environments/development.rb`:
@@ -1,3 +1,3 @@
1
1
  module AuthenticationZero
2
- VERSION = "1.0.2"
2
+ VERSION = "2.1.1"
3
3
  end
@@ -1,6 +1,8 @@
1
1
  require "rails/generators/active_record"
2
2
 
3
3
  class AuthenticationGenerator < Rails::Generators::NamedBase
4
+ include ActiveRecord::Generators::Migration
5
+
4
6
  class_option :api, type: :boolean, desc: "Generates API authentication"
5
7
 
6
8
  class_option :migration, type: :boolean, default: true
@@ -18,14 +20,16 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
18
20
  uncomment_lines "Gemfile", /bcrypt/
19
21
  end
20
22
 
21
- def create_migration
23
+ def create_migrations
22
24
  if options.migration
23
- invoke "migration", ["create_#{table_name}", "email:string:uniq", "password:digest", "session_token:string:uniq"]
25
+ migration_template "migrations/create_table_migration.rb", "#{db_migrate_path}/create_#{table_name}.rb"
26
+ migration_template "migrations/create_sessions_migration.rb", "#{db_migrate_path}/create_sessions.rb"
24
27
  end
25
28
  end
26
29
 
27
30
  def create_models
28
31
  template "models/model.rb", "app/models/#{file_name}.rb"
32
+ template "models/session.rb", "app/models/session.rb"
29
33
  template "models/current.rb", "app/models/current.rb"
30
34
  end
31
35
 
@@ -34,6 +38,7 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
34
38
  def create_fixture_file
35
39
  if options.fixture && options.fixture_replacement.nil?
36
40
  template "#{test_framework}/fixtures.yml", "test/fixtures/#{fixture_file_name}.yml"
41
+ template "#{test_framework}/sessions.yml", "test/fixtures/sessions.yml"
37
42
  end
38
43
  end
39
44
 
@@ -45,8 +50,10 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
45
50
 
46
51
  private
47
52
  def authenticate
48
- authenticate_or_request_with_http_token do |token, _options|
49
- Current.#{singular_table_name} = #{class_name}.find_signed_session_token(token)
53
+ if session = authenticate_with_http_token { |token, _| Session.find_signed(token) }
54
+ Current.session = session
55
+ else
56
+ request_http_token_authentication
50
57
  end
51
58
  end
52
59
  CODE
@@ -56,10 +63,10 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
56
63
 
57
64
  private
58
65
  def authenticate
59
- if #{singular_table_name} = #{class_name}.find_by_session_token(cookies.signed[:session_token])
60
- Current.#{singular_table_name} = #{singular_table_name}
66
+ if session = Session.find_by_id(cookies.signed[:session_token])
67
+ Current.session = session
61
68
  else
62
- redirect_to sign_in_path, alert: "You need to sign in or sign up before continuing"
69
+ redirect_to sign_in_path
63
70
  end
64
71
  end
65
72
  CODE
@@ -74,7 +81,7 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
74
81
 
75
82
  def create_views
76
83
  if options.api
77
- directory "erb/email_mailer", "app/views/email_mailer"
84
+ directory "erb/session_mailer", "app/views/session_mailer"
78
85
  directory "erb/password_mailer", "app/views/password_mailer"
79
86
  else
80
87
  directory "#{template_engine}", "app/views"
@@ -87,11 +94,11 @@ class AuthenticationGenerator < Rails::Generators::NamedBase
87
94
 
88
95
  def add_routes
89
96
  unless options.skip_routes
90
- route "resource :password_resets, only: [:new, :edit, :create, :update]"
91
- route "resource :cancellations, only: [:new, :create]"
92
- route "resource :passwords, only: [:edit, :update]"
93
- route "resource :emails, only: [:edit, :update]"
94
- route "delete 'sign_out', to: 'sessions#destroy'"
97
+ route "resource :password_reset, only: [:new, :edit, :create, :update]"
98
+ route "resource :cancellation, only: [:new, :create]"
99
+ route "resource :password, only: [:edit, :update]"
100
+ route "resource :email, only: [:edit, :update]"
101
+ route "resources :sessions, only: [:index, :show, :destroy]"
95
102
  route "post 'sign_up', to: 'registrations#create'"
96
103
  route "get 'sign_up', to: 'registrations#new'" unless options.api?
97
104
  route "post 'sign_in', to: 'sessions#create'"
@@ -1,11 +1,7 @@
1
1
  class PasswordResetsController < ApplicationController
2
2
  skip_before_action :authenticate
3
3
 
4
- before_action :set_<%= singular_table_name %>, only: %i[ edit update ]
5
-
6
- def edit
7
- render json: { error: "Open this link in your device" }, status: :not_found
8
- end
4
+ before_action :set_<%= singular_table_name %>, only: :update
9
5
 
10
6
  def create
11
7
  if @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
@@ -1,17 +1,39 @@
1
1
  class SessionsController < ApplicationController
2
- skip_before_action :authenticate, except: :destroy
2
+ skip_before_action :authenticate, only: :create
3
+
4
+ before_action :set_session, only: %i[ show destroy ]
5
+
6
+ def index
7
+ render json: Current.<%= singular_table_name %>.sessions.order(created_at: :desc)
8
+ end
9
+
10
+ def show
11
+ render json: @session
12
+ end
3
13
 
4
14
  def create
5
15
  @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
6
16
 
7
17
  if @<%= singular_table_name %>.try(:authenticate, params[:password])
8
- render json: { session_token: @<%= singular_table_name %>.signed_session_token }, status: :ok
18
+ session = @<%= singular_table_name %>.sessions.create!(session_params)
19
+ response.set_header("X-Session-Token", session.signed_id)
20
+
21
+ render json: session, status: :created
9
22
  else
10
- render json: { error: "Invalid email or password" }, status: :unauthorized
23
+ render json: { error: "That email or password is incorrect" }, status: :unauthorized
11
24
  end
12
25
  end
13
26
 
14
27
  def destroy
15
- Current.<%= singular_table_name %>.regenerate_session_token
28
+ @session.destroy
16
29
  end
30
+
31
+ private
32
+ def set_session
33
+ @session = Current.<%= singular_table_name %>.sessions.find(params[:id])
34
+ end
35
+
36
+ def session_params
37
+ { user_agent: request.user_agent, ip_address: request.remote_ip }
38
+ end
17
39
  end
@@ -4,6 +4,6 @@ class CancellationsController < ApplicationController
4
4
 
5
5
  def create
6
6
  Current.<%= singular_table_name %>.destroy
7
- redirect_to sign_in_path, notice: "Bye! Your account has been successfully cancelled"
7
+ redirect_to sign_in_path, notice: "Your account is closed"
8
8
  end
9
9
  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
@@ -12,9 +12,9 @@ class PasswordResetsController < ApplicationController
12
12
  def create
13
13
  if @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
14
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"
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: "Sorry, we didn't recognize that email address"
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: "Your token has expired, please request a new one"
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
@@ -9,8 +9,7 @@ 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
- cookies.signed[:session_token] = { value: @<%= singular_table_name %>.session_token, httponly: true }
13
- redirect_to root_path, notice: "Welcome! You have signed up successfully"
12
+ redirect_to sign_in_path, notice: "Welcome! You have signed up successfully"
14
13
  else
15
14
  render :new, status: :unprocessable_entity
16
15
  end
@@ -1,5 +1,11 @@
1
1
  class SessionsController < ApplicationController
2
- skip_before_action :authenticate, except: :destroy
2
+ skip_before_action :authenticate, only: %i[ new create ]
3
+
4
+ before_action :set_session, only: :destroy
5
+
6
+ def index
7
+ @sessions = Current.<%= singular_table_name %>.sessions.order(created_at: :desc)
8
+ end
3
9
 
4
10
  def new
5
11
  @<%= singular_table_name %> = <%= class_name %>.new
@@ -9,20 +15,26 @@ class SessionsController < ApplicationController
9
15
  @<%= singular_table_name %> = <%= class_name %>.find_by_email(params[:email])
10
16
 
11
17
  if @<%= singular_table_name %>.try(:authenticate, params[:password])
12
- if params[:remember_me] == "1"
13
- cookies.signed.permanent[:session_token] = { value: @<%= singular_table_name %>.session_token, httponly: true }
14
- else
15
- cookies.signed[:session_token] = { value: @<%= singular_table_name %>.session_token, httponly: true }
16
- end
18
+ @session = @<%= singular_table_name %>.sessions.create!(session_params)
19
+ cookies.signed.permanent[:session_token] = { value: @session.id, httponly: true }
17
20
 
18
21
  redirect_to root_path, notice: "Signed in successfully"
19
22
  else
20
- 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"
21
24
  end
22
25
  end
23
26
 
24
27
  def destroy
25
- Current.<%= singular_table_name %>.regenerate_session_token
26
- redirect_to sign_in_path, notice: "Signed out successfully"
28
+ @session.destroy
29
+ redirect_to sessions_path, notice: "That session has been logged out"
27
30
  end
31
+
32
+ private
33
+ def set_session
34
+ @session = Current.<%= singular_table_name %>.sessions.find(params[:id])
35
+ end
36
+
37
+ def session_params
38
+ { user_agent: request.user_agent, ip_address: request.remote_ip }
39
+ end
28
40
  end
@@ -7,5 +7,5 @@
7
7
  <br>
8
8
 
9
9
  <div>
10
- <%%= button_to "OK, close my account", cancellations_path %>
10
+ <%%= button_to "OK, close my account", cancellation_path %>
11
11
  </div>
@@ -2,7 +2,7 @@
2
2
 
3
3
  <h1>Change your email</h1>
4
4
 
5
- <%%= form_with(model: @<%= model_resource_name %>, url: emails_path) do |form| %>
5
+ <%%= form_with(model: @<%= model_resource_name %>, url: email_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>
@@ -2,7 +2,7 @@
2
2
 
3
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>
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
 
@@ -2,7 +2,7 @@ Hey there,
2
2
 
3
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.
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 won't 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 won't 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.
@@ -0,0 +1,34 @@
1
+ <p style="color: green"><%%= notice %></p>
2
+
3
+ <h1>Sessions</h1>
4
+
5
+ <div id="sessions">
6
+ <%% @sessions.each do |session| %>
7
+ <div id="<%%= dom_id session %>">
8
+ <p>
9
+ <strong>User Agent:</strong>
10
+ <%%= session.user_agent %>
11
+ </p>
12
+
13
+ <p>
14
+ <strong>Ip Address:</strong>
15
+ <%%= session.ip_address %>
16
+ </p>
17
+
18
+ <p>
19
+ <strong>Created at:</strong>
20
+ <%%= session.created_at %>
21
+ </p>
22
+
23
+ </div>
24
+ <p>
25
+ <%%= button_to "Log out", session, method: :delete %>
26
+ </p>
27
+ <%% end %>
28
+ </div>
29
+
30
+ <br>
31
+
32
+ <div>
33
+ <%%= link_to "Back", root_path %>
34
+ </div>
@@ -14,11 +14,6 @@
14
14
  <%%= form.password_field :password, required: true, autocomplete: "current-password" %>
15
15
  </div>
16
16
 
17
- <div>
18
- <%%= form.check_box :remember_me %>
19
- <%%= form.label :remember_me %>
20
- </div>
21
-
22
17
  <div>
23
18
  <%%= form.submit "Sign in" %>
24
19
  </div>
@@ -28,5 +23,5 @@
28
23
 
29
24
  <div>
30
25
  <%%= link_to "Sign up", sign_up_path %> |
31
- <%%= link_to "Forgot your password?", new_password_resets_path %>
26
+ <%%= link_to "Forgot your password?", new_password_reset_path %>
32
27
  </div>
@@ -1,10 +1,6 @@
1
1
  class PasswordMailer < ApplicationMailer
2
- def changed
3
- mail to: params[:<%= singular_table_name %>].email
4
- end
5
-
6
2
  def reset
7
3
  @signed_id = params[:<%= singular_table_name %>].signed_id(purpose: :password_reset, expires_in: 20.minutes)
8
- mail to: params[:<%= singular_table_name %>].email
4
+ mail to: params[:<%= singular_table_name %>].email, subject: "Reset your password"
9
5
  end
10
6
  end
@@ -0,0 +1,6 @@
1
+ class SessionMailer < ApplicationMailer
2
+ def signed_in
3
+ @session = params[:session]
4
+ mail to: @session.<%= singular_table_name %>.email, subject: "New sign-in to your account"
5
+ end
6
+ end
@@ -0,0 +1,11 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :sessions do |t|
4
+ t.references :<%= singular_table_name %>, null: false, foreign_key: true
5
+ t.string :user_agent
6
+ t.string :ip_address
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,12 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ create_table :<%= table_name %> do |t|
4
+ t.string :email, null: false
5
+ t.string :password_digest, null: false
6
+
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :<%= table_name %>, :email, unique: true
11
+ end
12
+ end
@@ -1,3 +1,7 @@
1
1
  class Current < ActiveSupport::CurrentAttributes
2
- attribute :<%= singular_table_name %>
2
+ attribute :session, :<%= singular_table_name %>
3
+
4
+ def session=(session)
5
+ super; Current.<%= singular_table_name %> = session.<%= singular_table_name %>
6
+ end
3
7
  end
@@ -1,7 +1,8 @@
1
1
  class <%= class_name %> < ApplicationRecord
2
- has_secure_token :session_token
3
2
  has_secure_password
4
3
 
4
+ has_many :sessions, dependent: :destroy
5
+
5
6
  validates :email, presence: true, uniqueness: true
6
7
  validates :email, format: { with: /\A[^@\s]+@[^@\s]+\z/ }
7
8
  validates_length_of :password, minimum: 8, allow_blank: true
@@ -9,31 +10,4 @@ class <%= class_name %> < ApplicationRecord
9
10
  before_validation do
10
11
  self.email = email.downcase.strip
11
12
  end
12
-
13
- after_update_commit do
14
- if self.email_previously_changed?
15
- EmailMailer.with(change: self.email_previous_change).changed.deliver_later
16
- end
17
- end
18
-
19
- after_update_commit do
20
- if self.password_digest_previously_changed?
21
- PasswordMailer.with(<%= singular_table_name %>: self).changed.deliver_later
22
- end
23
- end
24
- <% if options.api? %>
25
- def signed_session_token
26
- Rails.application.message_verifier(:session_token).generate(session_token)
27
- end
28
-
29
- def self.find_signed_session_token(signed_session_token)
30
- if session_token = Rails.application.message_verifier(:session_token).verified(signed_session_token)
31
- find_by_session_token(session_token)
32
- end
33
- end
34
-
35
- def as_json(options = {})
36
- super(options.merge(except: [:password_digest, :session_token]))
37
- end
38
- <% end -%>
39
13
  end
@@ -0,0 +1,7 @@
1
+ class Session < ApplicationRecord
2
+ belongs_to :<%= singular_table_name %>
3
+
4
+ after_create_commit do
5
+ SessionMailer.with(session: self).signed_in.deliver_later
6
+ end
7
+ end
@@ -7,7 +7,7 @@ class CancellationsControllerTest < ActionDispatch::IntegrationTest
7
7
 
8
8
  test "should create cancellation" do
9
9
  assert_difference("<%= class_name %>.count", -1) do
10
- post cancellations_url, headers: { "Authorization" => "Bearer #{@token}" }
10
+ post cancellation_url, headers: { "Authorization" => "Bearer #{@token}" }
11
11
  end
12
12
 
13
13
  assert_response :no_content
@@ -15,6 +15,6 @@ class CancellationsControllerTest < ActionDispatch::IntegrationTest
15
15
 
16
16
  def sign_in_as(<%= singular_table_name %>)
17
17
  post(sign_in_url, params: { email: <%= singular_table_name %>.email, password: "secret123" })
18
- [<%= singular_table_name %>, response.parsed_body["session_token"]]
19
- end
18
+ [<%= singular_table_name %>, response.headers["X-Session-Token"]]
19
+ end
20
20
  end