kaze 0.8.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kaze/commands/install_command.rb +3 -1
  3. data/lib/kaze/commands/installs_hotwire_stack.rb +2 -2
  4. data/lib/kaze/commands/installs_inertia_stacks.rb +7 -7
  5. data/lib/kaze/version.rb +1 -1
  6. data/stubs/default/app/mailers/application_mailer.rb +1 -1
  7. data/stubs/default/app/mailers/user_mailer.rb +16 -1
  8. data/stubs/default/app/models/concerns/can_reset_password.rb +1 -1
  9. data/stubs/default/app/models/concerns/must_verify_email.rb +15 -0
  10. data/stubs/default/app/models/user.rb +1 -0
  11. data/stubs/default/app/views/user_mailer/reset_password.html.erb +2 -2
  12. data/stubs/default/app/views/user_mailer/verify_email.html.erb +33 -0
  13. data/stubs/default/config/routes.rb +4 -0
  14. data/stubs/default/db/migrate/20240101000000_create_users.rb +1 -0
  15. data/stubs/default/test/factories/users.rb +5 -0
  16. data/stubs/default/test/integration/auth/authentication_test.rb +4 -6
  17. data/stubs/default/test/integration/auth/email_verification_test.rb +40 -0
  18. data/stubs/default/test/integration/auth/password_reset_test.rb +3 -3
  19. data/stubs/default/test/integration/password_update_test.rb +2 -2
  20. data/stubs/default/test/integration/profile_test.rb +4 -4
  21. data/stubs/hotwire/app/components/modal_component.rb +5 -5
  22. data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +3 -2
  23. data/stubs/hotwire/app/controllers/auth/email_verification_notification_controller.rb +21 -0
  24. data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +2 -2
  25. data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +1 -1
  26. data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +6 -2
  27. data/stubs/hotwire/app/controllers/auth/verified_email_controller.rb +23 -0
  28. data/stubs/hotwire/app/controllers/concerns/authenticate.rb +10 -0
  29. data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +17 -0
  30. data/stubs/hotwire/app/controllers/password_controller.rb +1 -1
  31. data/stubs/hotwire/app/controllers/profile_controller.rb +2 -2
  32. data/stubs/hotwire/app/views/auth/verify_email.html.erb +23 -0
  33. data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +1 -1
  34. data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +3 -3
  35. data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb +21 -0
  36. data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +2 -3
  37. data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +3 -3
  38. data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +6 -2
  39. data/stubs/inertia-common/app/controllers/auth/verified_email_controller.rb +23 -0
  40. data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +10 -0
  41. data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +17 -0
  42. data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
  43. data/stubs/inertia-common/app/controllers/profile_controller.rb +2 -2
  44. data/stubs/inertia-common/test/integration/password_update_test.rb +2 -2
  45. data/stubs/inertia-common/test/integration/profile_test.rb +4 -4
  46. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +1 -4
  47. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +3 -5
  48. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +4 -25
  49. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +1 -4
  50. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +1 -4
  51. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +3 -5
  52. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +3 -5
  53. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +3 -5
  54. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +1 -7
  55. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +14 -52
  56. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +3 -6
  57. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +9 -23
  58. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +1 -4
  59. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +1 -4
  60. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/VerifyEmail.tsx +47 -0
  61. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +2 -8
  62. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +1 -5
  63. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +7 -19
  64. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +7 -15
  65. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +7 -16
  66. data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +1 -2
  67. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +1 -5
  68. data/stubs/inertia-react-ts/config/tailwind.config.js +1 -6
  69. data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +3 -1
  70. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +1 -4
  71. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +3 -13
  72. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +10 -45
  73. data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +2 -6
  74. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +4 -11
  75. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +2 -10
  76. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +1 -5
  77. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +1 -4
  78. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/VerifyEmail.vue +50 -0
  79. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +3 -11
  80. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +2 -10
  81. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +5 -9
  82. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +2 -9
  83. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +3 -13
  84. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +2 -5
  85. data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -6
  86. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed80aded8509882926dcf8c40613b78779b50a01bf6a00edd3b130769e9020f5
