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
@@ -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)
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
@@ -3,7 +3,7 @@ module SetCurrentAuth
3
3
 
4
4
  included do
5
5
  before_action do
6
- Current.auth = Auth.new('web', session)
6
+ Current.auth = SessionGuard.new(name: 'web', session: session, cookies: cookies)
7
7
  end
8
8
  end
9
9
  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>
@@ -6,7 +6,7 @@
6
6
  <!-- Logo -->
7
7
  <div class="shrink-0 flex items-center">
8
8
  <a href="/">
9
- <%= render(ApplicationLogoComponent.new({ class: "block h-9 w-auto fill-current text-gray-800 dark:text-gray-200" })) %>
9
+ <%= render(ApplicationLogoComponent.new({ class: "block h-9 w-auto fill-current text-red-800 dark:text-red-200" })) %>
10
10
  </a>
11
11
  </div>
12
12
  <!-- Navigation Links -->
@@ -16,7 +16,7 @@
16
16
  <div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
17
17
  <div>
18
18
  <a href="/">
19
- <%= render(ApplicationLogoComponent.new({ class: "w-20 h-20 fill-current text-gray-500" })) %>
19
+ <%= render(ApplicationLogoComponent.new({ class: "w-20 h-20 fill-current text-red-500" })) %>
20
20
  </a>
21
21
  </div>
22
22
  <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
@@ -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">
@@ -2,17 +2,17 @@ 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
  def new
8
9
  render inertia: 'Auth/Login', props: {
9
- canResetPassword: true,
10
10
  status: flash[:status]
11
11
  }
12
12
  end
13
13
 
14
14
  def create
15
- form = Auth::LoginForm.new params.permit(:email, :password)
15
+ form = Auth::LoginForm.new(params.permit(:email, :password, :remember))
16
16
 
17
17
  form.authenticate
18
18
 
@@ -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
+ def new
7
+ return redirect_to dashboard_path if Current.auth.user.has_verified_email?
8
+
9
+ render inertia: 'Auth/VerifyEmail', props: {
10
+ status: flash[:status]
11
+ }
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 dashboard_path, flash: { status: 'verification-link-sent' }
20
+ end
21
+ end
@@ -10,11 +10,10 @@ class Auth::NewPasswordController < ApplicationController
10
10
  end
11
11
 
12
12
  def create
13
- form = Auth::NewPasswordForm.new params.permit(:token, :password, :password_confirmation)
13
+ form = Auth::NewPasswordForm.new(params.permit(:token, :password, :password_confirmation))
14
14
 
15
15
  return redirect_to login_path, flash: { status: 'Your password has been reset.' } if form.reset?
16
16
 
17
- redirect_back_or_to password_reset_path(token: form.token),
18
- inertia: { errors: form.error_messages }
17
+ redirect_back_or_to password_reset_path(token: form.token), inertia: { errors: form.error_messages }
19
18
  end
20
19
  end
@@ -10,10 +10,10 @@ class Auth::PasswordResetLinkController < ApplicationController
10
10
  end
11
11
 
12
12
  def create
13
- form = Auth::SendPasswordResetLinkForm.new params.permit(:email)
13
+ form = Auth::SendPasswordResetLinkForm.new(params.permit(:email))
14
14
 
15
- return redirect_back_or_to password_request_path, inertia: { errors: form.error_messages } unless form.send_reset_link?
15
+ return redirect_back_or_to password_request_path, flash: { status: 'We have emailed your password reset link.' } if form.send_reset_link?
16
16
 
17
- redirect_back_or_to password_request_path, flash: { status: 'We have emailed your password reset link.' }
17
+ redirect_back_or_to password_request_path, inertia: { errors: form.error_messages }
18
18
  end
19
19
  end
@@ -8,13 +8,17 @@ class Auth::RegisteredUserController < ApplicationController
8
8
  end
9
9
 
10
10
  def create
11
- form = Auth::RegisterForm.new params.permit(:name, :email, :password, :password_confirmation)
11
+ form = Auth::RegisterForm.new(params.permit(:name, :email, :password, :password_confirmation))
12
12
 
13
13
  return redirect_to register_path, inertia: { errors: form.error_messages } if form.invalid?
14
14
 
15
15
  user = User.create(name: form.name, email: form.email, password: form.password)
16
16
 
17
- Current.auth.login user
17
+ if User.include?(MustVerifyEmail) && !user.has_verified_email?
18
+ user.send_email_verification_notification
19
+ end
20
+
21
+ Current.auth.login(user)
18
22
 
19
23
  redirect_to dashboard_path
20
24
  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
@@ -3,7 +3,7 @@ module SetCurrentAuth
3
3
 
4
4
  included do
5
5
  before_action do
6
- Current.auth = Auth.new('web', session)
6
+ Current.auth = SessionGuard.new(name: 'web', session: session, cookies: cookies)
7
7
  end
8
8
  end
9
9
  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
