kaze 0.9.0 → 0.11.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/README.md +23 -11
- data/lib/kaze/commands/install_command.rb +13 -15
- data/lib/kaze/commands/installs_inertia_stacks.rb +2 -2
- data/lib/kaze/version.rb +1 -1
- data/stubs/default/app/forms/update_profile_information_form.rb +8 -0
- data/stubs/default/app/models/concerns/must_verify_email.rb +0 -2
- data/stubs/default/config/routes.rb +1 -1
- data/stubs/default/db/migrate/20240101000000_create_users.rb +7 -5
- data/stubs/default/test/integration/profile_test.rb +12 -0
- data/stubs/hotwire/app/components/modal_component.rb +1 -1
- data/stubs/hotwire/app/controllers/concerns/validate_signature.rb +1 -1
- data/stubs/hotwire/app/controllers/password_controller.rb +2 -0
- data/stubs/hotwire/app/controllers/profile_controller.rb +3 -1
- data/stubs/hotwire/app/views/profile/partials/_update_profile_information_form.html.erb +18 -0
- data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb +1 -1
- data/stubs/inertia-common/app/controllers/concerns/validate_signature.rb +1 -1
- data/stubs/inertia-common/app/controllers/password_controller.rb +2 -0
- data/stubs/inertia-common/app/controllers/profile_controller.rb +5 -2
- data/stubs/inertia-common/test/integration/profile_test.rb +12 -0
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +2 -2
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +2 -2
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +1 -1
- data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +1 -1
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +10 -6
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +6 -4
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +3 -3
- data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +38 -6
- data/stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts +3 -3
- data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +4 -4
- data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +8 -8
- data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +1 -18
- data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +23 -2
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts +27 -23
- data/stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts +3 -3
- data/stubs/inertia-vue-ts/app/javascript/types/global.d.ts +7 -7
- data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +8 -8
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 801eeb176444975c22103b1c30236940bc4f039cbd78247f0dd953adceb14e2a
|
4
|
+
data.tar.gz: 7a0023977ffddbcdc64c30969fcbfe7f467df905636196433eb39f0adc65dda0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e69b526a0219cb6bad9268ccf6f312fe81a020f6925988eb4eb4d7733f5264bd02167c9b4f13c96daf1dcb0bece48eef41c2fa68a27b80b2b9ad8f917f37d8a
|
7
|
+
data.tar.gz: d683e46426ef76a6356343ade5d5e6026c5a05020daa3fc599e4279e73c31c09d1b18eae029bb9d8324f89fc429c4368beedf1089d273173e74c567ee76a412e
|
data/README.md
CHANGED
@@ -2,29 +2,41 @@
|
|
2
2
|
|
3
3
|
# Kaze
|
4
4
|
|
5
|
-
Heavily inspired by [Laravel Breeze](https://github.com/laravel/breeze), this gem offers authentication and application starter kits to give you a head start building your new Rails application. These kits automatically scaffold your application with the routes, controllers, and views you need to register and authenticate your application's users.
|
5
|
+
Heavily inspired by [Laravel Breeze](https://github.com/laravel/breeze), this gem offers authentication and application starter kits to give you a head start building your new [Rails](https://rubyonrails.org) application. These kits automatically scaffold your application with the routes, controllers, and views you need to register and authenticate your application's users.
|
6
6
|
|
7
|
-
[Kaze](https://github.com/gtkvn/kaze) is a minimal, simple implementation of all
|
7
|
+
[Kaze](https://github.com/gtkvn/kaze) is a opinionated minimal, simple implementation of all authentication features that a modern web application should have, including login, registration, password reset, email verification. In addition, Kaze includes a simple "profile" page where the user may update their name, email address, and password.
|
8
8
|
|
9
9
|
Kaze provides scaffolding options based on [Hotwire](https://hotwired.dev) or [Inertia](https://inertiajs.com), with the choice of using Vue or React for the Inertia-based scaffolding.
|
10
10
|
|
11
11
|
## Installation
|
12
12
|
|
13
|
-
|
13
|
+
Before creating your first project powered by Kaze, make sure that your local machine has Ruby installed. Ruby can be installed in minutes via [mise](https://mise.jdx.dev).
|
14
14
|
|
15
|
-
|
15
|
+
```
|
16
|
+
mise use -g ruby@3.3
|
17
|
+
```
|
18
|
+
|
19
|
+
After you have installed Ruby, you may install `rails` and `kaze` gems globally:
|
16
20
|
|
17
21
|
```
|
22
|
+
gem install rails
|
18
23
|
gem install kaze
|
19
24
|
```
|
20
25
|
|
21
|
-
|
26
|
+
Then, you may create a new Rails application:
|
27
|
+
|
28
|
+
```
|
29
|
+
rails new kaze-example-app
|
30
|
+
cd kaze-example-app
|
31
|
+
```
|
32
|
+
|
33
|
+
Once the project has been created, you may scaffold your application using one of the Kaze "stacks" discussed in the documentation below.
|
22
34
|
|
23
|
-
## Kaze
|
35
|
+
## Kaze and Hotwire
|
24
36
|
|
25
37
|
The default Kaze "stack" is the Hotwire stack. Hotwire is a powerful way to building dynamic, reactive, front-end UIs primarily using Ruby and ERB templates without using much JavaScript by sending HTML instead of JSON over the wire.
|
26
38
|
|
27
|
-
The Hotwire stack may be installed by invoking the `install` command with no other additional arguments inside your app directory.
|
39
|
+
The Hotwire stack may be installed by invoking the `kaze install` command with no other additional arguments inside your app directory. This command publishes the authentication, views, routes, controllers, and other resources to your application.
|
28
40
|
|
29
41
|
```
|
30
42
|
kaze install
|
@@ -37,13 +49,13 @@ bin/setup
|
|
37
49
|
bin/dev
|
38
50
|
```
|
39
51
|
|
40
|
-
Next, you may navigate to your application's `/login` or `/register` URLs in your web browser.
|
52
|
+
Next, you may navigate to your application's `/login` or `/register` URLs in your web browser. All of Kaze's routes are defined within the `config/routes.rb` file.
|
41
53
|
|
42
|
-
## Kaze
|
54
|
+
## Kaze and React / Vue
|
43
55
|
|
44
56
|
Kaze offers React and Vue scaffolding via an Inertia frontend implementation. Inertia allows you to build modern, single-page React and Vue applications using classic server-side routing and controllers.
|
45
57
|
|
46
|
-
Inertia lets you enjoy the frontend power of React and Vue combined with the incredible backend productivity of Rails and lightning-fast Vite compilation. To use an Inertia stack, specify `react` or `vue` as your desired stack when executing the `install` command
|
58
|
+
Inertia lets you enjoy the frontend power of React and Vue combined with the incredible backend productivity of Rails and lightning-fast Vite compilation. To use an Inertia stack, specify `react` or `vue` as your desired stack when executing the `kaze install` command:
|
47
59
|
|
48
60
|
```
|
49
61
|
kaze install react
|
@@ -60,4 +72,4 @@ bin/setup
|
|
60
72
|
bin/dev
|
61
73
|
```
|
62
74
|
|
63
|
-
Next, you may navigate to your application's `/login` or `/register` URLs in your web browser.
|
75
|
+
Next, you may navigate to your application's `/login` or `/register` URLs in your web browser. All of Kaze's routes are defined within the `config/routes.rb` file.
|
@@ -11,17 +11,11 @@ class Kaze::Commands::InstallCommand < Thor
|
|
11
11
|
def install(stack = 'hotwire')
|
12
12
|
return say 'Kaze must be run in a new Rails application.', :red unless File.exist?("#{Dir.pwd}/bin/rails")
|
13
13
|
|
14
|
-
if stack == 'hotwire'
|
15
|
-
return install_hotwire_stack
|
16
|
-
end
|
14
|
+
return install_hotwire_stack if stack == 'hotwire'
|
17
15
|
|
18
|
-
if stack == 'react'
|
19
|
-
return install_inertia_react_stack
|
20
|
-
end
|
16
|
+
return install_inertia_react_stack if stack == 'react'
|
21
17
|
|
22
|
-
if stack == 'vue'
|
23
|
-
return install_inertia_vue_stack
|
24
|
-
end
|
18
|
+
return install_inertia_vue_stack if stack == 'vue'
|
25
19
|
|
26
20
|
say 'Invalid stack. Supported stacks are [hotwire], [react], [vue].', :red
|
27
21
|
end
|
@@ -53,14 +47,14 @@ class Kaze::Commands::InstallCommand < Thor
|
|
53
47
|
end
|
54
48
|
|
55
49
|
def install_migrations
|
56
|
-
ensure_directory_exists("#{Dir.pwd}/db/migrate")
|
57
|
-
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/db/migrate", "#{Dir.pwd}/db/migrate")
|
58
50
|
stdin, _ = Open3.capture3("#{Dir.pwd}/bin/rails version")
|
59
51
|
versions = stdin.gsub!('Rails ', '').split('.')
|
60
52
|
railsVersion = [ versions[0], versions[1] ].join('.')
|
53
|
+
|
54
|
+
ensure_directory_exists("#{Dir.pwd}/db/migrate")
|
55
|
+
FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/db/migrate", "#{Dir.pwd}/db/migrate")
|
61
56
|
Dir.children("#{Dir.pwd}/db/migrate").each do |file|
|
62
|
-
|
63
|
-
File.write(path, File.read(path).gsub!(/ActiveRecord::Migration$/, "ActiveRecord::Migration[#{railsVersion}]"))
|
57
|
+
replace_in_file(/ActiveRecord::Migration$/, "ActiveRecord::Migration[#{railsVersion}]", "#{Dir.pwd}/db/migrate/#{file}")
|
64
58
|
end
|
65
59
|
end
|
66
60
|
|
@@ -76,15 +70,19 @@ class Kaze::Commands::InstallCommand < Thor
|
|
76
70
|
FileUtils.mkdir_p(path) unless File.directory?(path)
|
77
71
|
end
|
78
72
|
|
73
|
+
def replace_in_file(search, replace, path)
|
74
|
+
File.write(path, File.read(path).gsub!(search, replace))
|
75
|
+
end
|
76
|
+
|
79
77
|
def run_command(command)
|
80
78
|
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
81
79
|
stdout_thread = Thread.new do
|
82
|
-
while
|
80
|
+
while line = stdout.gets do
|
83
81
|
say line
|
84
82
|
end
|
85
83
|
end
|
86
84
|
stderr_thread = Thread.new do
|
87
|
-
while
|
85
|
+
while line = stderr.gets do
|
88
86
|
say line, :red
|
89
87
|
end
|
90
88
|
end
|
@@ -60,7 +60,7 @@ module Kaze::Commands::InstallsInertiaStacks
|
|
60
60
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
|
61
61
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
|
62
62
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
|
63
|
-
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite:
|
63
|
+
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
|
64
64
|
run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
|
65
65
|
run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
|
66
66
|
|
@@ -140,7 +140,7 @@ module Kaze::Commands::InstallsInertiaStacks
|
|
140
140
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
|
141
141
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-common/bin/vite", "#{Dir.pwd}/bin/vite")
|
142
142
|
FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
|
143
|
-
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite:
|
143
|
+
File.write("#{Dir.pwd}/Procfile.dev", "#{File.read("#{Dir.pwd}/Procfile.dev")}\nvite: bin/vite dev\n")
|
144
144
|
run_command("#{Dir.pwd}/bin/rails generate js_routes:middleware")
|
145
145
|
run_command("#{Dir.pwd}/bin/rails tailwindcss:build")
|
146
146
|
|
data/lib/kaze/version.rb
CHANGED
@@ -3,4 +3,12 @@ class UpdateProfileInformationForm < ApplicationForm
|
|
3
3
|
|
4
4
|
validates :name, presence: true
|
5
5
|
validates :email, presence: true, lowercase: true, email: true, uniqueness: { model: User, attribute: :email, conditions: -> { where.not(id: Current.auth.user.id) } }
|
6
|
+
|
7
|
+
def update
|
8
|
+
Current.auth.user.name = name
|
9
|
+
Current.auth.user.email = email
|
10
|
+
Current.auth.user.email_verified_at = nil if Current.auth.user.changed.include?('email')
|
11
|
+
|
12
|
+
Current.auth.user.save
|
13
|
+
end
|
6
14
|
end
|
@@ -16,7 +16,7 @@ Rails.application.routes.draw do
|
|
16
16
|
post 'reset-password', to: 'auth/new_password#create', as: :password_store
|
17
17
|
|
18
18
|
get 'verify-email', to: 'auth/email_verification_notification#new', as: :verification_notice
|
19
|
-
post 'email
|
19
|
+
post 'verify-email', to: 'auth/email_verification_notification#create', as: :verification_send
|
20
20
|
get 'verify-email/:id/:hash', to: 'auth/verified_email#create', as: :verification_verify
|
21
21
|
|
22
22
|
post 'logout', to: 'auth/authenticated_session#destroy', as: :logout
|
@@ -1,11 +1,13 @@
|
|
1
1
|
class CreateUsers < ActiveRecord::Migration
|
2
2
|
def self.up
|
3
3
|
create_table :users do |table|
|
4
|
-
table.string :name
|
5
|
-
table.string :email
|
6
|
-
table.timestamp :email_verified_at
|
7
|
-
table.string :password_digest
|
8
|
-
table.string :remember_token
|
4
|
+
table.string :name, null: false
|
5
|
+
table.string :email, null: false
|
6
|
+
table.timestamp :email_verified_at
|
7
|
+
table.string :password_digest, null: false
|
8
|
+
table.string :remember_token
|
9
|
+
table.integer :current_team_id
|
10
|
+
table.string :profile_photo_path, limit: 2048
|
9
11
|
table.timestamps
|
10
12
|
end
|
11
13
|
end
|
@@ -25,6 +25,18 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
25
25
|
assert_equal 'test@example.com', user.email
|
26
26
|
end
|
27
27
|
|
28
|
+
test 'email verification status is unchanged when the email address is unchanged' do
|
29
|
+
user = FactoryBot.create(:user)
|
30
|
+
|
31
|
+
acting_as(user).patch profile_edit_path, params: {
|
32
|
+
name: 'Test User',
|
33
|
+
email: user.email
|
34
|
+
}
|
35
|
+
|
36
|
+
assert_redirected_to profile_edit_path
|
37
|
+
assert_not user.reload.email_verified_at.blank?
|
38
|
+
end
|
39
|
+
|
28
40
|
test 'user can delete their account' do
|
29
41
|
user = FactoryBot.create(:user)
|
30
42
|
|
@@ -8,7 +8,7 @@ class ModalComponent < ViewComponent::Base
|
|
8
8
|
lg: 'sm:max-w-lg',
|
9
9
|
xl: 'sm:max-w-xl',
|
10
10
|
'2xl': 'sm:max-w-2xl'
|
11
|
-
}[attributes[:max_width] || '2xl']
|
11
|
+
}[(attributes[:max_width] || '2xl').to_sym]
|
12
12
|
@attributes = attributes.without(:name, :show, :max_width)
|
13
13
|
end
|
14
14
|
end
|
@@ -3,7 +3,7 @@ module ValidateSignature
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
before_action do
|
6
|
-
render file: "#{Rails.root}/public/404.html",
|
6
|
+
render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless has_valid_signature?
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
@@ -1,4 +1,6 @@
|
|
1
1
|
class ProfileController < ApplicationController
|
2
|
+
skip_ensure_email_is_verified
|
3
|
+
|
2
4
|
def edit
|
3
5
|
@update_profile_information_form = UpdateProfileInformationForm.new(name: Current.auth.user.name, email: Current.auth.user.email)
|
4
6
|
@update_password_form = UpdatePasswordForm.new
|
@@ -12,7 +14,7 @@ class ProfileController < ApplicationController
|
|
12
14
|
|
13
15
|
return render partial: 'profile/partials/update_profile_information_form', status: :unprocessable_entity if @update_profile_information_form.invalid?
|
14
16
|
|
15
|
-
|
17
|
+
@update_profile_information_form.update
|
16
18
|
|
17
19
|
redirect_to profile_edit_path, flash: { status: 'profile-updated' }
|
18
20
|
end
|
@@ -8,6 +8,9 @@
|
|
8
8
|
Update your account's profile information and email address.
|
9
9
|
</p>
|
10
10
|
</header>
|
11
|
+
<form id="send-verification" method="post" action="<%= verification_send_path %>">
|
12
|
+
<input type="hidden" name="authenticity_token" value="<%= form_authenticity_token %>">
|
13
|
+
</form>
|
11
14
|
<%= form_with model: @update_profile_information_form, url: profile_update_path, method: "patch", class: "mt-6 space-y-6" do %>
|
12
15
|
<!-- Name -->
|
13
16
|
<div>
|
@@ -20,6 +23,21 @@
|
|
20
23
|
<%= render(InputLabelComponent.new({ for: "email", value: "Email" })) %>
|
21
24
|
<%= render(TextInputComponent.new({ id: "email", class: "block mt-1 w-full", type: "email", name: "email", value: @update_profile_information_form.email, required: true, autocomplete: "username" })) %>
|
22
25
|
<%= render(InputErrorComponent.new({ class: "mt-2", message: @update_profile_information_form.error_messages[:email] })) %>
|
26
|
+
<% if User.include?(MustVerifyEmail) && !Current.auth.user.has_verified_email? %>
|
27
|
+
<div>
|
28
|
+
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
29
|
+
Your email address is unverified.
|
30
|
+
<button form="send-verification" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
|
31
|
+
Click here to re-send the verification email.
|
32
|
+
</button>
|
33
|
+
</p>
|
34
|
+
<% if flash[:status] == 'verification-link-sent' %>
|
35
|
+
<p class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
36
|
+
A new verification link has been sent to your email address.
|
37
|
+
</p>
|
38
|
+
<% end %>
|
39
|
+
</div>
|
40
|
+
<% end %>
|
23
41
|
</div>
|
24
42
|
<div class="flex items-center gap-4">
|
25
43
|
<%= render(PrimaryButtonComponent.new) do %>
|
data/stubs/inertia-common/app/controllers/auth/email_verification_notification_controller.rb
CHANGED
@@ -16,6 +16,6 @@ class Auth::EmailVerificationNotificationController < ApplicationController
|
|
16
16
|
|
17
17
|
Current.auth.user.send_email_verification_notification
|
18
18
|
|
19
|
-
redirect_back_or_to
|
19
|
+
redirect_back_or_to verification_notice_path, flash: { status: 'verification-link-sent' }
|
20
20
|
end
|
21
21
|
end
|
@@ -3,7 +3,7 @@ module ValidateSignature
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
before_action do
|
6
|
-
render file: "#{Rails.root}/public/404.html",
|
6
|
+
render file: "#{Rails.root}/public/404.html", layout: false, status: :not_found unless has_valid_signature?
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
@@ -1,7 +1,10 @@
|
|
1
1
|
class ProfileController < ApplicationController
|
2
|
+
skip_ensure_email_is_verified
|
3
|
+
|
2
4
|
def edit
|
3
5
|
render inertia: 'Profile/Edit', props: {
|
4
|
-
|
6
|
+
mustVerifyEmail: User.include?(MustVerifyEmail),
|
7
|
+
status: flash[:status]
|
5
8
|
}
|
6
9
|
end
|
7
10
|
|
@@ -10,7 +13,7 @@ class ProfileController < ApplicationController
|
|
10
13
|
|
11
14
|
return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
|
12
15
|
|
13
|
-
|
16
|
+
form.update
|
14
17
|
|
15
18
|
redirect_to profile_edit_path
|
16
19
|
end
|
@@ -25,6 +25,18 @@ class ProfileTest < ActionDispatch::IntegrationTest
|
|
25
25
|
assert_equal 'test@example.com', user.email
|
26
26
|
end
|
27
27
|
|
28
|
+
test 'email verification status is unchanged when the email address is unchanged' do
|
29
|
+
user = FactoryBot.create(:user)
|
30
|
+
|
31
|
+
acting_as(user).patch profile_edit_path, params: {
|
32
|
+
name: 'Test User',
|
33
|
+
email: user.email
|
34
|
+
}
|
35
|
+
|
36
|
+
assert_redirected_to profile_edit_path
|
37
|
+
assert_not user.reload.email_verified_at.blank?
|
38
|
+
end
|
39
|
+
|
28
40
|
test 'user can delete their account' do
|
29
41
|
user = FactoryBot.create(:user)
|
30
42
|
|
@@ -21,12 +21,12 @@ export default function ForgotPassword({ status }: { status?: string }) {
|
|
21
21
|
<GuestLayout>
|
22
22
|
<Head title="Forgot Password" />
|
23
23
|
|
24
|
-
<div className="mb-4 text-sm text-gray-600">
|
24
|
+
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
25
25
|
Forgot your password? No problem. Just let us know your email address and we will email you a password reset
|
26
26
|
link that will allow you to choose a new one.
|
27
27
|
</div>
|
28
28
|
|
29
|
-
{status && <div className="mb-4 font-medium text-sm text-green-600">{status}</div>}
|
29
|
+
{status && <div className="mb-4 font-medium text-sm text-green-600 dark:text-green-400">{status}</div>}
|
30
30
|
|
31
31
|
<form onSubmit={submit}>
|
32
32
|
<TextInput
|
@@ -70,14 +70,14 @@ export default function Login({ status }: { status?: string }) {
|
|
70
70
|
<div className="block mt-4">
|
71
71
|
<label className="flex items-center">
|
72
72
|
<Checkbox name="remember" checked={data.remember} onChange={(e) => setData('remember', e.target.checked)} />
|
73
|
-
<span className="ms-2 text-sm text-gray-600">Remember me</span>
|
73
|
+
<span className="ms-2 text-sm text-gray-600 dark:text-gray-400">Remember me</span>
|
74
74
|
</label>
|
75
75
|
</div>
|
76
76
|
|
77
77
|
<div className="flex items-center justify-end mt-4">
|
78
78
|
<Link
|
79
79
|
href={password_request_path()}
|
80
|
-
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
80
|
+
className="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
|
81
81
|
>
|
82
82
|
Forgot your password?
|
83
83
|
</Link>
|
@@ -103,7 +103,7 @@ export default function Register() {
|
|
103
103
|
<div className="flex items-center justify-end mt-4">
|
104
104
|
<Link
|
105
105
|
href={login_path()}
|
106
|
-
className="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
106
|
+
className="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
|
107
107
|
>
|
108
108
|
Already registered?
|
109
109
|
</Link>
|
@@ -5,25 +5,29 @@ import UpdateProfileInformationForm from './Partials/UpdateProfileInformationFor
|
|
5
5
|
import { Head } from '@inertiajs/react'
|
6
6
|
import { PageProps } from '@/types'
|
7
7
|
|
8
|
-
export default function Edit({
|
8
|
+
export default function Edit({
|
9
|
+
auth,
|
10
|
+
mustVerifyEmail,
|
11
|
+
status,
|
12
|
+
}: PageProps<{ mustVerifyEmail: boolean; status?: string }>) {
|
9
13
|
return (
|
10
14
|
<AuthenticatedLayout
|
11
15
|
user={auth.user}
|
12
|
-
header={<h2 className="font-semibold text-xl text-gray-800 leading-tight">Profile</h2>}
|
16
|
+
header={<h2 className="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">Profile</h2>}
|
13
17
|
>
|
14
18
|
<Head title="Profile" />
|
15
19
|
|
16
20
|
<div className="py-12">
|
17
21
|
<div className="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
|
18
|
-
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
19
|
-
<UpdateProfileInformationForm className="max-w-xl" />
|
22
|
+
<div className="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
23
|
+
<UpdateProfileInformationForm mustVerifyEmail={mustVerifyEmail} status={status} className="max-w-xl" />
|
20
24
|
</div>
|
21
25
|
|
22
|
-
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
26
|
+
<div className="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
23
27
|
<UpdatePasswordForm className="max-w-xl" />
|
24
28
|
</div>
|
25
29
|
|
26
|
-
<div className="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
|
30
|
+
<div className="p-4 sm:p-8 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
27
31
|
<DeleteUserForm className="max-w-xl" />
|
28
32
|
</div>
|
29
33
|
</div>
|
@@ -47,9 +47,9 @@ export default function DeleteUserForm({ className = '' }: { className?: string
|
|
47
47
|
return (
|
48
48
|
<section className={`space-y-6 ${className}`}>
|
49
49
|
<header>
|
50
|
-
<h2 className="text-lg font-medium text-gray-900">Delete Account</h2>
|
50
|
+
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Delete Account</h2>
|
51
51
|
|
52
|
-
<p className="mt-1 text-sm text-gray-600">
|
52
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
53
53
|
Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your
|
54
54
|
account, please download any data or information that you wish to retain.
|
55
55
|
</p>
|
@@ -59,9 +59,11 @@ export default function DeleteUserForm({ className = '' }: { className?: string
|
|
59
59
|
|
60
60
|
<Modal show={confirmingUserDeletion} onClose={closeModal}>
|
61
61
|
<form onSubmit={deleteUser} className="p-6">
|
62
|
-
<h2 className="text-lg font-medium text-gray-900">
|
62
|
+
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">
|
63
|
+
Are you sure you want to delete your account?
|
64
|
+
</h2>
|
63
65
|
|
64
|
-
<p className="mt-1 text-sm text-gray-600">
|
66
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
65
67
|
Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your
|
66
68
|
password to confirm you would like to permanently delete your account.
|
67
69
|
</p>
|
@@ -40,9 +40,9 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
|
|
40
40
|
return (
|
41
41
|
<section className={className}>
|
42
42
|
<header>
|
43
|
-
<h2 className="text-lg font-medium text-gray-900">Update Password</h2>
|
43
|
+
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Update Password</h2>
|
44
44
|
|
45
|
-
<p className="mt-1 text-sm text-gray-600">
|
45
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
46
46
|
Ensure your account is using a long, random password to stay secure.
|
47
47
|
</p>
|
48
48
|
</header>
|
@@ -105,7 +105,7 @@ export default function UpdatePasswordForm({ className = '' }: { className?: str
|
|
105
105
|
leave="transition ease-in-out"
|
106
106
|
leaveTo="opacity-0"
|
107
107
|
>
|
108
|
-
<p className="text-sm text-gray-600">Saved.</p>
|
108
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
|
109
109
|
</Transition>
|
110
110
|
</div>
|
111
111
|
</form>
|
data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx
CHANGED
@@ -2,13 +2,21 @@ import InputError from '@/Components/InputError'
|
|
2
2
|
import InputLabel from '@/Components/InputLabel'
|
3
3
|
import PrimaryButton from '@/Components/PrimaryButton'
|
4
4
|
import TextInput from '@/Components/TextInput'
|
5
|
-
import { useForm, usePage } from '@inertiajs/react'
|
5
|
+
import { Link, useForm, usePage } from '@inertiajs/react'
|
6
6
|
import { Transition } from '@headlessui/react'
|
7
7
|
import { FormEventHandler } from 'react'
|
8
8
|
import { PageProps } from '@/types'
|
9
|
-
import { profile_update_path } from '@/routes'
|
9
|
+
import { profile_update_path, verification_send_path } from '@/routes'
|
10
10
|
|
11
|
-
export default function UpdateProfileInformation({
|
11
|
+
export default function UpdateProfileInformation({
|
12
|
+
mustVerifyEmail,
|
13
|
+
status,
|
14
|
+
className = '',
|
15
|
+
}: {
|
16
|
+
mustVerifyEmail: boolean
|
17
|
+
status?: string
|
18
|
+
className?: string
|
19
|
+
}) {
|
12
20
|
const user = usePage<PageProps>().props.auth.user
|
13
21
|
|
14
22
|
const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({
|
@@ -25,9 +33,11 @@ export default function UpdateProfileInformation({ className = '' }: { className
|
|
25
33
|
return (
|
26
34
|
<section className={className}>
|
27
35
|
<header>
|
28
|
-
<h2 className="text-lg font-medium text-gray-900">Profile Information</h2>
|
36
|
+
<h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Profile Information</h2>
|
29
37
|
|
30
|
-
<p className="mt-1 text-sm text-gray-600">
|
38
|
+
<p className="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
39
|
+
Update your account's profile information and email address.
|
40
|
+
</p>
|
31
41
|
</header>
|
32
42
|
|
33
43
|
<form onSubmit={submit} className="mt-6 space-y-6">
|
@@ -63,6 +73,28 @@ export default function UpdateProfileInformation({ className = '' }: { className
|
|
63
73
|
<InputError className="mt-2" message={errors.email} />
|
64
74
|
</div>
|
65
75
|
|
76
|
+
{mustVerifyEmail && user.email_verified_at === null && (
|
77
|
+
<div>
|
78
|
+
<p className="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
79
|
+
Your email address is unverified.
|
80
|
+
<Link
|
81
|
+
href={verification_send_path()}
|
82
|
+
method="post"
|
83
|
+
as="button"
|
84
|
+
className="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
|
85
|
+
>
|
86
|
+
Click here to re-send the verification email.
|
87
|
+
</Link>
|
88
|
+
</p>
|
89
|
+
|
90
|
+
{status === 'verification-link-sent' && (
|
91
|
+
<div className="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
|
92
|
+
A new verification link has been sent to your email address.
|
93
|
+
</div>
|
94
|
+
)}
|
95
|
+
</div>
|
96
|
+
)}
|
97
|
+
|
66
98
|
<div className="flex items-center gap-4">
|
67
99
|
<PrimaryButton disabled={processing}>Save</PrimaryButton>
|
68
100
|
|
@@ -73,7 +105,7 @@ export default function UpdateProfileInformation({ className = '' }: { className
|
|
73
105
|
leave="transition ease-in-out"
|
74
106
|
leaveTo="opacity-0"
|
75
107
|
>
|
76
|
-
<p className="text-sm text-gray-600">Saved.</p>
|
108
|
+
<p className="text-sm text-gray-600 dark:text-gray-400">Saved.</p>
|
77
109
|
</Transition>
|
78
110
|
</div>
|
79
111
|
</form>
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import axios from 'axios'
|
2
|
-
window.axios = axios
|
1
|
+
import axios from 'axios'
|
2
|
+
window.axios = axios
|
3
3
|
|
4
|
-
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
4
|
+
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
@@ -1,12 +1,12 @@
|
|
1
1
|
export interface User {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
id: number
|
3
|
+
name: string
|
4
|
+
email: string
|
5
|
+
email_verified_at: string
|
6
6
|
}
|
7
7
|
|
8
8
|
export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
}
|
9
|
+
auth: {
|
10
|
+
user: User
|
11
|
+
}
|
12
|
+
}
|
@@ -8,13 +8,11 @@ import { Head, useForm } from '@inertiajs/vue3'
|
|
8
8
|
import { password_store_path } from '@/routes'
|
9
9
|
|
10
10
|
const props = defineProps<{
|
11
|
-
email: string
|
12
11
|
token: string
|
13
12
|
}>()
|
14
13
|
|
15
14
|
const form = useForm({
|
16
15
|
token: props.token,
|
17
|
-
email: props.email,
|
18
16
|
password: '',
|
19
17
|
password_confirmation: '',
|
20
18
|
})
|
@@ -34,22 +32,6 @@ const submit = () => {
|
|
34
32
|
|
35
33
|
<form @submit.prevent="submit">
|
36
34
|
<div>
|
37
|
-
<InputLabel for="email" value="Email" />
|
38
|
-
|
39
|
-
<TextInput
|
40
|
-
id="email"
|
41
|
-
type="email"
|
42
|
-
class="mt-1 block w-full"
|
43
|
-
v-model="form.email"
|
44
|
-
required
|
45
|
-
autofocus
|
46
|
-
autocomplete="username"
|
47
|
-
/>
|
48
|
-
|
49
|
-
<InputError class="mt-2" :message="form.errors.email" />
|
50
|
-
</div>
|
51
|
-
|
52
|
-
<div class="mt-4">
|
53
35
|
<InputLabel for="password" value="Password" />
|
54
36
|
|
55
37
|
<TextInput
|
@@ -58,6 +40,7 @@ const submit = () => {
|
|
58
40
|
class="mt-1 block w-full"
|
59
41
|
v-model="form.password"
|
60
42
|
required
|
43
|
+
autofocus
|
61
44
|
autocomplete="new-password"
|
62
45
|
/>
|
63
46
|
|
data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue
CHANGED
@@ -3,8 +3,8 @@ import InputError from '@/Components/InputError.vue'
|
|
3
3
|
import InputLabel from '@/Components/InputLabel.vue'
|
4
4
|
import PrimaryButton from '@/Components/PrimaryButton.vue'
|
5
5
|
import TextInput from '@/Components/TextInput.vue'
|
6
|
-
import { useForm, usePage } from '@inertiajs/vue3'
|
7
|
-
import { profile_update_path } from '@/routes'
|
6
|
+
import { Link, useForm, usePage } from '@inertiajs/vue3'
|
7
|
+
import { profile_update_path, verification_send_path } from '@/routes'
|
8
8
|
|
9
9
|
defineProps<{
|
10
10
|
mustVerifyEmail?: Boolean
|
@@ -61,6 +61,27 @@ const form = useForm({
|
|
61
61
|
<InputError class="mt-2" :message="form.errors.email" />
|
62
62
|
</div>
|
63
63
|
|
64
|
+
<div v-if="mustVerifyEmail && user.email_verified_at === null">
|
65
|
+
<p class="text-sm mt-2 text-gray-800 dark:text-gray-200">
|
66
|
+
Your email address is unverified.
|
67
|
+
<Link
|
68
|
+
:href="verification_send_path()"
|
69
|
+
method="post"
|
70
|
+
as="button"
|
71
|
+
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
|
72
|
+
>
|
73
|
+
Click here to re-send the verification email.
|
74
|
+
</Link>
|
75
|
+
</p>
|
76
|
+
|
77
|
+
<div
|
78
|
+
v-show="status === 'verification-link-sent'"
|
79
|
+
class="mt-2 font-medium text-sm text-green-600 dark:text-green-400"
|
80
|
+
>
|
81
|
+
A new verification link has been sent to your email address.
|
82
|
+
</div>
|
83
|
+
</div>
|
84
|
+
|
64
85
|
<div class="flex items-center gap-4">
|
65
86
|
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
|
66
87
|
|
@@ -1,34 +1,38 @@
|
|
1
1
|
import './bootstrap'
|
2
2
|
import '../../assets/builds/tailwind.css'
|
3
3
|
|
4
|
-
import { createApp, h, DefineComponent } from 'vue'
|
5
|
-
import { createInertiaApp } from '@inertiajs/vue3'
|
4
|
+
import { createApp, h, DefineComponent } from 'vue'
|
5
|
+
import { createInertiaApp } from '@inertiajs/vue3'
|
6
6
|
|
7
|
-
async function resolvePageComponent<T>(
|
8
|
-
|
9
|
-
|
7
|
+
async function resolvePageComponent<T>(
|
8
|
+
path: string | string[],
|
9
|
+
pages: Record<string, Promise<T> | (() => Promise<T>)>,
|
10
|
+
): Promise<T> {
|
11
|
+
for (const p of Array.isArray(path) ? path : [path]) {
|
12
|
+
const page = pages[p]
|
10
13
|
|
11
|
-
|
12
|
-
|
13
|
-
}
|
14
|
-
|
15
|
-
return typeof page === 'function' ? page() : page
|
14
|
+
if (typeof page === 'undefined') {
|
15
|
+
continue
|
16
16
|
}
|
17
17
|
|
18
|
-
|
18
|
+
return typeof page === 'function' ? page() : page
|
19
|
+
}
|
20
|
+
|
21
|
+
throw new Error(`Page not found: ${path}`)
|
19
22
|
}
|
20
23
|
|
21
|
-
const appName = import.meta.env.VITE_APP_NAME || 'Rails'
|
24
|
+
const appName = import.meta.env.VITE_APP_NAME || 'Rails'
|
22
25
|
|
23
26
|
createInertiaApp({
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
}
|
27
|
+
title: (title) => `${title} - ${appName}`,
|
28
|
+
resolve: (name) =>
|
29
|
+
resolvePageComponent(`../Pages/${name}.vue`, import.meta.glob<DefineComponent>('../Pages/**/*.vue')),
|
30
|
+
setup({ el, App, props, plugin }) {
|
31
|
+
createApp({ render: () => h(App, props) })
|
32
|
+
.use(plugin)
|
33
|
+
.mount(el)
|
34
|
+
},
|
35
|
+
progress: {
|
36
|
+
color: '#4B5563',
|
37
|
+
},
|
38
|
+
})
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import axios from 'axios'
|
2
|
-
window.axios = axios
|
1
|
+
import axios from 'axios'
|
2
|
+
window.axios = axios
|
3
3
|
|
4
|
-
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
4
|
+
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
@@ -1,13 +1,13 @@
|
|
1
|
-
import { PageProps as InertiaPageProps } from '@inertiajs/core'
|
2
|
-
import { AxiosInstance } from 'axios'
|
3
|
-
import { PageProps as AppPageProps } from './'
|
1
|
+
import { PageProps as InertiaPageProps } from '@inertiajs/core'
|
2
|
+
import { AxiosInstance } from 'axios'
|
3
|
+
import { PageProps as AppPageProps } from './'
|
4
4
|
|
5
5
|
declare global {
|
6
|
-
|
7
|
-
|
8
|
-
|
6
|
+
interface Window {
|
7
|
+
axios: AxiosInstance
|
8
|
+
}
|
9
9
|
}
|
10
10
|
|
11
11
|
declare module '@inertiajs/core' {
|
12
|
-
|
12
|
+
interface PageProps extends InertiaPageProps, AppPageProps {}
|
13
13
|
}
|
@@ -1,12 +1,12 @@
|
|
1
1
|
export interface User {
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
2
|
+
id: number
|
3
|
+
name: string
|
4
|
+
email: string
|
5
|
+
email_verified_at: string
|
6
6
|
}
|
7
7
|
|
8
8
|
export type PageProps<T extends Record<string, unknown> = Record<string, unknown>> = T & {
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
}
|
9
|
+
auth: {
|
10
|
+
user: User
|
11
|
+
}
|
12
|
+
}
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kaze
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cuong Giang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-06-
|
11
|
+
date: 2024-06-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -260,7 +260,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
260
260
|
requirements:
|
261
261
|
- - ">="
|
262
262
|
- !ruby/object:Gem::Version
|
263
|
-
version:
|
263
|
+
version: 3.1.0
|
264
264
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
265
265
|
requirements:
|
266
266
|
- - ">="
|