4
- data.tar.gz: fedfaa322d578f194d46923d05c33dec0a0638b47653eb77b6a2898c9e1be49b
3
+ metadata.gz: 53f603a627b691ff3fee85613cd44327e85799c85c3b75557ee10c9757dba9e7
4
+ data.tar.gz: ca7677bad4b8446b1288a1d3a7d4db40edb6a77f851140a37b86e3607223c63d
5
5
  SHA512:
6
- metadata.gz: d94bc100f51b08ce70e149c898f7c36422980a0225e37e289146394bb2e216b37bed34be1a4a21c6ebdc091105abfebbe372dfcccf05d23886b348615a151673
7
- data.tar.gz: b6cbabd31838a806daeaa3cd719fc5e725242ae56783eb57a1028438e388a7b7f009edb04bb70a642383547c567bc46e7496f69053296d9834a661b0fbbd5e0e
6
+ metadata.gz: 979b1c79b3e9c447a4cf5adf3dfb1661b166f603114ce2e107c61719180c8a8102d2f5614de513ecef3b5f8847664f4618ea8e5bc6689f624d2646b76596ae3e
7
+ data.tar.gz: 4cb564c4b6ffd71e46adae28a2aeb903774bf7bd30db4c5375a0e649a91f77b4fd24ef8ddd79856513ef80d5b5f7b8c9e0f8aed8752cd9c637a65c589289a95b
@@ -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.8.0'
2
+ VERSION = '0.9.0'
3
3
  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
@@ -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,6 +3,7 @@ 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
7
8
  table.string :remember_token, null: true
8
9
  table.timestamps
@@ -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'
@@ -3,11 +3,11 @@ class ModalComponent < ViewComponent::Base
3
3
  @name = attributes[:name]
4
4
  @show = attributes[:show] || false
5
5
  @max_width = {
6
- :sm => 'sm:max-w-sm',
7
- :md => 'sm:max-w-md',
8
- :lg => 'sm:max-w-lg',
9
- :xl => 'sm:max-w-xl',
10
- '2xl' => 'sm:max-w-2xl'
6
+ sm: 'sm:max-w-sm',
7
+ md: 'sm:max-w-md',
8
+ lg: 'sm:max-w-lg',
9
+ xl: 'sm:max-w-xl',
10
+ '2xl': 'sm:max-w-2xl'
11
11
  }[attributes[:max_width] || '2xl']
12
12
  @attributes = attributes.without(:name, :show, :max_width)
13
13
  end
@@ -2,7 +2,8 @@ class Auth::AuthenticatedSessionController < ApplicationController
2
2
  include RedirectIfAuthenticated
3
3
 
4
4
  skip_authenticate only: %i[new create]
5
- skip_redirect_if_authenticated only: %i[destroy]
5
+ skip_redirect_if_authenticated only: :destroy
6
+ skip_ensure_email_is_verified only: :destroy
6
7
 
7
8
  layout 'guest'
8
9
 
@@ -13,7 +14,7 @@ class Auth::AuthenticatedSessionController < ApplicationController
13
14
  end
14
15
 
15
16
  def create
16
- @form = Auth::LoginForm.new params.permit(:email, :password, :remember)
17
+ @form = Auth::LoginForm.new(params.permit(:email, :password, :remember))
17
18
 
18
19
  @form.authenticate
19
20
 
@@ -0,0 +1,21 @@
1
+ class Auth::EmailVerificationNotificationController < ApplicationController
2
+ skip_ensure_email_is_verified
3
+
4
+ before_action { redirect_to dashboard_path unless User.include?(MustVerifyEmail) }
5
+
6
+ layout 'guest'
7
+
8
+ def new
9
+ return redirect_to dashboard_path if Current.auth.user.has_verified_email?
10
+
11
+ render 'auth/verify_email'
12
+ end
13
+
14
+ def create
15
+ return redirect_to dashboard_path if Current.auth.user.has_verified_email?
16
+
17
+ Current.auth.user.send_email_verification_notification
18
+
19
+ redirect_back_or_to verification_notice_path, flash: { status: 'verification-link-sent' }
20
+ end
21
+ end
@@ -6,13 +6,13 @@ class Auth::NewPasswordController < ApplicationController
6
6
  layout 'guest'
