kaze 0.7.0 → 0.9.0

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 (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