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.
Files changed (141) hide show
  1. checksums.yaml +4 -4
  2. data/lib/kaze/commands/install_command.rb +25 -13
  3. data/lib/kaze/commands/installs_hotwire_stack.rb +11 -8
  4. data/lib/kaze/commands/installs_inertia_stacks.rb +34 -24
  5. data/lib/kaze/commands/version_command.rb +6 -0
  6. data/lib/kaze/version.rb +1 -1
  7. data/lib/kaze.rb +1 -3
  8. data/stubs/default/app/forms/auth/login_form.rb +2 -8
  9. data/stubs/default/app/forms/auth/new_password_form.rb +1 -1
  10. data/stubs/default/app/forms/update_profile_information_form.rb +1 -1
  11. data/stubs/default/app/mailers/application_mailer.rb +4 -4
  12. data/stubs/default/app/mailers/user_mailer.rb +1 -1
  13. data/stubs/default/app/models/auth.rb +57 -0
  14. data/stubs/default/app/models/current.rb +1 -1
  15. data/stubs/default/app/validators/current_password_validator.rb +1 -1
  16. data/stubs/default/app/validators/email_validator.rb +1 -1
  17. data/stubs/default/app/validators/lowercase_validator.rb +2 -2
  18. data/stubs/default/app/views/layouts/mailer.html.erb +367 -372
  19. data/stubs/default/app/views/layouts/mailer.text.erb +1 -4
  20. data/stubs/default/app/views/user_mailer/reset_password.html.erb +21 -26
  21. data/stubs/default/config/routes.rb +16 -16
  22. data/stubs/default/db/migrate/20240101000001_create_delayed_jobs.rb +1 -1
  23. data/stubs/default/test/factories/users.rb +7 -0
  24. data/stubs/default/test/integration/auth/authentication_test.rb +43 -0
  25. data/stubs/default/test/integration/auth/password_reset_test.rb +41 -0
  26. data/stubs/default/test/integration/auth/registration_test.rb +21 -0
  27. data/stubs/default/test/integration/password_update_test.rb +28 -0
  28. data/stubs/default/test/integration/profile_test.rb +51 -0
  29. data/stubs/default/test/test_helper.rb +38 -0
  30. data/stubs/hotwire/app/components/danger_button_component.rb +1 -1
  31. data/stubs/hotwire/app/components/dropdown_component.html.erb +17 -18
  32. data/stubs/hotwire/app/components/dropdown_component.rb +7 -7
  33. data/stubs/hotwire/app/components/modal_component.html.erb +55 -59
  34. data/stubs/hotwire/app/components/modal_component.rb +6 -6
  35. data/stubs/hotwire/app/components/primary_button_component.rb +1 -1
  36. data/stubs/hotwire/app/components/secondary_button_component.rb +1 -1
  37. data/stubs/hotwire/app/controllers/application_controller.rb +1 -0
  38. data/stubs/hotwire/app/controllers/auth/authenticated_session_controller.rb +12 -9
  39. data/stubs/hotwire/app/controllers/auth/new_password_controller.rb +7 -5
  40. data/stubs/hotwire/app/controllers/auth/password_reset_link_controller.rb +7 -5
  41. data/stubs/hotwire/app/controllers/auth/registered_user_controller.rb +7 -5
  42. data/stubs/hotwire/app/controllers/concerns/authenticate.rb +5 -20
  43. data/stubs/hotwire/app/controllers/concerns/redirect_if_authenticated.rb +19 -0
  44. data/stubs/hotwire/app/controllers/concerns/set_current_auth.rb +9 -0
  45. data/stubs/hotwire/app/controllers/password_controller.rb +3 -3
  46. data/stubs/hotwire/app/controllers/profile_controller.rb +11 -9
  47. data/stubs/hotwire/app/controllers/welcome_controller.rb +1 -1
  48. data/stubs/hotwire/app/javascript/application.js +3 -3
  49. data/stubs/hotwire/app/views/auth/forgot_password.html.erb +12 -17
  50. data/stubs/hotwire/app/views/auth/login.html.erb +0 -9
  51. data/stubs/hotwire/app/views/auth/register.html.erb +0 -13
  52. data/stubs/hotwire/app/views/auth/reset_password.html.erb +0 -7
  53. data/stubs/hotwire/app/views/dashboard/index.html.erb +9 -10
  54. data/stubs/hotwire/app/views/layouts/_navigation.html.erb +77 -87
  55. data/stubs/hotwire/app/views/layouts/application.html.erb +0 -9
  56. data/stubs/hotwire/app/views/layouts/guest.html.erb +0 -6
  57. data/stubs/hotwire/app/views/profile/edit.html.erb +19 -22
  58. data/stubs/hotwire/app/views/profile/partials/_delete_user_form.html.erb +32 -42
  59. data/stubs/hotwire/app/views/profile/partials/_update_password_form.html.erb +42 -55
  60. data/stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb +36 -46
  61. data/stubs/hotwire/app/views/welcome/index.html.erb +34 -46
  62. data/stubs/hotwire/config/importmap.rb +3 -3
  63. data/stubs/hotwire/config/tailwind.config.js +2 -2
  64. data/stubs/inertia-common/app/controllers/application_controller.rb +1 -0
  65. data/stubs/inertia-common/app/controllers/auth/authenticated_session_controller.rb +11 -8
  66. data/stubs/inertia-common/app/controllers/auth/new_password_controller.rb +5 -3
  67. data/stubs/inertia-common/app/controllers/auth/password_reset_link_controller.rb +5 -3
  68. data/stubs/inertia-common/app/controllers/auth/registered_user_controller.rb +5 -3
  69. data/stubs/inertia-common/app/controllers/concerns/authenticate.rb +5 -20
  70. data/stubs/inertia-common/app/controllers/concerns/handle_inertia_requests.rb +1 -1
  71. data/stubs/inertia-common/app/controllers/concerns/redirect_if_authenticated.rb +19 -0
  72. data/stubs/inertia-common/app/controllers/concerns/set_current_auth.rb +9 -0
  73. data/stubs/inertia-common/app/controllers/concerns/verify_csrf_token.rb +4 -4
  74. data/stubs/inertia-common/app/controllers/dashboard_controller.rb +1 -1
  75. data/stubs/inertia-common/app/controllers/password_controller.rb +1 -1
  76. data/stubs/inertia-common/app/controllers/profile_controller.rb +7 -5
  77. data/stubs/inertia-common/app/controllers/welcome_controller.rb +2 -2
  78. data/stubs/inertia-common/bin/vite +6 -6
  79. data/stubs/inertia-common/test/integration/password_update_test.rb +28 -0
  80. data/stubs/inertia-common/test/integration/profile_test.rb +51 -0
  81. data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +13 -9
  82. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +15 -12
  83. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +20 -15
  84. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +119 -87
  85. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +14 -7
  86. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +18 -7
  87. data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +60 -60
  88. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +21 -16
  89. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +20 -15
  90. data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +19 -14
  91. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +22 -16
  92. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +35 -24
  93. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +157 -117
  94. data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +15 -15
  95. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +52 -49
  96. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +90 -82
  97. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +118 -115
  98. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +63 -60
  99. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +23 -17
  100. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +31 -27
  101. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +109 -99
  102. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +121 -113
  103. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +76 -69
  104. data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +87 -63
  105. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +32 -25
  106. data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +0 -4
  107. data/stubs/inertia-react-ts/config/tailwind.config.js +2 -2
  108. data/stubs/inertia-react-ts/vite.config.ts +2 -5
  109. data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +10 -6
  110. data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +18 -18
  111. data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +5 -5
  112. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +60 -57
  113. data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +9 -9
  114. data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +7 -7
  115. data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +6 -6
  116. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +84 -74
  117. data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +12 -12
  118. data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +5 -5
  119. data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +12 -12
  120. data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +13 -13
  121. data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +13 -13
  122. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +168 -136
  123. data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +15 -13
  124. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +56 -49
  125. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +78 -72
  126. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +101 -97
  127. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +71 -68
  128. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +22 -14
  129. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +34 -30
  130. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +87 -83
  131. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +105 -98
  132. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +69 -59
  133. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +74 -47
  134. data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +0 -4
  135. data/stubs/inertia-vue-ts/config/tailwind.config.js +2 -2
  136. data/stubs/inertia-vue-ts/vite.config.ts +2 -5
  137. metadata +19 -6
  138. data/stubs/hotwire/bin/vite +0 -27
  139. data/stubs/inertia-common/Procfile.dev +0 -3
  140. /data/stubs/{hotwire → default}/Procfile.dev +0 -0
  141. /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><%= ENV.fetch("APP_NAME", "Rails") %></p>
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 "up" => "rails/health#show", as: :rails_health_check
2
+ get 'up' => 'rails/health#show', as: :rails_health_check
3
3
 