7
7
 
8
8
  def new
9
- @form = Auth::NewPasswordForm.new params.permit(:token)
9
+ @form = Auth::NewPasswordForm.new(params.permit(:token))
10
10
 
11
11
  render 'auth/reset_password'
12
12
  end
13
13
 
14
14
  def create
15
- @form = Auth::NewPasswordForm.new params.permit(:token, :password, :password_confirmation)
15
+ @form = Auth::NewPasswordForm.new(params.permit(:token, :password, :password_confirmation))
16
16
 
17
17
  return redirect_to login_path, flash: { status: 'Your password has been reset.' } if @form.reset?
18
18
 
@@ -12,7 +12,7 @@ class Auth::PasswordResetLinkController < ApplicationController
12
12
  end
13
13
 
14
14
  def create
15
- @form = Auth::SendPasswordResetLinkForm.new params.permit(:email)
15
+ @form = Auth::SendPasswordResetLinkForm.new(params.permit(:email))
16
16
 
17
17
  return redirect_back_or_to password_request_path, flash: { status: 'We have emailed your password reset link.' } if @form.send_reset_link?
18
18
 
@@ -12,13 +12,17 @@ class Auth::RegisteredUserController < ApplicationController
12
12
  end
13
13
 
14
14
  def create
15
- @form = Auth::RegisterForm.new params.permit(:name, :email, :password, :password_confirmation)
15
+ @form = Auth::RegisterForm.new(params.permit(:name, :email, :password, :password_confirmation))
16
16
 
17
17
  return render 'auth/register', status: :unprocessable_entity if @form.invalid?
18
18
 
19
19
  user = User.create(name: @form.name, email: @form.email, password: @form.password)
20
20
 
21
- Current.auth.login user
21
+ if User.include?(MustVerifyEmail) && !user.has_verified_email?
22
+ user.send_email_verification_notification
23
+ end
24
+
25
+ Current.auth.login(user)
22
26
 
23
27
  redirect_to dashboard_path
24
28
  end
@@ -0,0 +1,23 @@
1
+ class Auth::VerifiedEmailController < ApplicationController
2
+ include ValidateSignature
3
+
4
+ skip_ensure_email_is_verified
5
+
6
+ before_action { redirect_to dashboard_path unless User.include?(MustVerifyEmail) }
7
+
8
+ def create
9
+ return redirect_to dashboard_path(verified: '1') if Current.auth.user.has_verified_email?
10
+
11
+ Current.auth.user.mark_email_as_verified if email_verification_request_is_authorized?
12
+
13
+ redirect_to dashboard_path(verified: '1')
14
+ end
15
+
16
+ private
17
+
18
+ def email_verification_request_is_authorized?
19
+ return false if !ActiveSupport::SecurityUtils.secure_compare Current.auth.user.id.to_s, params[:id]
20
+
21
+ ActiveSupport::SecurityUtils.secure_compare Digest::SHA1.hexdigest(Current.auth.user.email), params[:hash]
22
+ end
23
+ end
@@ -3,11 +3,17 @@ module Authenticate
3
3
 
4
4
  included do
5
5
  before_action :authenticate!
6
+ before_action :ensure_email_is_verified! if User.include?(MustVerifyEmail)
6
7
  end
7
8
 
8
9
  class_methods do
9
10
  def skip_authenticate(**options)
10
11
  skip_before_action :authenticate!, **options