- form = UpdatePasswordForm.new params.permit(:current_password, :password, :password_confirmation)
3
+ form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
4
4
 
5
5
  return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
6
6
 
@@ -6,7 +6,7 @@ class ProfileController < ApplicationController
6
6
  end
7
7
 
8
8
  def update
9
- form = UpdateProfileInformationForm.new params.permit(:name, :email)
9
+ form = UpdateProfileInformationForm.new(params.permit(:name, :email))
10
10
 
11
11
  return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
12
12
 
@@ -16,7 +16,7 @@ class ProfileController < ApplicationController
16
16
  end
17
17
 
18
18
  def destroy
19
- form = DeleteUserForm.new params.permit(:password)
19
+ form = DeleteUserForm.new(params.permit(:password))
20
20
 
21
21
  return redirect_back_or_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
22
22
 
@@ -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'
@@ -2,15 +2,8 @@ import { SVGAttributes } from 'react'
2
2
 
3
3
  export default function ApplicationLogo(props: SVGAttributes<SVGElement>) {
4
4
  return (
5
- <svg {...props} viewBox="0 -6 32 32" xmlns="http://www.w3.org/2000/svg">
6
- <g fill="none" fillRule="evenodd">
7
- <path d="M0-6h32v32H0z" />
8
- <path
9
- fill="#c00"
10
- fillRule="nonzero"
11
- 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"
12
- />
13
- </g>
5
+ <svg {...props} viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
6
+ <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" />
14
7
  </svg>
15
8
  )
16
9
  }
@@ -1,9 +1,6 @@
1
1
  import { InputHTMLAttributes } from 'react'
2
2
 
3
- export default function Checkbox({
4
- className = '',
5
- ...props
6
- }: InputHTMLAttributes<HTMLInputElement>) {
3
+ export default function Checkbox({ className = '', ...props }: InputHTMLAttributes<HTMLInputElement>) {
7
4
  return (
8
5
  <input
9
6
  {...props}
@@ -9,11 +9,9 @@ export default function DangerButton({
9
9
  return (
10
10
  <button
11
11
  {...props}
12
- className={
13
- `inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 ${
14
- disabled && 'opacity-25'
15
- } ${className}`
16
- }
12
+ className={`inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150 ${
13
+ disabled && 'opacity-25'
14
+ } ${className}`}
17
15
  disabled={disabled}
18
16
  >
19
17
  {children}
@@ -1,12 +1,4 @@
1
- import {
2
- useState,
3
- createContext,
4
- useContext,
5
- Fragment,
6
- PropsWithChildren,
7
- Dispatch,
8
- SetStateAction,
9
- } from 'react'
1
+ import { useState, createContext, useContext, Fragment, PropsWithChildren, Dispatch, SetStateAction } from 'react'
10
2
  import { Link, InertiaLinkProps } from '@inertiajs/react'
11
3
  import { Transition } from '@headlessui/react'
12
4
 
@@ -41,12 +33,7 @@ const Trigger = ({ children }: PropsWithChildren) => {
41
33
  <>
42
34
  <div onClick={toggleOpen}>{children}</div>
43
35
 
44
- {open && (
45
- <div
46
- className="fixed inset-0 z-40"
47
- onClick={() => setOpen(false)}
48
- ></div>
49
- )}
36
+ {open && <div className="fixed inset-0 z-40" onClick={() => setOpen(false)}></div>}
50
37
  </>
51
38
  )
52
39
  }
@@ -93,22 +80,14 @@ const Content = ({
93
80
  className={`absolute z-50 mt-2 rounded-md shadow-lg ${alignmentClasses} ${widthClasses}`}
94
81
  onClick={() => setOpen(false)}
95
82
  >
96
- <div
97
- className={`rounded-md ring-1 ring-black ring-opacity-5 ${contentClasses}`}
98
- >
99
- {children}
100
- </div>
83
+ <div className={`rounded-md ring-1 ring-black ring-opacity-5 ${contentClasses}`}>{children}</div>
101
84
  </div>
102
85
  </Transition>
103
86
  </>
104
87
  )
105
88
  }
106
89
 
107
- const DropdownLink = ({
108
- className = '',
109
- children,
110
- ...props
111
- }: InertiaLinkProps) => {
90
+ const DropdownLink = ({ className = '', children, ...props }: InertiaLinkProps) => {
112
91
  return (
113
92
  <Link
114
93
  {...props}
@@ -6,10 +6,7 @@ export default function InputError({
6
6
  ...props
7
7
  }: HTMLAttributes<HTMLParagraphElement> & { message?: string }) {
8
8
  return message ? (
9
- <p
10
- {...props}
11
- className={`text-sm text-red-600 dark:text-red-400 ${className}`}
12
- >
9
+ <p {...props} className={`text-sm text-red-600 dark:text-red-400 ${className}`}>
13
10
  {message}
14
11
  </p>
15
12
  ) : null