kaze 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/lib/kaze/commands/install_command.rb +3 -1
  4. data/lib/kaze/commands/installs_hotwire_stack.rb +2 -2
  5. data/lib/kaze/commands/installs_inertia_stacks.rb +7 -7
  6. data/lib/kaze/version.rb +1 -1
  7. data/stubs/default/app/forms/auth/login_form.rb +2 -2
  8. data/stubs/default/app/forms/auth/new_password_form.rb +2 -2
  9. data/stubs/default/app/forms/auth/send_password_reset_link_form.rb +2 -4
  10. data/stubs/default/app/mailers/application_mailer.rb +1 -1
  11. data/stubs/default/app/mailers/user_mailer.rb +16 -1
  12. data/stubs/default/app/models/concerns/can_reset_password.rb +1 -1
  13. data/stubs/default/app/models/concerns/must_verify_email.rb +15 -0
  14. data/stubs/default/app/models/session_guard.rb +157 -0
  15. data/stubs/default/app/models/user.rb +1 -0
  16. data/stubs/default/app/views/user_mailer/reset_password.html.erb +2 -2
  17. data/stubs/default/app/views/user_mailer/verify_email.html.erb +33 -0
  18. data/stubs/default/config/routes.rb +4 -0
  19. data/stubs/default/db/migrate/20240101000000_create_users.rb +2 -0
  20. data/stubs/default/test/factories/users.rb +5 -0
  21. data/stubs/default/test/integration/auth/authentication_test.rb +4 -6
  22. data/stubs/default/test/integration/auth/email_verification_test.rb +40 -0
  23. data/stubs/default/test/integration/auth/password_reset_test.rb +3 -3
  24. data/stubs/default/test/integration/password_update_test.rb +2 -2
  25. data/stubs/default/test/integration/profile_test.rb +4 -4
  26. data/stubs/default/test/test_helper.rb +1 -1
  27. data/stubs/hotwire/app/components/application_logo_component.rb +2 -5
  28. data/stubs/hotwire/app/components/modal_component.rb +5 -5
  29. data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +3 -2
  30. data/stubs/hotwire/app/controllers/auth/email_verification_notification_controller.rb +21 -0
  31. data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +2 -2
  32. data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +1 -1
  33. data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +6 -2
  34. data/stubs/hotwire/app/controllers/auth/verified_email_controller.rb +23 -0
  35. data/stubs/hotwire/app/controllers/concerns/authenticate.rb +10 -0
  36. data/stubs/hotwire/app/controllers/concerns/set_current_auth.rb +1 -1
  37. data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +17 -0
  38. data/stubs/hotwire/app/controllers/password_controller.rb +1 -1
  39. data/stubs/hotwire/app/controllers/profile_controller.rb +2 -2
  40. data/stubs/hotwire/app/views/auth/verify_email.html.erb +23 -0
  41. data/stubs/hotwire/app/views/layouts/_navigation.html.erb +1 -1
  42. data/stubs/hotwire/app/views/layouts/guest.html.erb +1 -1
  43. data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +1 -1
  44. data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +3 -3
  45. data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb +21 -0
  46. data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +2 -3
  47. data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +3 -3
  48. data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +6 -2
  49. data/stubs/inertia-common/app/controllers/auth/verified_email_controller.rb +23 -0
  50. data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +10 -0
  51. data/stubs/inertia-common/app/controllers/concerns/set_current_auth.rb +1 -1
  52. data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +17 -0
  53. data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
  54. data/stubs/inertia-common/app/controllers/profile_controller.rb +2 -2
  55. data/stubs/inertia-common/test/integration/password_update_test.rb +2 -2
  56. data/stubs/inertia-common/test/integration/profile_test.rb +4 -4
  57. data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +2 -9
  58. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +1 -4
  59. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +3 -5
  60. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +4 -25
  61. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +1 -4
  62. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +1 -4
  63. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +3 -5
  64. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +3 -5
  65. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +3 -5
  66. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +1 -7
  67. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +15 -53
  68. data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +1 -1
  69. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +3 -6
  70. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +9 -23
  71. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +1 -4
  72. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +1 -4
  73. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/VerifyEmail.tsx +47 -0
  74. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +2 -8
  75. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +1 -5
  76. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +7 -19
  77. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +7 -15
  78. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +7 -16
  79. data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +1 -2
  80. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +1 -5
  81. data/stubs/inertia-react-ts/config/tailwind.config.js +1 -6
  82. data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +4 -9
  83. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +1 -4
  84. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +3 -13
  85. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +10 -45
  86. data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +3 -7
  87. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +4 -11
  88. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +2 -10
  89. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +1 -5
  90. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +1 -4
  91. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/VerifyEmail.vue +50 -0
  92. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +3 -11
  93. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +2 -10
  94. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +5 -9
  95. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +2 -9
  96. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +3 -13
  97. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +2 -5
  98. data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -6
  99. metadata +15 -4
  100. data/MIT-LICENSE +0 -20
  101. data/stubs/default/app/models/auth.rb +0 -57
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d575337712c9b2651ca597a59bb56dfea851cf552ff3d1e4878d2182b6bff4a5
4
- data.tar.gz: 8e91a3772f93eba05dbe2e656a087c015789ccf75b70b5132fd2ba419009effc
3
+ metadata.gz: 53f603a627b691ff3fee85613cd44327e85799c85c3b75557ee10c9757dba9e7
4
+ data.tar.gz: ca7677bad4b8446b1288a1d3a7d4db40edb6a77f851140a37b86e3607223c63d
5
5
  SHA512:
