kaze 0.8.0 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/kaze/commands/install_command.rb +3 -1
- data/lib/kaze/commands/installs_hotwire_stack.rb +2 -2
- data/lib/kaze/commands/installs_inertia_stacks.rb +7 -7
- data/lib/kaze/version.rb +1 -1
- data/stubs/default/app/forms/update_profile_information_form.rb +8 -0
- data/stubs/default/app/mailers/application_mailer.rb +1 -1
- data/stubs/default/app/mailers/user_mailer.rb +16 -1
- data/stubs/default/app/models/concerns/can_reset_password.rb +1 -1
- data/stubs/default/app/models/concerns/must_verify_email.rb +15 -0
- data/stubs/default/app/models/user.rb +1 -0
- data/stubs/default/app/views/user_mailer/reset_password.html.erb +2 -2
- data/stubs/default/app/views/user_mailer/verify_email.html.erb +33 -0
- data/stubs/default/config/routes.rb +4 -0
- data/stubs/default/db/migrate/20240101000000_create_users.rb +1 -0
- data/stubs/default/test/factories/users.rb +5 -0
- data/stubs/default/test/integration/auth/authentication_test.rb +4 -6
- data/stubs/default/test/integration/auth/email_verification_test.rb +40 -0
- data/stubs/default/test/integration/auth/password_reset_test.rb +3 -3
- data/stubs/default/test/integration/password_update_test.rb +2 -2
- data/stubs/default/test/integration/profile_test.rb +16 -4
- data/stubs/hotwire/app/components/modal_component.rb +5 -5
- data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +3 -2
- data/stubs/hotwire/app/controllers/auth/email_verification_notification_controller.rb +21 -0
- data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +2 -2
- data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +1 -1
- data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +6 -2
- data/stubs/hotwire/app/controllers/auth/verified_email_controller.rb +23 -0
- data/stubs/hotwire/app/controllers/concerns/authenticate.rb +10 -0
- data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/hotwire/app/controllers/password_controller.rb +3 -1
- data/stubs/hotwire/app/controllers/profile_controller.rb +5 -3
- data/stubs/hotwire/app/views/auth/verify_email.html.erb +23 -0
- data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +1 -1
- data/stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb +18 -0
- data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +3 -3
- data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb +21 -0
- data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +2 -3
- data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +3 -3
- data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +6 -2
- data/stubs/inertia-common/app/controllers/auth/verified_email_controller.rb +23 -0
- data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +10 -0
- data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/inertia-common/app/controllers/password_controller.rb +3 -1
- data/stubs/inertia-common/app/controllers/profile_controller.rb +7 -4
- data/stubs/inertia-common/test/integration/password_update_test.rb +2 -2
- data/stubs/inertia-common/test/integration/profile_test.rb +16 -4
- data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +4 -25
- data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +3 -5
- data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +1 -7
- data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +14 -52
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +4 -7
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +10 -24
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +2 -5
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +2 -5
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/VerifyEmail.tsx +47 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +2 -8
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +10 -10
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +10 -20
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +10 -18
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +35 -12
- data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +1 -2
- data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +1 -5
- data/stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts +3 -3
- data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +4 -4
- data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +8 -8
- data/stubs/inertia-react-ts/config/tailwind.config.js +1 -6
- data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +3 -1
- data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +1 -4
- data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +3 -13
- data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +10 -45
- data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +2 -6
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +4 -11
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +2 -10
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +1 -5
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +2 -22
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/VerifyEmail.vue +50 -0
- data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +3 -11
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +2 -10
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +5 -9
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +2 -9
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +26 -15
- data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +2 -5
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts +27 -23
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts +3 -3
- data/stubs/inertia-vue-ts/app/javascript/types/global.d.ts +7 -7
- data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +8 -8
- data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -6
- metadata +14 -2
@@ -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({ :
|
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">
|
@@ -8,6 +8,9 @@
|
|
8
8
|
Update your account's profile information and email address.
|
9
9
|
</p>
|
10
10
|
</header>
|
11
|
+
<form id="send-verification" method="post" action="<%= verification_send_path %>">
|
12
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
13
|
+
</form>
|
11
14
|
<%= form_with model: @update_profile_information_form, url: profile_update_path, method: "patch", class: "mt-6 space-y-6" do %>
|
12
15
|
<!-- Name -->
|
13
16
|
<div>
|
@@ -20,6 +23,21 @@
|
|
20
23
|
<%= render(InputLabelComponent.new({ for: "email", value: "Email" })) %>
|
21
24
|
<%= render(TextInputComponent.new({ id: "email", class: "block mt-1 w-full", type: "email", name: "email", value: @update_profile_information_form.email, required: true, autocomplete: "username" })) %>
|
22
25
|
<%= render(InputErrorComponent.new({ class: "mt-2", message: @update_profile_information_form.error_messages[:email] })) %>
|
26
|
+
<% if User.include?(MustVerifyEmail) && !Current.auth.user.has_verified_email? %>
|
27
|
+
<div>
|
28
|
+
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
29
|
+
Your email address is unverified.
|
30
|
+
<button form="send-verification" 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">
|
31
|
+
Click here to re-send the verification email.
|
32
|
+
</button>
|
33
|
+
</p>
|
34
|
+
<% if flash[:status] == 'verification-link-sent' %>
|
35
|
+
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
36
|
+
A new verification link has been sent to your email address.
|
37
|
+
</p>
|
38
|
+
<% end %>
|
39
|
+
</div>
|
40
|
+
<% end %>
|
23
41
|
</div>
|
24
42
|
<div class="flex items-center gap-4">
|
25
43
|
<%= render(PrimaryButtonComponent.new) do %>
|
@@ -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:
|
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
|
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 verification_notice_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
|
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
|
13
|
+
form = Auth::SendPasswordResetLinkForm.new(params.permit(:email))
|
14
14
|
|
15
|
-
return redirect_back_or_to password_request_path,
|
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,
|
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
|
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
|
-
|
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
|
@@ -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,8 @@
|
|
1
1
|
class PasswordController < ApplicationController
|
2
|
+
skip_ensure_email_is_verified
|
3
|
+
|
2
4
|
def update
|
3
|
-
form = UpdatePasswordForm.new
|
5
|
+
form = UpdatePasswordForm.new(params.permit(:current_password, :password, :password_confirmation))
|
4
6
|
|
5
7
|
return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
|
6
8
|
|
@@ -1,22 +1,25 @@
|
|
1
1
|
class ProfileController < ApplicationController
|
2
|
+
skip_ensure_email_is_verified
|
3
|
+
|
2
4
|
def edit
|
3
5
|
render inertia: 'Profile/Edit', props: {
|
4
|
-
|
6
|
+
mustVerifyEmail: User.include?(MustVerifyEmail),
|
7
|
+
status: flash[:status]
|
5
8
|
}
|
6
9
|
end
|
7
10
|
|
8
11
|
def update
|
9
|
-
form = UpdateProfileInformationForm.new
|
12
|
+
form = UpdateProfileInformationForm.new(params.permit(:name, :email))
|
10
13
|
|
11
14
|
return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
|
12
15
|
|
13
|
-
|
16
|
+
form.update
|
14
17
|
|
15
18
|
redirect_to profile_edit_path
|
16
19
|
end
|
17
20
|
|
18
21
|
def destroy
|
19
|
-
form = DeleteUserForm.new
|
22
|
+
form = DeleteUserForm.new(params.permit(:password))
|
20
23
|
|
21
24
|
return redirect_back_or_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
|
22
25
|
|
@@ -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
|
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
|
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
|
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
|
13
|
+
user = FactoryBot.create(:user)
|
14
14
|
|
15
15
|
acting_as(user).patch profile_edit_path, params: {
|
16
16
|
name: 'Test User',
|
@@ -25,8 +25,20 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
25
25
|
assert_equal 'test@example.com', user.email
|
26
26
|
end
|
27
27
|
|
28
|
+
test 'email verification status is unchanged when the email address is unchanged' do
|
29
|
+
user = FactoryBot.create(:user)
|
30
|
+
|
31
|
+
acting_as(user).patch profile_edit_path, params: {
|
32
|
+
name: 'Test User',
|
33
|
+
email: user.email
|
34
|
+
}
|
35
|
+
|
36
|
+
assert_redirected_to profile_edit_path
|
37
|
+
assert_not user.reload.email_verified_at.blank?
|
38
|
+
end
|
39
|
+
|
28
40
|
test 'user can delete their account' do
|
29
|
-
user = FactoryBot.create
|
41
|
+
user = FactoryBot.create(:user)
|
30
42
|
|
31
43
|
acting_as(user).delete profile_destroy_path, params: {
|
32
44
|
password: 'password'
|
@@ -39,7 +51,7 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
39
51
|
end
|
40
52
|
|
41
53
|
test 'correct password must be provided to delete account' do
|
42
|
-
user = FactoryBot.create
|
54
|
+
user = FactoryBot.create(:user)
|
43
55
|
|
44
56
|
acting_as(user).delete profile_destroy_path, params: {
|
45
57
|
password: 'wrong-password'
|
@@ -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
|
-
|
14
|
-
|
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
|
@@ -7,10 +7,7 @@ export default function InputLabel({
|
|
7
7
|
...props
|
8
8
|
}: LabelHTMLAttributes<HTMLLabelElement> & { value?: string }) {
|
9
9
|
return (
|
10
|
-
<label
|
11
|
-
{...props}
|
12
|
-
className={`block font-medium text-sm text-gray-700 dark:text-gray-300 ${className}`}
|
13
|
-
>
|
10
|
+
<label {...props} className={`block font-medium text-sm text-gray-700 dark:text-gray-300 ${className}`}>
|
14
11
|
{value ? value : children}
|
15
12
|
</label>
|
16
13
|
)
|
@@ -9,13 +9,11 @@ export default function NavLink({
|
|
9
9
|
return (
|
10
10
|
<Link
|
11
11
|
{...props}
|
12
|
-
className={
|
13
|
-
|
14
|
-
active
|
12
|
+
className={`inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out focus:outline-none ${
|
13
|
+
active
|
15
14
|
? 'border-indigo-400 dark:border-indigo-600 text-gray-900 dark:text-gray-100 focus:border-indigo-700 '
|
16
15
|
: 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 '
|
17
|
-
|
18
|
-
}
|
16
|
+
} ${className}`}
|
19
17
|
>
|
20
18
|
{children}
|
21
19
|
</Link>
|
@@ -9,11 +9,9 @@ export default function PrimaryButton({
|
|
9
9
|
return (
|
10
10
|
<button
|
11
11
|
{...props}
|
12
|
-
className={
|
13
|
-
|
14
|
-
|
15
|
-
} ${className}`
|
16
|
-
}
|
12
|
+
className={`inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-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}
|
@@ -11,11 +11,9 @@ export default function SecondaryButton({
|
|
11
11
|
<button
|
12
12
|
{...props}
|
13
13
|
type={type}
|
14
|
-
className={
|
15
|
-
|
16
|
-
|
17
|
-
} ${className}`
|
18
|
-
}
|
14
|
+
className={`inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150 ${
|
15
|
+
disabled && 'opacity-25'
|
16
|
+
} ${className}`}
|
19
17
|
disabled={disabled}
|
20
18
|
>
|
21
19
|
{children}
|
@@ -1,10 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
forwardRef,
|
3
|
-
useEffect,
|
4
|
-
useImperativeHandle,
|
5
|
-
useRef,
|
6
|
-
InputHTMLAttributes,
|
7
|
-
} from 'react'
|
1
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef, InputHTMLAttributes } from 'react'
|
8
2
|
|
9
3
|
export default forwardRef(function TextInput(
|
10
4
|
{
|
@@ -12,8 +12,7 @@ export default function Authenticated({
|
|
12
12
|
header,
|
13
13
|
children,
|
14
14
|
}: PropsWithChildren<{ user: User; header?: ReactNode }>) {
|
15
|
-
const [showingNavigationDropdown, setShowingNavigationDropdown] =
|
16
|
-
useState(false)
|
15
|
+
const [showingNavigationDropdown, setShowingNavigationDropdown] = useState(false)
|
17
16
|
|
18
17
|
const { pathname = '' } = typeof window !== 'undefined' ? window.location : {}
|
19
18
|
|
@@ -30,10 +29,7 @@ export default function Authenticated({
|
|
30
29
|
</div>
|
31
30
|
|
32
31
|
<div className="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
33
|
-
<NavLink
|
34
|
-
href={dashboard_path()}
|
35
|
-
active={pathname.match(/dashboard/) != null}
|
36
|
-
>
|
32
|
+
<NavLink href={dashboard_path()} active={pathname.match(/dashboard/) != null}>
|
37
33
|
Dashboard
|
38
34
|
</NavLink>
|
39
35
|
</div>
|
@@ -67,14 +63,8 @@ export default function Authenticated({
|
|
67
63
|
</Dropdown.Trigger>
|
68
64
|
|
69
65
|
<Dropdown.Content>
|
70
|
-
<Dropdown.Link href={profile_edit_path()}>
|
71
|
-
|
72
|
-
</Dropdown.Link>
|
73
|
-
<Dropdown.Link
|
74
|
-
href={logout_path()}
|
75
|
-
method="post"
|
76
|
-
as="button"
|
77
|
-
>
|
66
|
+
<Dropdown.Link href={profile_edit_path()}>Profile</Dropdown.Link>
|
67
|
+
<Dropdown.Link href={logout_path()} method="post" as="button">
|
78
68
|
Log Out
|
79
69
|
</Dropdown.Link>
|
80
70
|
</Dropdown.Content>
|
@@ -84,32 +74,19 @@ export default function Authenticated({
|
|
84
74
|
|
85
75
|
<div className="-me-2 flex items-center sm:hidden">
|
86
76
|
<button
|
87
|
-
onClick={() =>
|
88
|
-
setShowingNavigationDropdown(
|
89
|
-
(previousState) => !previousState,
|
90
|
-
)
|
91
|
-
}
|
77
|
+
onClick={() => setShowingNavigationDropdown((previousState) => !previousState)}
|
92
78
|
className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out"
|
93
79
|
>
|
94
|
-
<svg
|
95
|
-
className="h-6 w-6"
|
96
|
-
stroke="currentColor"
|
97
|
-
fill="none"
|
98
|
-
viewBox="0 0 24 24"
|
99
|
-
>
|
80
|
+
<svg className="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
100
81
|
<path
|
101
|
-
className={
|
102
|
-
!showingNavigationDropdown ? 'inline-flex' : 'hidden'
|
103
|
-
}
|
82
|
+
className={!showingNavigationDropdown ? 'inline-flex' : 'hidden'}
|
104
83
|
strokeLinecap="round"
|
105
84
|
strokeLinejoin="round"
|
106
85
|
strokeWidth="2"
|
107
86
|
d="M4 6h16M4 12h16M4 18h16"
|
108
87
|
/>
|
109
88
|
<path
|
110
|
-
className={
|
111
|
-
showingNavigationDropdown ? 'inline-flex' : 'hidden'
|
112
|
-
}
|
89
|
+
className={showingNavigationDropdown ? 'inline-flex' : 'hidden'}
|
113
90
|
strokeLinecap="round"
|
114
91
|
strokeLinejoin="round"
|
115
92
|
strokeWidth="2"
|
@@ -121,34 +98,21 @@ export default function Authenticated({
|
|
121
98
|
</div>
|
122
99
|
</div>
|
123
100
|
|
124
|
-
<div
|
125
|
-
className={
|
126
|
-
(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'
|
127
|
-
}
|
128
|
-
>
|
101
|
+
<div className={(showingNavigationDropdown ? 'block' : 'hidden') + ' sm:hidden'}>
|
129
102
|
<div className="pt-2 pb-3 space-y-1">
|
130
|
-
<ResponsiveNavLink
|
131
|
-
href={dashboard_path()}
|
132
|
-
active={pathname.match(/dashboard/) != null}
|
133
|
-
>
|
103
|
+
<ResponsiveNavLink href={dashboard_path()} active={pathname.match(/dashboard/) != null}>
|
134
104
|
Dashboard
|
135
105
|
</ResponsiveNavLink>
|
136
106
|
</div>
|
137
107
|
|
138
108
|
<div className="pt-4 pb-1 border-t border-gray-200">
|
139
109
|
<div className="px-4">
|
140
|
-
<div className="font-medium text-base text-gray-800">
|
141
|
-
|
142
|
-
</div>
|
143
|
-
<div className="font-medium text-sm text-gray-500">
|
144
|
-
{user.email}
|
145
|
-
</div>
|
110
|
+
<div className="font-medium text-base text-gray-800">{user.name}</div>
|
111
|
+
<div className="font-medium text-sm text-gray-500">{user.email}</div>
|
146
112
|
</div>
|
147
113
|
|
148
114
|
<div className="mt-3 space-y-1">
|
149
|
-
<ResponsiveNavLink href={profile_edit_path()}>
|
150
|
-
Profile
|
151
|
-
</ResponsiveNavLink>
|
115
|
+
<ResponsiveNavLink href={profile_edit_path()}>Profile</ResponsiveNavLink>
|
152
116
|
<ResponsiveNavLink method="post" href={logout_path()} as="button">
|
153
117
|
Log Out
|
154
118
|
</ResponsiveNavLink>
|
@@ -159,9 +123,7 @@ export default function Authenticated({
|
|
159
123
|
|
160
124
|
{header && (
|
161
125
|
<header className="bg-white shadow">
|
162
|
-
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
|
163
|
-
{header}
|
164
|
-
</div>
|
126
|
+
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">{header}</div>
|
165
127
|
</header>
|
166
128
|
)}
|
167
129
|
|