kaze 0.7.0 → 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- 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/auth/login_form.rb +2 -2
- data/stubs/default/app/forms/auth/new_password_form.rb +2 -2
- data/stubs/default/app/forms/auth/send_password_reset_link_form.rb +2 -4
- 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/session_guard.rb +157 -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 +2 -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 +4 -4
- data/stubs/default/test/test_helper.rb +1 -1
- data/stubs/hotwire/app/components/application_logo_component.rb +2 -5
- 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/set_current_auth.rb +1 -1
- data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/hotwire/app/controllers/password_controller.rb +1 -1
- data/stubs/hotwire/app/controllers/profile_controller.rb +2 -2
- data/stubs/hotwire/app/views/auth/verify_email.html.erb +23 -0
- data/stubs/hotwire/app/views/layouts/_navigation.html.erb +1 -1
- data/stubs/hotwire/app/views/layouts/guest.html.erb +1 -1
- data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +1 -1
- 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/set_current_auth.rb +1 -1
- data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +17 -0
- data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
- data/stubs/inertia-common/app/controllers/profile_controller.rb +2 -2
- data/stubs/inertia-common/test/integration/password_update_test.rb +2 -2
- data/stubs/inertia-common/test/integration/profile_test.rb +4 -4
- data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +2 -9
- 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 +15 -53
- data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +1 -1
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +3 -6
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +9 -23
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +1 -4
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +1 -4
- 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 +1 -5
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +7 -19
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +7 -15
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +7 -16
- 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/config/tailwind.config.js +1 -6
- data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +4 -9
- 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 +3 -7
- 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 +1 -4
- 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 +3 -13
- data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +2 -5
- data/stubs/inertia-vue-ts/config/tailwind.config.js +1 -6
- metadata +15 -4
- data/MIT-LICENSE +0 -20
- 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
|
-
:
|
7
|
-
:
|
8
|
-
:
|
9
|
-
:
|
10
|
-
'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:
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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
|
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
|
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
|
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-
|
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-
|
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({ :
|
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:
|
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 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
|
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,6 @@
|
|
1
1
|
class PasswordController < ApplicationController
|
2
2
|
def update
|
3
|
-
form = UpdatePasswordForm.new
|
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
|
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
|
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
|
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',
|
@@ -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
|
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
|
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
|
-
<
|
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
|
-
|
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
|