6
- metadata.gz: a2562af016d65f894dd72e473a4b159bbf27799018872f4c5b4d29c8fb3290af031c3f5b2f487a3f2ef8552ed95247c2ad2a78cdbb31c62a91583376e1ca051c
7
- data.tar.gz: 5d110365415c3d2878dbfb8c4f364af7200f5cfba500731ced7c47fbdde0680d9d27ac28e4c1c681f8804d28c397d5754fcde08ec4d6ecb7a973c313ebe5127b
6
+ metadata.gz: 979b1c79b3e9c447a4cf5adf3dfb1661b166f603114ce2e107c61719180c8a8102d2f5614de513ecef3b5f8847664f4618ea8e5bc6689f624d2646b76596ae3e
7
+ data.tar.gz: 4cb564c4b6ffd71e46adae28a2aeb903774bf7bd30db4c5375a0e649a91f77b4fd24ef8ddd79856513ef80d5b5f7b8c9e0f8aed8752cd9c637a65c589289a95b
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ <p align="center"><img src="/art/logo.svg" alt="Logo Kaze"></p>
2
+
1
3
  # Kaze
2
4
 
3
5
  Heavily inspired by [Laravel Breeze](https://github.com/laravel/breeze), this gem offers authentication and application starter kits to give you a head start building your new Rails application. These kits automatically scaffold your application with the routes, controllers, and views you need to register and authenticate your application's users.
@@ -9,6 +9,8 @@ class Kaze::Commands::InstallCommand < Thor
9
9
 
10
10
  desc 'install [STACK]', 'Install the Kaze controllers and resources. Supported stacks: hotwire, react, vue.'
11
11
  def install(stack = 'hotwire')
12
+ return say 'Kaze must be run in a new Rails application.', :red unless File.exist?("#{Dir.pwd}/bin/rails")
13
+
12
14
  if stack == 'hotwire'
13
15
  return install_hotwire_stack
14
16
  end
@@ -53,7 +55,7 @@ class Kaze::Commands::InstallCommand < Thor
53
55
  def install_migrations
54
56
  ensure_directory_exists("#{Dir.pwd}/db/migrate")
55
57
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/db/migrate", "#{Dir.pwd}/db/migrate")
56
- stdin, _ = Open3.capture3('rails version')
58
+ stdin, _ = Open3.capture3("#{Dir.pwd}/bin/rails version")
57
59
  versions = stdin.gsub!('Rails ', '').split('.')
58
60
  railsVersion = [ versions[0], versions[1] ].join('.')
59
61
  Dir.children("#{Dir.pwd}/db/migrate").each do |file|
@@ -32,7 +32,7 @@ module Kaze::Commands::InstallsHotwireStack
32
32
  FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/hotwire/app/views", "#{Dir.pwd}/app/views")
33
33
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
34
34
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
35
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
35
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer", "#{Dir.pwd}/app/views/user_mailer")
36
36
 
37
37
  # Components + Pages...
38
38
  ensure_directory_exists("#{Dir.pwd}/app/components")
@@ -57,7 +57,7 @@ module Kaze::Commands::InstallsHotwireStack
57
57
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/hotwire/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
58
58
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
59
59
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
60
- run_command('rails tailwindcss:build')
60
+ run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
61
61
 
62
62
  say ''
63
63
  say 'Kaze scaffolding installed successfully.', :green
@@ -34,7 +34,7 @@ module Kaze::Commands::InstallsInertiaStacks
34
34
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/app/views/layouts/application.html.erb", "#{Dir.pwd}/app/views/layouts/application.html.erb")
35
35
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
36
36
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
37
- FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
37
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer", "#{Dir.pwd}/app/views/user_mailer")
38
38
 
39
39
  # Components + Pages...
40
40
  ensure_directory_exists("#{Dir.pwd}/app/javascript")
@@ -60,9 +60,9 @@ module Kaze::Commands::InstallsInertiaStacks
60
60
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
61
61
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
62
62
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
63
- File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
64
- run_command('rails generate js_routes:middleware')
65
- run_command('rails tailwindcss:build')
63
+ File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bundle exec rake js:routes:typescript && bin/vite dev\n")
64
+ run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
65
+ run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
66
66
 
67
67
  say ''
68
68
  say 'Installing and building Node dependencies.', :magenta
@@ -140,9 +140,9 @@ module Kaze::Commands::InstallsInertiaStacks
140
140
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
141
141
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
142
142
  FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
143
- File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
144
- run_command('rails generate js_routes:middleware')
145
- run_command('rails tailwindcss:build')
143
+ File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bundle exec rake js:routes:typescript && bin/vite dev\n")
144
+ run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
145
+ run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
146
146
 
147
147
  say ''
148
148
  say 'Installing and building Node dependencies.', :magenta
data/lib/kaze/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kaze
2
- VERSION = '0.7.0'
2
+ VERSION = '0.9.0'
3
3
  end
@@ -1,5 +1,5 @@
1
1
  class Auth::LoginForm < ApplicationForm
2
- attr_accessor :email, :password
2
+ attr_accessor :email, :password, :remember
3
3
 
4
4
  validates :email, presence: true, email: true
5
5
  validates :password, presence: true
@@ -7,6 +7,6 @@ class Auth::LoginForm < ApplicationForm
7
7
  def authenticate
8
8
  return if invalid?
9
9
 
10
- errors.add(:email, message: 'These credentials do not match our records.') unless Current.auth.attempt(email: email, password: password)
10
+ errors.add(:email, message: 'These credentials do not match our records.') unless Current.auth.attempt({ email: @email, password: @password }, @remember)
11
11
  end
12
12
  end
@@ -7,14 +7,14 @@ class Auth::NewPasswordForm < ApplicationForm
7
7
  def reset?
8
8
  return false if invalid?
9
9
 
10
- user = User.find_by_token_for(:password_reset, token)
10
+ user = User.find_by_token_for(:password_reset, @token)
11
11
 
12
12
  if user.nil?
13
13
  errors.add(:password, message: 'This password reset token is invalid.')
14
14
  return false
15
15
  end
16
16
 
17
- user.update(password: password)
17
+ user.update(password: @password)
18
18
 
19
19
  true
20
20
  end
@@ -6,16 +6,14 @@ class Auth::SendPasswordResetLinkForm < ApplicationForm
6
6
  def send_reset_link?
7
7
  return false if invalid?
8
8
 
9
- user = User.find_by(email: email)
9
+ user = User.find_by(email: @email)
10
10
 
11
11
  if user.nil?
12
12
  errors.add(:email, message: "We can't find a user with that email address.")
13
13
  return false
14
14
  end
15
15
 
16
- token = user.generate_token_for(:password_reset)
17
-
18
- user.send_password_reset_notification(token)
16
+ user.send_password_reset_notification(user.generate_token_for(:password_reset))
19
17
 
20
18
  true
21
19
  end
@@ -6,6 +6,6 @@ class ApplicationMailer < ActionMailer::Base
6
6
  layout 'mailer'
7
7
 
8
8
  def default_url_options
9
- { host: ENV.fetch('APP_URL', 'http://localhost') }
9
+ { host: ENV.fetch('APP_URL', 'http://localhost:3000') }
10
10
  end
11
11
  end
@@ -1,8 +1,23 @@
1
1
  class UserMailer < ApplicationMailer
2
- def reset_password
2
+ def reset_password(token)
3
+ @reset_url = password_reset_url(token: token)
4
+
3
5
  mail(
4
6
  to: params[:user].email,
5
7
  subject: 'Reset Password Notification'
6
8
  )
7
9
  end
10
+
11
+ def verify_email
12
+ verifier = ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', ''))
13
+
14
+ signature = verifier.generate(verification_verify_url(id: params[:user].id, hash: Digest::SHA1.hexdigest(params[:user].email)), expires_in: 60.minutes.from_now.to_i)
15
+
16
+ @verification_url = verification_verify_url(id: params[:user].id, hash: Digest::SHA1.hexdigest(params[:user].email), signature: signature)
17
+
18
+ mail(
19
+ to: params[:user].email,
20
+ subject: 'Verify Email Address'
21
+ )
22
+ end
8
23
  end
@@ -1,5 +1,5 @@
1
1
  module CanResetPassword
2
2
  def send_password_reset_notification(token)
3
- UserMailer.with(user: self, token: token).reset_password.deliver_later
3
+ UserMailer.with(user: self).reset_password(token).deliver_later
4
4
  end
5
5
  end
@@ -0,0 +1,15 @@
1
+ module MustVerifyEmail
2
+ extend ActiveSupport::Concern
3
+
4
+ def has_verified_email?
5
+ !email_verified_at.nil?
6
+ end
7
+
8
+ def mark_email_as_verified
9
+ self.update(email_verified_at: Time.now)
10
+ end
11
+
12
+ def send_email_verification_notification
13
+ UserMailer.with(user: self).verify_email.deliver_later
14
+ end
15
+ end
@@ -0,0 +1,157 @@
1
+ class SessionGuard
2
+ attr_reader :name
3
+ attr_reader :last_attempted
4
+ attr_reader :via_remember
5
+ attr_reader :remember_duration
6
+ attr_reader :session
7
+ attr_reader :cookies
8
+ attr_reader :logged_out
9
+ attr_reader :recall_attempted
10
+ attr_reader :user
11
+
12
+ def initialize(name:, session:, cookies: nil)
13
+ @name = name
14
+ @session = session
15
+ @cookies = cookies
16
+ @via_remember = false
17
+ @remember_duration = 576000
18
+ @logged_out = false
19
+ @recall_attempted = false
20
+ end
21
+
22
+ def get_user
23
+ return nil if @logged_out
24
+
25
+ return @user unless @user.nil?
26
+
27
+ id = @session[get_name]
28
+
29
+ @user = User.find_by(id: id) unless id.nil?
30
+
31
+ if @user.nil?
32
+ @user = user_from_recaller
33
+
34
+ update_session(@user.id) if @user
35
+ end
36
+
37
+ @user
38
+ end
39
+
40
+ def attempt(credentials = {}, remember = false)
41
+ @last_attempted = user = User.authenticate_by(credentials)
42
+
43
+ return false if user.nil?
44
+
45
+ login(user, remember)
46
+
47
+ true
48
+ end
49
+
50
+ def login(user, remember = false)
51
+ update_session(user.id)
52
+
53
+ if remember
54
+ ensure_remember_token_is_set(user)
55
+ create_recaller_cookie(user)
56
+ end
57
+
58
+ set_user(user)
59
+ end
60
+
61
+ def logout
62
+ user = get_user
63
+
64
+ clear_user_data_from_storage
65
+
66
+ cycle_remember_token(user) unless @user.nil? || user.remember_token.blank?
67
+
68
+ @user = nil
69
+
70
+ @logged_out = true
71
+ end
72
+
73
+ def set_user(user)
74
+ @user = user
75
+ @logged_out = false
76
+ end
77
+
78
+ def check?
79
+ !get_user.nil?
80
+ end
81
+
82
+ private
83
+
84
+ def user_from_recaller
85
+ _recaller = recaller
86
+
87
+ return nil if _recaller.nil? || !_recaller.valid? || @recall_attempted
88
+
89
+ @recall_attempted = true
90
+
91
+ user = User.find_by(id: _recaller.id, remember_token: _recaller.token)
92
+
93
+ @via_remember = true unless user.nil?
94
+
95
+ user
96
+ end
97
+
98
+ def recaller
99
+ return nil if @cookies.nil?
100
+
101
+ _recaller = @cookies.signed[get_recaller_name]
102
+
103
+ _recaller ? Recaller.new(_recaller) : nil
104
+ end
105
+
106
+ def update_session(id)
107
+ @session[get_name] = id
108
+ end
109
+
110
+ def ensure_remember_token_is_set(user)
111
+ cycle_remember_token(user) if user.remember_token.blank?
112
+ end
113
+
114
+ def create_recaller_cookie(user)
115
+ @cookies.signed[get_recaller_name] = {
116
+ value: "#{user.id}|#{user.remember_token}|#{user.password_digest}",
117
+ expires: @remember_duration.minutes.from_now
118
+ }
119
+ end
120
+
121
+ def clear_user_data_from_storage
122
+ @session.delete(get_name)
123
+ @cookies.delete(get_recaller_name) unless @cookies.nil?
124
+ end
125
+
126
+ def cycle_remember_token(user)
127
+ user.update(remember_token: SecureRandom.alphanumeric(60))
128
+ end
129
+
130
+ def get_name
131
+ "login_#{@name}_#{Digest::SHA1.hexdigest(self.class.name)}".to_sym
132
+ end
133
+
134
+ def get_recaller_name
135
+ "remember_#{@name}_#{Digest::SHA1.hexdigest(self.class.name)}".to_sym
136
+ end
137
+ end
138
+
139
+ class Recaller
140
+ attr_reader :recaller
141
+
142
+ def initialize(recaller)
143
+ @recaller = recaller.split('|')
144
+ end
145
+
146
+ def id
147
+ @recaller[0]
148
+ end
149
+
150
+ def token
151
+ @recaller[1]
152
+ end
153
+
154
+ def valid?
155
+ @recaller.length >= 3 && @recaller[0].present? && @recaller[1].present?
156
+ end
157
+ end
@@ -1,5 +1,6 @@
1
1
  class User < ApplicationRecord
2
2
  include CanResetPassword
3
+ # include MustVerifyEmail
3
4
 
4
5
  has_secure_password
5
6
 
@@ -12,7 +12,7 @@
12
12
  <table border="0" cellpadding="0" cellspacing="0" role="presentation">
13
13
  <tr>
14
14
  <td>
15
- <a href="<%= password_reset_url(token: params[:token]) %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
15
+ <a href="<%= @reset_url %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
16
16
  </td>
17
17
  </tr>
18
18
  </table>
@@ -30,5 +30,5 @@
30
30
  <%= ENV.fetch("APP_NAME", "Rails") %></p>
31
31
  <!-- Subcopy -->
32
32
  <% content_for :subcopy do %>
33
- <p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to password_reset_url(token: params[:token]), password_reset_url(token: params[:token]) %><span></p>
33
+ <p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to @reset_url, @reset_url %><span></p>
34
34
  <% end %>
@@ -0,0 +1,33 @@
1
+ <!-- Greeting -->
2
+ <h1>Hello!</h1>
3
+ <!-- Intro Lines -->
4
+ <p>Please click the button below to verify your email address.</p>
5
+ <!-- Action Button -->
6
+ <table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
7
+ <tr>
8
+ <td align="center">
9
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
10
+ <tr>
11
+ <td align="center">
12
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation">
13
+ <tr>
14
+ <td>
15
+ <a href="<%= @verification_url %>" class="button button-primary" target="_blank" rel="noopener">Verify Email Address</a>
16
+ </td>
17
+ </tr>
18
+ </table>
19
+ </td>
20
+ </tr>
21
+ </table>
22
+ </td>
23
+ </tr>
24
+ </table>
25
+ <!-- Outro Lines -->
26
+ <p>If you did not create an account, no further action is required.</p>
27
+ <!-- Salutation -->
28
+ <p>Regards,<br>
29
+ <%= ENV.fetch("APP_NAME", "Rails") %></p>
30
+ <!-- Subcopy -->
31
+ <% content_for :subcopy do %>
32
+ <p>If you're having trouble clicking the "Verify Email Address" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to @verification_url, @verification_url %><span></p>
33
+ <% end %>
@@ -15,6 +15,10 @@ Rails.application.routes.draw do
15
15
  get 'reset-password/:token', to: 'auth/new_password#new', as: :password_reset
16
16
  post 'reset-password', to: 'auth/new_password#create', as: :password_store
17
17
 
18
+ get 'verify-email', to: 'auth/email_verification_notification#new', as: :verification_notice
19
+ post 'email/verification-notification', to: 'auth/email_verification_notification#create', as: :verification_send
20
+ get 'verify-email/:id/:hash', to: 'auth/verified_email#create', as: :verification_verify
21
+
18
22
  post 'logout', to: 'auth/authenticated_session#destroy', as: :logout
19
23
 
20
24
  get 'dashboard', to: 'dashboard#index', as: :dashboard
@@ -3,7 +3,9 @@ class CreateUsers < ActiveRecord::Migration
3
3
  create_table :users do |table|
4
4
  table.string :name
5
5
  table.string :email
6
+ table.timestamp :email_verified_at, null: true
6
7
  table.string :password_digest
8
+ table.string :remember_token, null: true
7
9
  table.timestamps
8
10
  end
9
11
  end
@@ -2,6 +2,11 @@ FactoryBot.define do
2
2
  factory :user do
3
3
  name { Faker::Name.name }
4
4
  email { Faker::Internet.email }
5
+ email_verified_at { Time.now }
5
6
  password { 'password' }
7
+
8
+ trait :unverified do
9
+ email_verified_at { nil }
10
+ end
6
11
  end
7
12
  end
@@ -8,7 +8,7 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
8
8
  end
9
9
 
10
10
  test 'users can authenticate using the login screen' do
11
- user = FactoryBot.create :user
11
+ user = FactoryBot.create(:user)
12
12
 
13
13
  post login_path, params: {
14
14
  email: user.email,
@@ -20,7 +20,7 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
20
20
  end
21
21
 
22
22
  test 'users cannot authenticate with invalid password' do
23
- user = FactoryBot.create :user
23
+ user = FactoryBot.create(:user)
24
24
 
25
25
  post login_path, params: {
26
26
  email: user.email,
@@ -31,11 +31,9 @@ class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
31
31
  end
32
32
 
33
33
  test 'users can logout' do
34
- user = FactoryBot.create :user
34
+ user = FactoryBot.create(:user)
35
35
 
36
- acting_as user
37
-
38
- post logout_path
36
+ acting_as(user).post logout_path
39
37
 
40
38
  assert_guest
41
39
  assert_redirected_to '/'
@@ -0,0 +1,40 @@
1
+ require 'test_helper'
2
+
3
+ class EmailVerificationTest < ActionDispatch::IntegrationTest
4
+ if User.include?(MustVerifyEmail)
5
+ test 'email verification screen can be rendered' do
6
+ user = FactoryBot.create(:user, :unverified)
7
+
8
+ acting_as(user).get verification_notice_path
9
+
10
+ assert_response :success
11
+ end
12
+
13
+ test 'email can be verified' do
14
+ user = FactoryBot.create(:user, :unverified)
15
+
16
+ acting_as(user).get verification_url(id: user.id, hash: Digest::SHA1.hexdigest(user.email))
17
+
18
+ assert user.reload.has_verified_email?
19
+ assert_redirected_to dashboard_path(verified: '1')
20
+ end
21
+
22
+ test 'email is not verified with invalid hash' do
23
+ user = FactoryBot.create(:user, :unverified)
24
+
25
+ acting_as(user).get verification_url(id: user.id, hash: Digest::SHA1.hexdigest('wrong-email'))
26
+
27
+ assert_not user.reload.has_verified_email?
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def verification_url(params)
34
+ verifier = ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', ''))
35
+
36
+ signature = verifier.generate(verification_verify_url(params), expires_in: 60.minutes.from_now.to_i)
37
+
38
+ verification_verify_url(params.merge(signature: signature))
39
+ end
40
+ end
@@ -8,7 +8,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
8
8
  end
9
9
 
10
10
  test 'reset password link can be requested' do
11
- user = FactoryBot.create :user
11
+ user = FactoryBot.create(:user)
12
12
  token = user.generate_token_for(:password_reset)
13
13
  email = UserMailer.with(user: user, token: token).reset_password
14
14
 
@@ -20,7 +20,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
20
20
  end
21
21
 
22
22
  test 'reset password screen can be rendered' do
23
- user = FactoryBot.create :user
23
+ user = FactoryBot.create(:user)
24
24
 
25
25
  get password_reset_path(token: user.generate_token_for(:password_reset))
26
26
 
@@ -28,7 +28,7 @@ class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
28
28
  end
29
29
 
30
30
  test 'password can be reset_with_valid_token' do
31
- user = FactoryBot.create :user
31
+ user = FactoryBot.create(:user)
32
32
 
33
33
  post password_store_path, params: {
34
34
  token: user.generate_token_for(:password_reset),
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  class PasswordUpdateTest < ActionDispatch::IntegrationTest
4
4
  test 'password can be updated' do
5
- user = FactoryBot.create :user
5
+ user = FactoryBot.create(:user)
6
6
 
7
7
  acting_as(user).put password_update_path, params: {
8
8
  current_password: 'password',
@@ -15,7 +15,7 @@ class PasswordUpdateTest < ActionDispatch::IntegrationTest
15
15
  end
16
16
 
17
17
  test 'correct password must be provided to update password' do
18
- user = FactoryBot.create :user
18
+ user = FactoryBot.create(:user)
19
19
 
20
20
  acting_as(user).put password_update_path, params: {
21
21
  current_password: 'wrong-password',
@@ -2,7 +2,7 @@ require 'test_helper'
2
2
 
3
3
  class ProfileTest < ActionDispatch::IntegrationTest
4
4
  test 'profile page is displayed' do
5
- user = FactoryBot.create :user
5
+ user = FactoryBot.create(:user)
6
6
 
7
7
  acting_as(user).get profile_edit_path
8
8
 
@@ -10,7 +10,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
10
10
  end
11
11
 
12
12
  test 'profile information can be updated' do
13
- user = FactoryBot.create :user
13
+ user = FactoryBot.create(:user)
14
14
 
15
15
  acting_as(user).patch profile_edit_path, params: {
16
16
  name: 'Test User',
@@ -26,7 +26,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
26
26
  end
27
27
 
28
28
  test 'user can delete their account' do
29
- user = FactoryBot.create :user
29
+ user = FactoryBot.create(:user)
30
30
 
31
31
  acting_as(user).delete profile_destroy_path, params: {
32
32
  password: 'password'
@@ -39,7 +39,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
39
39
  end
40
40
 
41
41
  test 'correct password must be provided to delete account' do
42
- user = FactoryBot.create :user
42
+ user = FactoryBot.create(:user)
43
43
 
44
44
  acting_as(user).delete profile_destroy_path, params: {
45
45
  password: 'wrong-password'
@@ -18,7 +18,7 @@ module ActiveSupport
18
18
  end
19
19
 
20
20
  def current_auth
21
- Auth.new('web', session)
21
+ SessionGuard.new(name: 'web', session: session)
22
22
  end
23
23
 
24
24
  def assert_authenticated
@@ -1,10 +1,7 @@
1
1
  class ApplicationLogoComponent < ViewComponent::Base
2
2
  erb_template <<~ERB
3
- <svg viewBox="0 -6 32 32" xmlns="http://www.w3.org/2000/svg" <%= sanitize @attributes.join(" ") %>>
4
- <g fill="none" fill-rule="evenodd">
5
- <path d="M0-6h32v32H0z"/>
6
- <path fill="#c00" fill-rule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/>
7
- </g>
3
+ <svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg" <%= sanitize @attributes.join(" ") %>>
4
+ <path d="m 237.03985,281.43515 c -1.54085,-2.49315 -1.43016,-32.97159 0.24592,-67.72985 3.02813,-62.79557 2.98467,-63.24086 -6.84044,-70.12264 -5.43834,-3.80913 -16.32202,-6.92574 -24.18594,-6.92574 -22.00734,0 -30.84257,-6.63121 -30.84257,-23.14872 0,-18.460367 5.31742,-22.209117 35.7331,-25.191397 27.97004,-2.74248 30.5663,-4.04226 26.94175,-13.48771 -2.14986,-5.6024 -13.90805,-6.59952 -77.8227,-6.59952 -69.400535,0 -75.177944,-0.58548 -73.8551,-7.48424 1.284851,-6.700534 9.490461,-7.708169 78.36353,-9.622595 42.31063,-1.176095 74.04161,-3.44104 70.51333,-5.033217 -11.28272,-5.091508 -103.7232,-12.917843 -134.65799,-11.40068 l -29.878628,1.465415 -3.171696,17.106837 c -1.744436,9.40876 -4.948521,63.009007 -7.120182,119.111597 -2.277699,58.84196 -5.815243,103.87156 -8.360116,106.41647 -2.4264,2.42639 -7.676475,3.15877 -11.666833,1.62754 -5.877786,-2.25554 -6.747202,-6.22009 -4.579445,-20.88223 1.471667,-9.954 3.74014,-63.32438 5.041047,-118.60085 2.647925,-112.511911 3.2489,-117.857541 13.890324,-123.552663 4.513472,-2.415528 45.033525,-3.067128 100.341569,-1.613517 109.0299,2.86548 110.09891,3.263428 110.09891,40.985371 0,30.923222 -7.28516,39.906492 -35.24013,43.454229 -29.06687,3.68892 -37.46391,6.17185 -37.46391,11.07783 0,2.19365 11.40965,3.98851 25.35478,3.98851 37.65833,0 38.79584,2.06051 38.79584,70.27428 0,31.25825 -1.20855,65.65073 -2.68573,76.42778 -2.59506,18.93304 -11.18137,28.79145 -16.94872,19.45971 z M 102.84208,259.48743 c -5.809371,-2.9396 -13.184926,-10.12819 -16.390123,-15.97462 -8.396565,-15.3158 2.912296,-41.49358 19.494463,-45.12587 20.60401,-4.51326 22.42665,-5.69589 22.42665,-14.55198 0,-6.8018 -2.78507,-8.68766 -12.83013,-8.68766 -16.253558,0 -29.936969,-13.44405 -29.936969,-29.41328 0,-14.42513 5.553783,-19.82093 24.591079,-23.89154 9.82484,-2.1008 13.8993,-5.37975 13.8993,-11.1856 0,-6.50902 -3.05408,-8.21363 -14.71608,-8.21363 -16.019022,0 -24.105594,-7.285117 -14.699611,-13.242787 3.241585,-2.05321 12.812471,-3.76252 21.268621,-3.79858 19.92576,-0.0847 30.60952,11.25947 28.76224,30.540617 -1.22046,12.73879 -3.23695,14.76612 -18.47681,18.57607 -30.922304,7.73057 -36.377959,19.24518 -9.11843,19.24518 22.57064,0 29.1179,7.65172 27.56665,32.21701 l -1.34139,21.24186 -20.81946,5.13821 c -11.45071,2.826 -21.79219,6.1109 -22.981063,7.29978 -4.590664,4.59067 3.475793,15.68791 14.430943,19.85304 16.19851,6.15868 88.0503,3.43548 90.32313,-3.42324 1.28741,-3.88501 -5.33813,-5.34589 -24.24513,-5.34589 -19.38669,0 -26.01664,-1.51819 -26.01664,-5.9575 0,-8.34429 9.63998,-11.14934 38.3165,-11.14934 28.67934,0 35.63045,5.45011 33.61018,26.35253 -0.96086,9.94142 -4.50427,15.66293 -12.05276,19.46151 -14.27136,7.1817 -96.89032,7.20826 -111.06516,0.0342 z m 57.07409,-60.66299 c -4.41463,-11.5043 3.01548,-16.23905 21.50632,-13.70463 20.26375,2.77747 30.64103,-1.74032 26.8271,-11.67922 -1.8078,-4.7111 -8.00515,-6.84671 -19.86838,-6.84671 -19.48707,0 -22.72529,-6.68 -6.27087,-12.93598 25.01034,-9.50892 44.62703,0.19588 44.62703,22.07791 0,17.75258 -8.58709,23.84253 -37.57444,26.6478 -21.20481,2.05209 -27.38199,1.30033 -29.24676,-3.55917 z" />
8
5
  </svg>
9
6
  ERB
10
7