12
+ skip_ensure_email_is_verified(**options)
13
+ end
14
+
15
+ def skip_ensure_email_is_verified(**options)
16
+ skip_before_action :ensure_email_is_verified!, **options if User.include?(MustVerifyEmail)
11
17
  end
12
18
  end
13
19
 
@@ -16,4 +22,8 @@ module Authenticate
16
22
  def authenticate!
17
23
  redirect_to login_path unless Current.auth.check?
18
24
  end
25
+
26
+ def ensure_email_is_verified!
27
+ redirect_to verification_notice_path unless Current.auth.user && Current.auth.user.has_verified_email?
28
+ end
19
29
  end
@@ -0,0 +1,17 @@
1
+ module ValidateSignature
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action do
6
+ render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless has_valid_signature?
7
+ end
8
+ end
9
+
10
+ private
11
+
12
+ def has_valid_signature?
13
+ request.original_url.split('?')[0] == ActiveSupport::MessageVerifier.new(ENV.fetch('RAILS_MASTER_KEY', '')).verify(params[:signature])
14
+ rescue
15
+ false
16
+ end
17
+ end
@@ -1,6 +1,6 @@
1
1
  class PasswordController < ApplicationController
2
2
  def update
3
- @update_password_form = UpdatePasswordForm.new params.permit(:current_password, :password, :password_confirmation)
3
+ @update_password_form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
4
4
 
5
5
  return render partial: 'profile/partials/update_password_form', status: :unprocessable_entity if @update_password_form.invalid?
6
6
 
@@ -8,7 +8,7 @@ class ProfileController < ApplicationController
8
8
  end
9
9
 
10
10
  def update
11
- @update_profile_information_form = UpdateProfileInformationForm.new params.permit(:name, :email)
11
+ @update_profile_information_form = UpdateProfileInformationForm.new(params.permit(:name, :email))
12
12
 
13
13
  return render partial: 'profile/partials/update_profile_information_form', status: :unprocessable_entity if @update_profile_information_form.invalid?
14
14
 
@@ -18,7 +18,7 @@ class ProfileController < ApplicationController
18
18
  end
19
19
 
20
20
  def destroy
21
- @delete_user_form = DeleteUserForm.new params.permit(:password)
21
+ @delete_user_form = DeleteUserForm.new(params.permit(:password))
22
22
 
23
23
  return render partial: 'profile/partials/delete_user_form', status: :unprocessable_entity if @delete_user_form.invalid?
24
24
 
@@ -0,0 +1,23 @@
1
+ <div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
2
+ Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.
3
+ </div>
4
+ <% if flash[:status] == 'verification-link-sent' %>
5
+ <div class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
6
+ A new verification link has been sent to the email address you provided during registration.
7
+ </div>
8
+ <% end %>
9
+ <div class="mt-4 flex items-center justify-between">
10
+ <form method="POST" action="<%= verification_send_path %>">
11
+ <div>
12
+ <%= render(PrimaryButtonComponent.new) do %>
13
+ Resend Verification Email
14
+ <% end %>
15
+ </div>
16
+ </form>
17
+ <form method="POST" action="<%= logout_path %>">
18
+ <input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
19
+ <button type="submit" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
20
+ Log Out
21
+ </button>
22
+ </form>
23
+ </div>
@@ -10,7 +10,7 @@
10
10
  <%= render(DangerButtonComponent.new({"x-data": "", "x-on:click.prevent": "$dispatch('open-modal', 'confirm-user-deletion')"})) do %>
11
11
  Delete Account
12
12
  <% end %>
13
- <%= render(ModalComponent.new({ :name => "confirm-user-deletion", :show => @delete_user_form.error_messages.has_key?(:password), :focusable => true })) do %>
13
+ <%= render(ModalComponent.new({ name: "confirm-user-deletion", show: @delete_user_form.error_messages.has_key?(:password), focusable: true })) do %>
14
14
  <turbo-frame id="delete_user_form">
15
15
  <%= form_with model: @delete_user_form, url: profile_destroy_path, method: "delete", class: "p-6" do %>
16
16
  <h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">