4
- root "welcome#index"
4
+ root 'welcome#index'
5
5
 
6
- get "register", to: "auth/registered_user#new", as: :register
7
- post "register", to: "auth/registered_user#create"
6
+ get 'register', to: 'auth/registered_user#new', as: :register
7
+ post 'register', to: 'auth/registered_user#create'
8
8
 
9
- get "login", to: "auth/authenticated_session#new", as: :login
10
- post "login", to: "auth/authenticated_session#create"
9
+ get 'login', to: 'auth/authenticated_session#new', as: :login
10
+ post 'login', to: 'auth/authenticated_session#create'
11
11
 
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
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 "reset-password/:token", to: "auth/new_password#new", as: :password_reset
16
- post "reset-password", to: "auth/new_password#create", as: :password_store
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 "logout", to: "auth/authenticated_session#destroy", as: :logout
18
+ post 'logout', to: 'auth/authenticated_session#destroy', as: :logout
19
19
 
20
- get "dashboard", to: "dashboard#index", as: :dashboard
20
+ get 'dashboard', to: 'dashboard#index', as: :dashboard
21
21
 
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
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 "password", to: "password#update", as: :password_update
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: "delayed_jobs_priority"
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,7 @@
1
+ FactoryBot.define do
2
+ factory :user do
3
+ name { Faker::Name.name }
4
+ email { Faker::Internet.email }
5
+ password { 'password' }
6
+ end
7
+ end
@@ -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] || "submit"
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 }" @click.outside="open = false" @close.stop="open = false">
2
- <div @click="open = ! open">
3
- <%= trigger %>
4
- </div>
5
-
6
- <div x-show="open"
7
- x-transition:enter="transition ease-out duration-200"
8
- x-transition:enter-start="opacity-0 scale-95"
9
- x-transition:enter-end="opacity-100 scale-100"
10
- x-transition:leave="transition ease-in duration-75"
11
- x-transition:leave-start="opacity-100 scale-100"
12
- x-transition:leave-end="opacity-0 scale-95"
13
- class="absolute z-50 mt-2 <%= @width %> rounded-md shadow-lg <%= @alignment_classes %>"
14
- style="display: none;"
15
- @click="open = false">
16
- <div class="rounded-md ring-1 ring-black ring-opacity-5 <%= @content_classes %>">
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: "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"
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 = "ltr:origin-top-right rtl:origin-top-left end-0"
10
+ @alignment_classes = 'ltr:origin-top-right rtl:origin-top-left end-0'
11
11
  end
12
- @width = width == "48" ? "w-48" : 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
- 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()"
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 overflow-y-auto px-4 py-6 sm:px-0 z-50"
34
- style="display: <%= @show ? 'block' : 'none' %>;"
35
- >
36
- <div
37
- x-show="show"
38
- class="fixed inset-0 transform transition-all"
39
- x-on:click="show = false"
40
- x-transition:enter="ease-out duration-300"
41
- x-transition:enter-start="opacity-0"
42
- x-transition:enter-end="opacity-100"
43
- x-transition:leave="ease-in duration-200"
44
- x-transition:leave-start="opacity-100"
45
- x-transition:leave-end="opacity-0"
46
- >
47
- <div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75"></div>
48
- </div>
49
-
50
- <div
51
- x-show="show"
52
- 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"
53
- x-transition:enter="ease-out duration-300"
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 => "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"]
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] || "submit"
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] || "button"
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
@@ -1,3 +1,4 @@
1
1
  class ApplicationController < ActionController::Base
2
+ include SetCurrentAuth
2
3
  include Authenticate
3
4
  end