kaze 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/kaze/commands/install_command.rb +25 -13
- data/lib/kaze/commands/installs_hotwire_stack.rb +11 -8
- data/lib/kaze/commands/installs_inertia_stacks.rb +34 -24
- data/lib/kaze/commands/version_command.rb +6 -0
- data/lib/kaze/version.rb +1 -1
- data/lib/kaze.rb +1 -3
- data/stubs/default/app/forms/auth/login_form.rb +2 -8
- data/stubs/default/app/forms/auth/new_password_form.rb +1 -1
- data/stubs/default/app/forms/update_profile_information_form.rb +1 -1
- data/stubs/default/app/mailers/application_mailer.rb +4 -4
- data/stubs/default/app/mailers/user_mailer.rb +1 -1
- data/stubs/default/app/models/auth.rb +57 -0
- data/stubs/default/app/models/current.rb +1 -1
- data/stubs/default/app/validators/current_password_validator.rb +1 -1
- data/stubs/default/app/validators/email_validator.rb +1 -1
- data/stubs/default/app/validators/lowercase_validator.rb +2 -2
- data/stubs/default/app/views/layouts/mailer.html.erb +367 -372
- data/stubs/default/app/views/layouts/mailer.text.erb +1 -4
- data/stubs/default/app/views/user_mailer/reset_password.html.erb +21 -26
- data/stubs/default/config/routes.rb +16 -16
- data/stubs/default/db/migrate/20240101000001_create_delayed_jobs.rb +1 -1
- data/stubs/default/test/factories/users.rb +7 -0
- data/stubs/default/test/integration/auth/authentication_test.rb +43 -0
- data/stubs/default/test/integration/auth/password_reset_test.rb +41 -0
- data/stubs/default/test/integration/auth/registration_test.rb +21 -0
- data/stubs/default/test/integration/password_update_test.rb +28 -0
- data/stubs/default/test/integration/profile_test.rb +51 -0
- data/stubs/default/test/test_helper.rb +38 -0
- data/stubs/hotwire/app/components/danger_button_component.rb +1 -1
- data/stubs/hotwire/app/components/dropdown_component.html.erb +17 -18
- data/stubs/hotwire/app/components/dropdown_component.rb +7 -7
- data/stubs/hotwire/app/components/modal_component.html.erb +55 -59
- data/stubs/hotwire/app/components/modal_component.rb +6 -6
- data/stubs/hotwire/app/components/primary_button_component.rb +1 -1
- data/stubs/hotwire/app/components/secondary_button_component.rb +1 -1
- data/stubs/hotwire/app/controllers/application_controller.rb +1 -0
- data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +12 -9
- data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +7 -5
- data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +7 -5
- data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +7 -5
- data/stubs/hotwire/app/controllers/concerns/authenticate.rb +5 -20
- data/stubs/hotwire/app/controllers/concerns/redirect_if_authenticated.rb +19 -0
- data/stubs/hotwire/app/controllers/concerns/set_current_auth.rb +9 -0
- data/stubs/hotwire/app/controllers/password_controller.rb +3 -3
- data/stubs/hotwire/app/controllers/profile_controller.rb +11 -9
- data/stubs/hotwire/app/controllers/welcome_controller.rb +1 -1
- data/stubs/hotwire/app/javascript/application.js +3 -3
- data/stubs/hotwire/app/views/auth/forgot_password.html.erb +12 -17
- data/stubs/hotwire/app/views/auth/login.html.erb +0 -9
- data/stubs/hotwire/app/views/auth/register.html.erb +0 -13
- data/stubs/hotwire/app/views/auth/reset_password.html.erb +0 -7
- data/stubs/hotwire/app/views/dashboard/index.html.erb +9 -10
- data/stubs/hotwire/app/views/layouts/_navigation.html.erb +77 -87
- data/stubs/hotwire/app/views/layouts/application.html.erb +0 -9
- data/stubs/hotwire/app/views/layouts/guest.html.erb +0 -6
- data/stubs/hotwire/app/views/profile/edit.html.erb +19 -22
- data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +32 -42
- data/stubs/hotwire/app/views/profile/partials/_update_password_form.html.erb +42 -55
- data/stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb +36 -46
- data/stubs/hotwire/app/views/welcome/index.html.erb +34 -46
- data/stubs/hotwire/config/importmap.rb +3 -3
- data/stubs/hotwire/config/tailwind.config.js +2 -2
- data/stubs/inertia-common/app/controllers/application_controller.rb +1 -0
- data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +11 -8
- data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +5 -3
- data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +5 -3
- data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +5 -3
- data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +5 -20
- data/stubs/inertia-common/app/controllers/concerns/handle_inertia_requests.rb +1 -1
- data/stubs/inertia-common/app/controllers/concerns/redirect_if_authenticated.rb +19 -0
- data/stubs/inertia-common/app/controllers/concerns/set_current_auth.rb +9 -0
- data/stubs/inertia-common/app/controllers/concerns/verify_csrf_token.rb +4 -4
- data/stubs/inertia-common/app/controllers/dashboard_controller.rb +1 -1
- data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
- data/stubs/inertia-common/app/controllers/profile_controller.rb +7 -5
- data/stubs/inertia-common/app/controllers/welcome_controller.rb +2 -2
- data/stubs/inertia-common/bin/vite +6 -6
- data/stubs/inertia-common/test/integration/password_update_test.rb +28 -0
- data/stubs/inertia-common/test/integration/profile_test.rb +51 -0
- data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +13 -9
- data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +15 -12
- data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +20 -15
- data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +119 -87
- data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +14 -7
- data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +18 -7
- data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +60 -60
- data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +21 -16
- data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +20 -15
- data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +19 -14
- data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +22 -16
- data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +35 -24
- data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +157 -117
- data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +15 -15
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +52 -49
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +90 -82
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +118 -115
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +63 -60
- data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +23 -17
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +31 -27
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +109 -99
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +121 -113
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +76 -69
- data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +87 -63
- data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +32 -25
- data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +0 -4
- data/stubs/inertia-react-ts/config/tailwind.config.js +2 -2
- data/stubs/inertia-react-ts/vite.config.ts +2 -5
- data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +10 -6
- data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +18 -18
- data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +5 -5
- data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +60 -57
- data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +9 -9
- data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +7 -7
- data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +6 -6
- data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +84 -74
- data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +12 -12
- data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +5 -5
- data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +12 -12
- data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +13 -13
- data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +13 -13
- data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +168 -136
- data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +15 -13
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +56 -49
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +78 -72
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +101 -97
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +71 -68
- data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +22 -14
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +34 -30
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +87 -83
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +105 -98
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +69 -59
- data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +74 -47
- data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +0 -4
- data/stubs/inertia-vue-ts/config/tailwind.config.js +2 -2
- data/stubs/inertia-vue-ts/vite.config.ts +2 -5
- metadata +19 -6
- data/stubs/hotwire/bin/vite +0 -27
- data/stubs/inertia-common/Procfile.dev +0 -3
- /data/stubs/{hotwire → default}/Procfile.dev +0 -0
- /data/stubs/hotwire/app/javascript/{alpinejs.js → alpinejs.stub} +0 -0
@@ -1,13 +1,10 @@
|
|
1
1
|
<!-- Header -->
|
2
2
|
<%= ENV.fetch("APP_NAME", "Rails") %>: <%= root_url %>
|
3
|
-
|
4
3
|
<!-- Body -->
|
5
4
|
<%= yield %>
|
6
|
-
|
7
5
|
<!-- Subcopy -->
|
8
6
|
<% if content_for?(:subcopy) %>
|
9
|
-
<%= yield :subcopy %>
|
7
|
+
<%= yield :subcopy %>
|
10
8
|
<% end %>
|
11
|
-
|
12
9
|
<!-- Footer -->
|
13
10
|
© <%= Time.new.year %> <%= ENV.fetch("APP_NAME", "Rails") %>. All rights reserved.
|
@@ -1,39 +1,34 @@
|
|
1
1
|
<!-- Greeting -->
|
2
2
|
<h1>Hello!</h1>
|
3
|
-
|
4
3
|
<!-- Intro Lines -->
|
5
4
|
<p>You are receiving this email because we received a password reset request for your account.</p>
|
6
|
-
|
7
5
|
<!-- Action Button -->
|
8
6
|
<table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
|
9
|
-
<tr>
|
10
|
-
<td align="center">
|
11
|
-
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
12
|
-
<tr>
|
13
|
-
<td align="center">
|
14
|
-
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
|
15
|
-
<tr>
|
16
|
-
<td>
|
17
|
-
<a href="<%= password_reset_url(token: params[:token]) %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
|
18
|
-
</td>
|
19
|
-
</tr>
|
7
|
+
<tr>
|
8
|
+
<td align="center">
|
9
|
+
<table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
|
10
|
+
<tr>
|
11
|
+
<td align="center">
|
12
|
+
<table border="0" cellpadding="0" cellspacing="0" role="presentation">
|
13
|
+
<tr>
|
14
|
+
<td>
|
15
|
+
<a href="<%= password_reset_url(token: params[:token]) %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
|
16
|
+
</td>
|
17
|
+
</tr>
|
18
|
+
</table>
|
19
|
+
</td>
|
20
|
+
</tr>
|
21
|
+
</table>
|
22
|
+
</td>
|
23
|
+
</tr>
|
20
24
|
</table>
|
21
|
-
</td>
|
22
|
-
</tr>
|
23
|
-
</table>
|
24
|
-
</td>
|
25
|
-
</tr>
|
26
|
-
</table>
|
27
|
-
|
28
25
|
<!-- Outro Lines -->
|
29
26
|
<p>This password reset link will expire in 60 minutes.</p>
|
30
|
-
|
31
27
|
<p>If you did not request a password reset, no further action is required.</p>
|
32
|
-
|
33
28
|
<!-- Salutation -->
|
34
|
-
<p>Regards,<br
|
35
|
-
|
29
|
+
<p>Regards,<br>
|
30
|
+
<%= ENV.fetch("APP_NAME", "Rails") %></p>
|
36
31
|
<!-- Subcopy -->
|
37
32
|
<% content_for :subcopy do %>
|
38
|
-
<p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to password_reset_url(token: params[:token]), password_reset_url(token: params[:token]) %><span></p>
|
39
|
-
<% end %>
|
33
|
+
<p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to password_reset_url(token: params[:token]), password_reset_url(token: params[:token]) %><span></p>
|
34
|
+
<% end %>
|
@@ -1,27 +1,27 @@
|
|
1
1
|
Rails.application.routes.draw do
|
2
|
-
get
|
2
|
+
get 'up' => 'rails/health#show', as: :rails_health_check
|
3
3
|
|
4
|
-
root
|
4
|
+
root 'welcome#index'
|
5
5
|
|
6
|
-
get
|
7
|
-
post
|
6
|
+
get 'register', to: 'auth/registered_user#new', as: :register
|
7
|
+
post 'register', to: 'auth/registered_user#create'
|
8
8
|
|
9
|
-
get
|
10
|
-
post
|
9
|
+
get 'login', to: 'auth/authenticated_session#new', as: :login
|
10
|
+
post 'login', to: 'auth/authenticated_session#create'
|
11
11
|
|
12
|
-
get
|
13
|
-
post
|
12
|
+
get 'forgot-password', to: 'auth/password_reset_link#new', as: :password_request
|
13
|
+
post 'forgot-password', to: 'auth/password_reset_link#create', as: :password_email
|
14
14
|
|
15
|
-
get
|
16
|
-
post
|
15
|
+
get 'reset-password/:token', to: 'auth/new_password#new', as: :password_reset
|
16
|
+
post 'reset-password', to: 'auth/new_password#create', as: :password_store
|
17
17
|
|
18
|
-
post
|
18
|
+
post 'logout', to: 'auth/authenticated_session#destroy', as: :logout
|
19
19
|
|
20
|
-
get
|
20
|
+
get 'dashboard', to: 'dashboard#index', as: :dashboard
|
21
21
|
|
22
|
-
get
|
23
|
-
patch
|
24
|
-
delete
|
22
|
+
get 'profile', to: 'profile#edit', as: :profile_edit
|
23
|
+
patch 'profile', to: 'profile#update', as: :profile_update
|
24
|
+
delete 'profile', to: 'profile#destroy', as: :profile_destroy
|
25
25
|
|
26
|
-
put
|
26
|
+
put 'password', to: 'password#update', as: :password_update
|
27
27
|
end
|
@@ -13,7 +13,7 @@ class CreateDelayedJobs < ActiveRecord::Migration
|
|
13
13
|
table.timestamps null: true
|
14
14
|
end
|
15
15
|
|
16
|
-
add_index :delayed_jobs, [ :priority, :run_at ], name:
|
16
|
+
add_index :delayed_jobs, [ :priority, :run_at ], name: 'delayed_jobs_priority'
|
17
17
|
end
|
18
18
|
|
19
19
|
def self.down
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Auth::AuthenticationTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'login screen can be rendered' do
|
5
|
+
get login_path
|
6
|
+
|
7
|
+
assert_response :success
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'users can authenticate using the login screen' do
|
11
|
+
user = FactoryBot.create :user
|
12
|
+
|
13
|
+
post login_path, params: {
|
14
|
+
email: user.email,
|
15
|
+
password: 'password'
|
16
|
+
}
|
17
|
+
|
18
|
+
assert_authenticated
|
19
|
+
assert_redirected_to dashboard_path
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'users cannot authenticate with invalid password' do
|
23
|
+
user = FactoryBot.create :user
|
24
|
+
|
25
|
+
post login_path, params: {
|
26
|
+
email: user.email,
|
27
|
+
password: 'wrong-password'
|
28
|
+
}
|
29
|
+
|
30
|
+
assert_guest
|
31
|
+
end
|
32
|
+
|
33
|
+
test 'users can logout' do
|
34
|
+
user = FactoryBot.create :user
|
35
|
+
|
36
|
+
acting_as user
|
37
|
+
|
38
|
+
post logout_path
|
39
|
+
|
40
|
+
assert_guest
|
41
|
+
assert_redirected_to '/'
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Auth::PasswordResetTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'reset password link screen can be rendered' do
|
5
|
+
get password_request_path
|
6
|
+
|
7
|
+
assert_response :success
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'reset password link can be requested' do
|
11
|
+
user = FactoryBot.create :user
|
12
|
+
token = user.generate_token_for(:password_reset)
|
13
|
+
email = UserMailer.with(user: user, token: token).reset_password
|
14
|
+
|
15
|
+
post password_email_path, params: { email: user.email }
|
16
|
+
|
17
|
+
assert_enqueued_email_with UserMailer, :reset_password, params: { user: user, token: token } do
|
18
|
+
email.deliver_later
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
test 'reset password screen can be rendered' do
|
23
|
+
user = FactoryBot.create :user
|
24
|
+
|
25
|
+
get password_reset_path(token: user.generate_token_for(:password_reset))
|
26
|
+
|
27
|
+
assert_response :success
|
28
|
+
end
|
29
|
+
|
30
|
+
test 'password can be reset_with_valid_token' do
|
31
|
+
user = FactoryBot.create :user
|
32
|
+
|
33
|
+
post password_store_path, params: {
|
34
|
+
token: user.generate_token_for(:password_reset),
|
35
|
+
password: 'password',
|
36
|
+
password_confirmation: 'password'
|
37
|
+
}
|
38
|
+
|
39
|
+
assert_redirected_to login_path
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class Auth::RegistrationTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'registration screen can be rendered' do
|
5
|
+
get register_path
|
6
|
+
|
7
|
+
assert_response :success
|
8
|
+
end
|
9
|
+
|
10
|
+
test 'new users can register' do
|
11
|
+
post register_path, params: {
|
12
|
+
name: 'Test User',
|
13
|
+
email: 'test@example.com',
|
14
|
+
password: 'password',
|
15
|
+
password_confirmation: 'password'
|
16
|
+
}
|
17
|
+
|
18
|
+
assert_authenticated
|
19
|
+
assert_redirected_to dashboard_url
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class PasswordUpdateTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'password can be updated' do
|
5
|
+
user = FactoryBot.create :user
|
6
|
+
|
7
|
+
acting_as(user).put password_update_path, params: {
|
8
|
+
current_password: 'password',
|
9
|
+
password: 'new-password',
|
10
|
+
password_confirmation: 'new-password'
|
11
|
+
}
|
12
|
+
|
13
|
+
assert_redirected_to profile_edit_path
|
14
|
+
assert BCrypt::Password.new(user.reload.password_digest).is_password?('new-password')
|
15
|
+
end
|
16
|
+
|
17
|
+
test 'correct password must be provided to update password' do
|
18
|
+
user = FactoryBot.create :user
|
19
|
+
|
20
|
+
acting_as(user).put password_update_path, params: {
|
21
|
+
current_password: 'wrong-password',
|
22
|
+
password: 'new-password',
|
23
|
+
password_confirmation: 'new-password'
|
24
|
+
}
|
25
|
+
|
26
|
+
assert_response :unprocessable_entity
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ProfileTest < ActionDispatch::IntegrationTest
|
4
|
+
test 'profile page is displayed' do
|
5
|
+
user = FactoryBot.create :user
|
6
|
+
|
7
|
+
acting_as(user).get profile_edit_path
|
8
|
+
|
9
|
+
assert_response :success
|
10
|
+
end
|
11
|
+
|
12
|
+
test 'profile information can be updated' do
|
13
|
+
user = FactoryBot.create :user
|
14
|
+
|
15
|
+
acting_as(user).patch profile_edit_path, params: {
|
16
|
+
name: 'Test User',
|
17
|
+
email: 'test@example.com'
|
18
|
+
}
|
19
|
+
|
20
|
+
assert_redirected_to profile_edit_path
|
21
|
+
|
22
|
+
user.reload
|
23
|
+
|
24
|
+
assert_equal 'Test User', user.name
|
25
|
+
assert_equal 'test@example.com', user.email
|
26
|
+
end
|
27
|
+
|
28
|
+
test 'user can delete their account' do
|
29
|
+
user = FactoryBot.create :user
|
30
|
+
|
31
|
+
acting_as(user).delete profile_destroy_path, params: {
|
32
|
+
password: 'password'
|
33
|
+
}
|
34
|
+
|
35
|
+
assert_redirected_to '/'
|
36
|
+
|
37
|
+
assert_guest
|
38
|
+
assert_raise(ActiveRecord::RecordNotFound) { user.reload }
|
39
|
+
end
|
40
|
+
|
41
|
+
test 'correct password must be provided to delete account' do
|
42
|
+
user = FactoryBot.create :user
|
43
|
+
|
44
|
+
acting_as(user).delete profile_destroy_path, params: {
|
45
|
+
password: 'wrong-password'
|
46
|
+
}
|
47
|
+
|
48
|
+
assert_response :unprocessable_entity
|
49
|
+
assert_not_nil user.reload
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
ENV['RAILS_ENV'] ||= 'test'
|
2
|
+
require_relative '../config/environment'
|
3
|
+
require 'rails/test_help'
|
4
|
+
|
5
|
+
module ActiveSupport
|
6
|
+
class TestCase
|
7
|
+
# Run tests in parallel with specified workers
|
8
|
+
parallelize(workers: :number_of_processors)
|
9
|
+
|
10
|
+
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
11
|
+
fixtures :all
|
12
|
+
|
13
|
+
# Add more helper methods to be used by all tests here...
|
14
|
+
def acting_as(user)
|
15
|
+
post login_path, params: { email: user.email, password: 'password' }
|
16
|
+
|
17
|
+
self
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_auth
|
21
|
+
Auth.new('web', session)
|
22
|
+
end
|
23
|
+
|
24
|
+
def assert_authenticated
|
25
|
+
assert is_authenticated, 'The user is not authenticated'
|
26
|
+
end
|
27
|
+
|
28
|
+
def assert_guest
|
29
|
+
assert_not is_authenticated, 'The user is authenticated'
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def is_authenticated
|
35
|
+
current_auth.check?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -6,7 +6,7 @@ class DangerButtonComponent < ViewComponent::Base
|
|
6
6
|
ERB
|
7
7
|
|
8
8
|
def initialize(attributes = {})
|
9
|
-
attributes[:type] = attributes[:type] ||
|
9
|
+
attributes[:type] = attributes[:type] || 'submit'
|
10
10
|
attributes[:class] = "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#{" #{attributes[:class]}" if attributes[:class]}"
|
11
11
|
@attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
|
12
12
|
end
|
@@ -1,20 +1,19 @@
|
|
1
|
-
<div class="relative" x-data="{ open: false }"
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
<%= content %>
|
18
|
-
</div>
|
1
|
+
<div class="relative" x-data="{ open: false }" x-on:click.outside="open = false" x-on:close.stop="open = false">
|
2
|
+
<div x-on:click="open = ! open">
|
3
|
+
<%= trigger %>
|
4
|
+
</div>
|
5
|
+
<div x-show="open"
|
6
|
+
x-transition:enter="transition ease-out duration-200"
|
7
|
+
x-transition:enter-start="opacity-0 scale-95"
|
8
|
+
x-transition:enter-end="opacity-100 scale-100"
|
9
|
+
x-transition:leave="transition ease-in duration-75"
|
10
|
+
x-transition:leave-start="opacity-100 scale-100"
|
11
|
+
x-transition:leave-end="opacity-0 scale-95"
|
12
|
+
class="absolute z-50 mt-2 <%= @width %> rounded-md shadow-lg <%= @alignment_classes %>"
|
13
|
+
style="display: none;"
|
14
|
+
x-on:click="open = false">
|
15
|
+
<div class="rounded-md ring-1 ring-black ring-opacity-5 <%= @content_classes %>">
|
16
|
+
<%= content %>
|
19
17
|
</div>
|
18
|
+
</div>
|
20
19
|
</div>
|
@@ -1,15 +1,15 @@
|
|
1
1
|
class DropdownComponent < ViewComponent::Base
|
2
2
|
renders_one :trigger
|
3
3
|
|
4
|
-
def initialize(align:
|
5
|
-
if align ==
|
6
|
-
@alignment_classes =
|
7
|
-
elsif align ==
|
8
|
-
@alignment_classes =
|
4
|
+
def initialize(align: 'right', width: '48', content_classes: 'py-1 bg-white dark:bg-gray-700')
|
5
|
+
if align == 'left'
|
6
|
+
@alignment_classes = 'ltr:origin-top-left rtl:origin-top-right start-0'
|
7
|
+
elsif align == 'top'
|
8
|
+
@alignment_classes = 'origin-top'
|
9
9
|
else
|
10
|
-
@alignment_classes =
|
10
|
+
@alignment_classes = 'ltr:origin-top-right rtl:origin-top-left end-0'
|
11
11
|
end
|
12
|
-
@width = width ==
|
12
|
+
@width = width == '48' ? 'w-48' : width
|
13
13
|
@content_classes = content_classes
|
14
14
|
end
|
15
15
|
end
|
@@ -1,62 +1,58 @@
|
|
1
1
|
<div
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
2
|
+
x-data="{
|
3
|
+
show: <%= @show %>,
|
4
|
+
focusables() {
|
5
|
+
// All focusable element types...
|
6
|
+
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
|
7
|
+
return [...$el.querySelectorAll(selector)]
|
8
|
+
// All non-disabled elements...
|
9
|
+
.filter(el => ! el.hasAttribute('disabled'))
|
10
|
+
},
|
11
|
+
firstFocusable() { return this.focusables()[0] },
|
12
|
+
lastFocusable() { return this.focusables().slice(-1)[0] },
|
13
|
+
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
|
14
|
+
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
|
15
|
+
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
|
16
|
+
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
|
17
|
+
}"
|
18
|
+
x-init="$watch('show', value => {
|
19
|
+
if (value) {
|
20
|
+
document.body.classList.add('overflow-y-hidden')
|
21
|
+
<%= @attributes.has_key?(:focusable) ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' %>
|
22
|
+
} else {
|
23
|
+
document.body.classList.remove('overflow-y-hidden')
|
24
|
+
}
|
25
|
+
})"
|
26
|
+
x-on:open-modal.window="$event.detail == '<%= @name %>' ? show = true : null"
|
27
|
+
x-on:close-modal.window="$event.detail == '<%= @name %>' ? show = false : null"
|
28
|
+
x-on:close.stop="show = false"
|
29
|
+
x-on:keydown.escape.window="show = false"
|
30
|
+
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
|
31
|
+
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
|
32
|
+
x-show="show"
|
33
|
+
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
|
34
|
+
style="display: <%= @show ? 'block' : 'none' %>">
|
35
|
+
<div
|
32
36
|
x-show="show"
|
33
|
-
class="fixed inset-0
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
55
|
-
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
56
|
-
x-transition:leave="ease-in duration-200"
|
57
|
-
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
58
|
-
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
59
|
-
>
|
60
|
-
<%= content %>
|
61
|
-
</div>
|
37
|
+
class="fixed inset-0 transform transition-all"
|
38
|
+
x-on:click="show = false"
|
39
|
+
x-transition:enter="ease-out duration-300"
|
40
|
+
x-transition:enter-start="opacity-0"
|
41
|
+
x-transition:enter-end="opacity-100"
|
42
|
+
x-transition:leave="ease-in duration-200"
|
43
|
+
x-transition:leave-start="opacity-100"
|
44
|
+
x-transition:leave-end="opacity-0">
|
45
|
+
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
|
46
|
+
</div>
|
47
|
+
<div
|
48
|
+
x-show="show"
|
49
|
+
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full <%= @max_width %> sm:mx-auto"
|
50
|
+
x-transition:enter="ease-out duration-300"
|
51
|
+
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
52
|
+
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
53
|
+
x-transition:leave="ease-in duration-200"
|
54
|
+
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
55
|
+
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
|
56
|
+
<%= content %>
|
57
|
+
</div>
|
62
58
|
</div>
|
@@ -3,12 +3,12 @@ class ModalComponent < ViewComponent::Base
|
|
3
3
|
@name = attributes[:name]
|
4
4
|
@show = attributes[:show] || false
|
5
5
|
@max_width = {
|
6
|
-
:sm =>
|
7
|
-
:md =>
|
8
|
-
:lg =>
|
9
|
-
:xl =>
|
10
|
-
|
11
|
-
}[attributes[: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'
|
11
|
+
}[attributes[:max_width] || '2xl']
|
12
12
|
@attributes = attributes.without(:name, :show, :max_width)
|
13
13
|
end
|
14
14
|
end
|
@@ -6,7 +6,7 @@ class PrimaryButtonComponent < ViewComponent::Base
|
|
6
6
|
ERB
|
7
7
|
|
8
8
|
def initialize(attributes = {})
|
9
|
-
attributes[:type] = attributes[:type] ||
|
9
|
+
attributes[:type] = attributes[:type] || 'submit'
|
10
10
|
attributes[:class] = "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#{" #{attributes[:class]}" if attributes[:class]}"
|
11
11
|
@attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
|
12
12
|
end
|
@@ -6,7 +6,7 @@ class SecondaryButtonComponent < ViewComponent::Base
|
|
6
6
|
ERB
|
7
7
|
|
8
8
|
def initialize(attributes = {})
|
9
|
-
attributes[:type] = attributes[:type] ||
|
9
|
+
attributes[:type] = attributes[:type] || 'button'
|
10
10
|
attributes[:class] = "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#{" #{attributes[:class]}" if attributes[:class]}"
|
11
11
|
@attributes = attributes.map { |key, attribute| "#{key}=\"#{attribute}\"" }
|
12
12
|
end
|