kaze 0.4.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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