kaze 0.2.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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +42 -0
  4. data/bin/kaze +11 -0
  5. data/lib/kaze/commands/install_command.rb +63 -0
  6. data/lib/kaze/commands/install_inertia_stacks.rb +151 -0
  7. data/lib/kaze/commands.rb +2 -0
  8. data/lib/kaze/version.rb +3 -0
  9. data/lib/kaze.rb +9 -0
  10. data/stubs/default/Procfile.dev +3 -0
  11. data/stubs/default/app/assets/stylesheets/application.css +1 -0
  12. data/stubs/default/app/assets/stylesheets/application.tailwind.css +3 -0
  13. data/stubs/default/app/controllers/application_controller.rb +5 -0
  14. data/stubs/default/app/controllers/auth/authenticated_session_controller.rb +28 -0
  15. data/stubs/default/app/controllers/auth/new_password_controller.rb +18 -0
  16. data/stubs/default/app/controllers/auth/password_reset_link_controller.rb +17 -0
  17. data/stubs/default/app/controllers/auth/registered_user_controller.rb +19 -0
  18. data/stubs/default/app/controllers/concerns/authenticate.rb +34 -0
  19. data/stubs/default/app/controllers/concerns/handle_inertia_requests.rb +9 -0
  20. data/stubs/default/app/controllers/concerns/verify_csrf_token.rb +24 -0
  21. data/stubs/default/app/controllers/dashboard_controller.rb +5 -0
  22. data/stubs/default/app/controllers/password_controller.rb +11 -0
  23. data/stubs/default/app/controllers/profile_controller.rb +31 -0
  24. data/stubs/default/app/controllers/welcome_controller.rb +10 -0
  25. data/stubs/default/app/forms/application_form.rb +9 -0
  26. data/stubs/default/app/forms/auth/login_form.rb +18 -0
  27. data/stubs/default/app/forms/auth/new_password_form.rb +21 -0
  28. data/stubs/default/app/forms/auth/register_form.rb +7 -0
  29. data/stubs/default/app/forms/auth/send_password_reset_link_form.rb +22 -0
  30. data/stubs/default/app/forms/delete_user_form.rb +5 -0
  31. data/stubs/default/app/forms/update_password_form.rb +6 -0
  32. data/stubs/default/app/forms/update_profile_information_form.rb +6 -0
  33. data/stubs/default/app/mailers/application_mailer.rb +11 -0
  34. data/stubs/default/app/mailers/user_mailer.rb +8 -0
  35. data/stubs/default/app/models/application_record.rb +3 -0
  36. data/stubs/default/app/models/concerns/can_reset_password.rb +5 -0
  37. data/stubs/default/app/models/current.rb +3 -0
  38. data/stubs/default/app/models/user.rb +11 -0
  39. data/stubs/default/app/validators/current_password_validator.rb +5 -0
  40. data/stubs/default/app/validators/email_validator.rb +7 -0
  41. data/stubs/default/app/validators/lowercase_validator.rb +5 -0
  42. data/stubs/default/app/validators/uniqueness_validator.rb +24 -0
  43. data/stubs/default/app/views/layouts/mailer.html.erb +374 -0
  44. data/stubs/default/app/views/layouts/mailer.text.erb +11 -0
  45. data/stubs/default/app/views/user_mailer/reset_password.html.erb +39 -0
  46. data/stubs/default/bin/dev +16 -0
  47. data/stubs/default/bin/vite +27 -0
  48. data/stubs/default/config/routes.rb +27 -0
  49. data/stubs/default/config/vite.json +16 -0
  50. data/stubs/default/db/migrate/20240101000000_create_users.rb +14 -0
  51. data/stubs/default/db/migrate/20240101000001_create_delayed_jobs.rb +22 -0
  52. data/stubs/inertia-react-ts/app/javascript/Components/ApplicationLogo.tsx +12 -0
  53. data/stubs/inertia-react-ts/app/javascript/Components/Checkbox.tsx +14 -0
  54. data/stubs/inertia-react-ts/app/javascript/Components/DangerButton.tsx +17 -0
  55. data/stubs/inertia-react-ts/app/javascript/Components/Dropdown.tsx +99 -0
  56. data/stubs/inertia-react-ts/app/javascript/Components/InputError.tsx +9 -0
  57. data/stubs/inertia-react-ts/app/javascript/Components/InputLabel.tsx +9 -0
  58. data/stubs/inertia-react-ts/app/javascript/Components/Modal.tsx +68 -0
  59. data/stubs/inertia-react-ts/app/javascript/Components/NavLink.tsx +18 -0
  60. data/stubs/inertia-react-ts/app/javascript/Components/PrimaryButton.tsx +17 -0
  61. data/stubs/inertia-react-ts/app/javascript/Components/ResponsiveNavLink.tsx +16 -0
  62. data/stubs/inertia-react-ts/app/javascript/Components/SecondaryButton.tsx +18 -0
  63. data/stubs/inertia-react-ts/app/javascript/Components/TextInput.tsx +30 -0
  64. data/stubs/inertia-react-ts/app/javascript/Layouts/AuthenticatedLayout.tsx +131 -0
  65. data/stubs/inertia-react-ts/app/javascript/Layouts/GuestLayout.tsx +19 -0
  66. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ForgotPassword.tsx +52 -0
  67. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Login.tsx +98 -0
  68. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/Register.tsx +118 -0
  69. data/stubs/inertia-react-ts/app/javascript/Pages/Auth/ResetPassword.tsx +74 -0
  70. data/stubs/inertia-react-ts/app/javascript/Pages/Dashboard.tsx +22 -0
  71. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Edit.tsx +33 -0
  72. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.tsx +100 -0
  73. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.tsx +114 -0
  74. data/stubs/inertia-react-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.tsx +84 -0
  75. data/stubs/inertia-react-ts/app/javascript/Pages/Welcome.tsx +66 -0
  76. data/stubs/inertia-react-ts/app/javascript/entrypoints/application.tsx +34 -0
  77. data/stubs/inertia-react-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
  78. data/stubs/inertia-react-ts/app/javascript/types/global.d.ts +7 -0
  79. data/stubs/inertia-react-ts/app/javascript/types/index.d.ts +12 -0
  80. data/stubs/inertia-react-ts/app/javascript/types/vite-env.d.ts +1 -0
  81. data/stubs/inertia-react-ts/app/views/layouts/application.html.erb +26 -0
  82. data/stubs/inertia-react-ts/config/tailwind.config.js +22 -0
  83. data/stubs/inertia-react-ts/package.json +26 -0
  84. data/stubs/inertia-react-ts/tsconfig.json +19 -0
  85. data/stubs/inertia-react-ts/vite.config.ts +13 -0
  86. data/stubs/inertia-vue-ts/app/javascript/Components/ApplicationLogo.vue +8 -0
  87. data/stubs/inertia-vue-ts/app/javascript/Components/Checkbox.vue +29 -0
  88. data/stubs/inertia-vue-ts/app/javascript/Components/DangerButton.vue +7 -0
  89. data/stubs/inertia-vue-ts/app/javascript/Components/Dropdown.vue +75 -0
  90. data/stubs/inertia-vue-ts/app/javascript/Components/DropdownLink.vue +16 -0
  91. data/stubs/inertia-vue-ts/app/javascript/Components/InputError.vue +13 -0
  92. data/stubs/inertia-vue-ts/app/javascript/Components/InputLabel.vue +12 -0
  93. data/stubs/inertia-vue-ts/app/javascript/Components/Modal.vue +96 -0
  94. data/stubs/inertia-vue-ts/app/javascript/Components/NavLink.vue +21 -0
  95. data/stubs/inertia-vue-ts/app/javascript/Components/PrimaryButton.vue +7 -0
  96. data/stubs/inertia-vue-ts/app/javascript/Components/ResponsiveNavLink.vue +21 -0
  97. data/stubs/inertia-vue-ts/app/javascript/Components/SecondaryButton.vue +19 -0
  98. data/stubs/inertia-vue-ts/app/javascript/Components/TextInput.vue +23 -0
  99. data/stubs/inertia-vue-ts/app/javascript/Layouts/AuthenticatedLayout.vue +155 -0
  100. data/stubs/inertia-vue-ts/app/javascript/Layouts/GuestLayout.vue +20 -0
  101. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ForgotPassword.vue +60 -0
  102. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Login.vue +93 -0
  103. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/Register.vue +106 -0
  104. data/stubs/inertia-vue-ts/app/javascript/Pages/Auth/ResetPassword.vue +89 -0
  105. data/stubs/inertia-vue-ts/app/javascript/Pages/Dashboard.vue +22 -0
  106. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Edit.vue +42 -0
  107. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/DeleteUserForm.vue +98 -0
  108. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdatePasswordForm.vue +108 -0
  109. data/stubs/inertia-vue-ts/app/javascript/Pages/Profile/Partials/UpdateProfileInformationForm.vue +78 -0
  110. data/stubs/inertia-vue-ts/app/javascript/Pages/Welcome.vue +56 -0
  111. data/stubs/inertia-vue-ts/app/javascript/entrypoints/application.ts +34 -0
  112. data/stubs/inertia-vue-ts/app/javascript/entrypoints/bootstrap.ts +4 -0
  113. data/stubs/inertia-vue-ts/app/javascript/types/global.d.ts +13 -0
  114. data/stubs/inertia-vue-ts/app/javascript/types/index.d.ts +12 -0
  115. data/stubs/inertia-vue-ts/app/javascript/types/vite-env.d.ts +1 -0
  116. data/stubs/inertia-vue-ts/app/views/layouts/application.html.erb +25 -0
  117. data/stubs/inertia-vue-ts/config/tailwind.config.js +22 -0
  118. data/stubs/inertia-vue-ts/package.json +24 -0
  119. data/stubs/inertia-vue-ts/tsconfig.json +19 -0
  120. data/stubs/inertia-vue-ts/vite.config.ts +13 -0
  121. metadata +205 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fa07032d843778439756cd1f5440d066f46e368ca699aadd2c96559721e9456d
4
+ data.tar.gz: e9a408babe88ac76f5d524b8ed4e35213eb2407a0135c422dfd1c0ee29ae5438
5
+ SHA512:
6
+ metadata.gz: 5e6ca9927646f4aaca149938b413af65d8257f65a0883fe5dff23512f10829e77c865ba08295edca37c8293273443aaf75e60c2cab375fb1fdf5b766c5dcc105
7
+ data.tar.gz: 8824870ef8846a2c370cf64c06def95cb657032482dcd56521e63483c895175fa921c9658efb49dbde2f23770cbd8b58be3fa34fc279c924dc0bc2d57e0aad8b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 Cuong Giang
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # Kaze
2
+
3
+ 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.
4
+
5
+ [Kaze](https://github.com/gtkvn/kaze) is a minimal, simple implementation of all of Rails's authentication features, including login, registration, password reset. In addition, Kaze includes a simple "profile" page where the user may update their name, email address, and password.
6
+
7
+ Kaze provides scaffolding options based on Inertia, with the choice of using Vue or React for the Inertia-based scaffolding.
8
+
9
+ ## Installation
10
+
11
+ First, you should create a new Rails application, configure your database, and run your database migrations.
12
+
13
+ You may install Kaze globally with:
14
+
15
+ ```
16
+ gem install kaze
17
+ ```
18
+
19
+ Once Kaze is installed, you may scaffold your application using one of the Kaze "stacks" discussed in the documentation below.
20
+
21
+ ## Kaze & React / Vue
22
+
23
+ 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.
24
+
25
+ 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 vue or react as your desired stack when executing the install command inside your app directory:
26
+
27
+ ```
28
+ kaze install react
29
+
30
+ # Or...
31
+
32
+ kaze install vue
33
+ ```
34
+
35
+ After Kaze's scaffolding is installed, you may start your application:
36
+
37
+ ```
38
+ bin/setup
39
+ bin/dev
40
+ ```
41
+
42
+ Next, you may navigate to your application's `/login` or `/register` URLs in your web browser.
data/bin/kaze ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "kaze"
4
+
5
+ begin
6
+ Kaze::Commands::InstallCommand.start(ARGV)
7
+ rescue => e
8
+ puts " \e[31mERROR (#{e.class}): #{e.message}\e[0m"
9
+ puts e.backtrace if ENV["VERBOSE"]
10
+ exit 1
11
+ end
@@ -0,0 +1,63 @@
1
+ require "bundler"
2
+ require "fileutils"
3
+ require "open3"
4
+ require "thor"
5
+
6
+ class Kaze::Commands::InstallCommand < Thor
7
+ include Kaze::Commands::InstallInertiaStacks
8
+
9
+ desc "install [STACK]", "Install the Kaze controllers and resources. Supported stacks: react, vue."
10
+ def install(stack = "hotwire")
11
+ if stack == "react"
12
+ return install_inertia_react_stack
13
+ end
14
+
15
+ if stack == "vue"
16
+ return install_inertia_vue_stack
17
+ end
18
+
19
+ say "Invalid stack. Supported stacks are [react], [vue].", :red
20
+ end
21
+
22
+ private
23
+
24
+ def require_gems(gems = [])
25
+ installed_gems = Bundler::Definition.build("#{Dir.pwd}/Gemfile", nil, {}).dependencies.map(&:name)
26
+
27
+ installing_gems = gems.map { |gem| gem unless installed_gems.include?(gem) }.compact
28
+
29
+ return true if installing_gems.empty?
30
+
31
+ status = run_command("bundle add #{installing_gems.join(" ")}")
32
+
33
+ status.success?
34
+ end
35
+
36
+ def install_migrations
37
+ ensure_directory_exists("#{Dir.pwd}/db/migrate")
38
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/db/migrate", "#{Dir.pwd}/db/migrate")
39
+ stdin, _ = Open3.capture3("rails version")
40
+ versions = stdin.gsub!("Rails ", "").split(".")
41
+ Open3.capture3('grep -rl ActiveRecord::Migration$ db | xargs sed -i "" "s/ActiveRecord::Migration/ActiveRecord::Migration[' + [ versions[0], versions[1] ].join(".") + ']/g"')
42
+ end
43
+
44
+ def ensure_directory_exists(path)
45
+ FileUtils.mkdir_p(path) unless File.directory?(path)
46
+ end
47
+
48
+ def run_command(command)
49
+ Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
50
+ Thread.new do
51
+ stdout.each { |line| say line }
52
+ end
53
+ Thread.new do
54
+ stderr.each { |line| say line, :red }
55
+ end
56
+ wait_thr.value
57
+ end
58
+ end
59
+
60
+ def run_commands(commands)
61
+ commands.each { |command| run_command(command) }
62
+ end
63
+ end
@@ -0,0 +1,151 @@
1
+ module Kaze::Commands::InstallInertiaStacks
2
+ private
3
+
4
+ def install_inertia_react_stack
5
+ # Install Inertia...
6
+ return unless require_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
7
+
8
+ # NPM Packages...
9
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/package.json", "#{Dir.pwd}/package.json")
10
+
11
+ # Controllers...
12
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/controllers", "#{Dir.pwd}/app/controllers")
13
+
14
+ # Models...
15
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/models", "#{Dir.pwd}/app/models")
16
+
17
+ # Forms...
18
+ ensure_directory_exists("#{Dir.pwd}/app/forms")
19
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/forms", "#{Dir.pwd}/app/forms")
20
+
21
+ # Validators...
22
+ ensure_directory_exists("#{Dir.pwd}/app/validators")
23
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/validators", "#{Dir.pwd}/app/validators")
24
+
25
+ # Views...
26
+ ensure_directory_exists("#{Dir.pwd}/app/views/layouts")
27
+ ensure_directory_exists("#{Dir.pwd}/app/views/user_mailer")
28
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/app/views/layouts/application.html.erb", "#{Dir.pwd}/app/views/layouts/application.html.erb")
29
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
30
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
31
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
32
+
33
+ # Mailers...
34
+ ensure_directory_exists("#{Dir.pwd}/app/mailers")
35
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/mailers", "#{Dir.pwd}/app/mailers")
36
+
37
+ # Components + Pages...
38
+ ensure_directory_exists("#{Dir.pwd}/app/javascript")
39
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/app/javascript", "#{Dir.pwd}/app/javascript")
40
+
41
+ # Tests...
42
+
43
+ # Routes...
44
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/routes.rb", "#{Dir.pwd}/config/routes.rb")
45
+
46
+ # Migrations...
47
+ install_migrations
48
+
49
+ # Tailwind / Vite...
50
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.css", "#{Dir.pwd}/app/assets/stylesheets/application.css")
51
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.tailwind.css", "#{Dir.pwd}/app/assets/stylesheets/application.tailwind.css")
52
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
53
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/vite.json", "#{Dir.pwd}/config/vite.json")
54
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/tsconfig.json", "#{Dir.pwd}/tsconfig.json")
55
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-react-ts/vite.config.ts", "#{Dir.pwd}/vite.config.ts")
56
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
57
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/vite", "#{Dir.pwd}/bin/vite")
58
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
59
+ run_command("rails generate js_routes:middleware")
60
+
61
+ say ""
62
+ say "Installing and building Node dependencies.", :magenta
63
+
64
+ if File.exist?("#{Dir.pwd}/pnpm-lock.yaml")
65
+ run_commands([ "pnpm install", "pnpm run build" ])
66
+ elsif File.exist?("#{Dir.pwd}/yarn.lock")
67
+ run_commands([ "yarn install", "yarn build" ])
68
+ elsif File.exist?("#{Dir.pwd}/bun.lockb")
69
+ run_commands([ "bun install", "bun run build" ])
70
+ else
71
+ run_commands([ "npm install", "npm run build" ])
72
+ end
73
+
74
+ say ""
75
+ say "Kaze scaffolding installed successfully.", :green
76
+ end
77
+
78
+ def install_inertia_vue_stack
79
+ # Install Inertia...
80
+ return unless require_gems([ "propshaft", "tailwindcss-rails", "inertia_rails", "vite_rails", "dotenv", "bcrypt", "js-routes" ])
81
+
82
+ # NPM Packages...
83
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/package.json", "#{Dir.pwd}/package.json")
84
+
85
+ # Controllers...
86
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/controllers", "#{Dir.pwd}/app/controllers")
87
+
88
+ # Models...
89
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/models", "#{Dir.pwd}/app/models")
90
+
91
+ # Forms...
92
+ ensure_directory_exists("#{Dir.pwd}/app/forms")
93
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/forms", "#{Dir.pwd}/app/forms")
94
+
95
+ # Validators...
96
+ ensure_directory_exists("#{Dir.pwd}/app/validators")
97
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/validators", "#{Dir.pwd}/app/validators")
98
+
99
+ # Views...
100
+ ensure_directory_exists("#{Dir.pwd}/app/views/layouts")
101
+ ensure_directory_exists("#{Dir.pwd}/app/views/user_mailer")
102
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/app/views/layouts/application.html.erb", "#{Dir.pwd}/app/views/layouts/application.html.erb")
103
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.html.erb", "#{Dir.pwd}/app/views/layouts/mailer.html.erb")
104
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/layouts/mailer.text.erb", "#{Dir.pwd}/app/views/layouts/mailer.text.erb")
105
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/views/user_mailer/reset_password.html.erb", "#{Dir.pwd}/app/views/user_mailer/reset_password.html.erb")
106
+
107
+ # Mailers...
108
+ ensure_directory_exists("#{Dir.pwd}/app/mailers")
109
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/default/app/mailers", "#{Dir.pwd}/app/mailers")
110
+
111
+ # Components + Pages...
112
+ ensure_directory_exists("#{Dir.pwd}/app/javascript")
113
+ FileUtils.copy_entry("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/app/javascript", "#{Dir.pwd}/app/javascript")
114
+
115
+ # Tests...
116
+
117
+ # Routes...
118
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/routes.rb", "#{Dir.pwd}/config/routes.rb")
119
+
120
+ # Migrations...
121
+ install_migrations
122
+
123
+ # Tailwind / Vite...
124
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.css", "#{Dir.pwd}/app/assets/stylesheets/application.css")
125
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/app/assets/stylesheets/application.tailwind.css", "#{Dir.pwd}/app/assets/stylesheets/application.tailwind.css")
126
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/config/tailwind.config.js", "#{Dir.pwd}/config/tailwind.config.js")
127
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/config/vite.json", "#{Dir.pwd}/config/vite.json")
128
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/tsconfig.json", "#{Dir.pwd}/tsconfig.json")
129
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/inertia-vue-ts/vite.config.ts", "#{Dir.pwd}/vite.config.ts")
130
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/dev", "#{Dir.pwd}/bin/dev")
131
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/bin/vite", "#{Dir.pwd}/bin/vite")
132
+ FileUtils.copy_file("#{File.dirname(__FILE__)}/../../../stubs/default/Procfile.dev", "#{Dir.pwd}/Procfile.dev")
133
+ run_command("rails generate js_routes:middleware")
134
+
135
+ say ""
136
+ say "Installing and building Node dependencies.", :magenta
137
+
138
+ if File.exist?("#{Dir.pwd}/pnpm-lock.yaml")
139
+ run_commands([ "pnpm install", "pnpm run build" ])
140
+ elsif File.exist?("#{Dir.pwd}/yarn.lock")
141
+ run_commands([ "yarn install", "yarn build" ])
142
+ elsif File.exist?("#{Dir.pwd}/bun.lockb")
143
+ run_commands([ "bun install", "bun run build" ])
144
+ else
145
+ run_commands([ "npm install", "npm run build" ])
146
+ end
147
+
148
+ say ""
149
+ say "Kaze scaffolding installed successfully.", :green
150
+ end
151
+ end
@@ -0,0 +1,2 @@
1
+ module Kaze::Commands
2
+ end
@@ -0,0 +1,3 @@
1
+ module Kaze
2
+ VERSION = "0.2.0"
3
+ end
data/lib/kaze.rb ADDED
@@ -0,0 +1,9 @@
1
+ module Kaze
2
+ end
3
+
4
+ require "active_support"
5
+ require "zeitwerk"
6
+
7
+ loader = Zeitwerk::Loader.for_gem
8
+ loader.setup
9
+ loader.eager_load # We need all commands loaded.
@@ -0,0 +1,3 @@
1
+ web: bin/rails server
2
+ css: bin/rails tailwindcss:watch
3
+ vite: bin/vite dev
@@ -0,0 +1 @@
1
+ /* Application styles */
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1,5 @@
1
+ class ApplicationController < ActionController::Base
2
+ include Authenticate
3
+ include HandleInertiaRequests
4
+ include VerifyCsrfToken
5
+ end
@@ -0,0 +1,28 @@
1
+ class Auth::AuthenticatedSessionController < ApplicationController
2
+ skip_authentication only: %i[new create]
3
+
4
+ def new
5
+ render inertia: "Auth/Login", props: {
6
+ canResetPassword: true,
7
+ status: flash[:status]
8
+ }
9
+ end
10
+
11
+ def create
12
+ form = Auth::LoginForm.new params.permit(:email, :password)
13
+
14
+ user = form.authenticate
15
+
16
+ return redirect_back_or_to login_path, inertia: { errors: form.error_messages } if user.nil?
17
+
18
+ login user
19
+
20
+ redirect_to dashboard_path
21
+ end
22
+
23
+ def destroy
24
+ logout
25
+
26
+ redirect_to login_path
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ class Auth::NewPasswordController < ApplicationController
2
+ skip_authentication
3
+
4
+ def new
5
+ render inertia: "Auth/ResetPassword", props: {
6
+ token: params[:token]
7
+ }
8
+ end
9
+
10
+ def create
11
+ form = Auth::NewPasswordForm.new params.permit(:token, :password, :password_confirmation)
12
+
13
+ return redirect_to login_path, flash: { status: "Your password has been reset." } if form.reset?
14
+
15
+ redirect_back_or_to password_reset_path(token: form.token),
16
+ inertia: { errors: form.error_messages }
17
+ end
18
+ end
@@ -0,0 +1,17 @@
1
+ class Auth::PasswordResetLinkController < ApplicationController
2
+ skip_authentication
3
+
4
+ def new
5
+ render inertia: "Auth/ForgotPassword", props: {
6
+ status: flash[:status]
7
+ }
8
+ end
9
+
10
+ def create
11
+ form = Auth::SendPasswordResetLinkForm.new params.permit(:email)
12
+
13
+ return redirect_back_or_to password_request_path, inertia: { errors: form.error_messages } unless form.send_reset_link?
14
+
15
+ redirect_back_or_to password_request_path, flash: { status: "We have emailed your password reset link." }
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ class Auth::RegisteredUserController < ApplicationController
2
+ skip_authentication
3
+
4
+ def new
5
+ render inertia: "Auth/Register"
6
+ end
7
+
8
+ def create
9
+ form = Auth::RegisterForm.new params.permit(:name, :email, :password, :password_confirmation)
10
+
11
+ return redirect_to register_path, inertia: { errors: form.error_messages } if form.invalid?
12
+
13
+ user = User.create(name: form.name, email: form.email, password: form.password)
14
+
15
+ login user
16
+
17
+ redirect_to dashboard_path
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module Authenticate
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :authenticate_user!
6
+ end
7
+
8
+ class_methods do
9
+ def skip_authentication(**options)
10
+ skip_before_action :authenticate_user!, **options
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def authenticate_user!
17
+ if user = User.find_by(id: session[:user_id])
18
+ Current.user = user
19
+ else
20
+ redirect_to login_path
21
+ end
22
+ end
23
+
24
+ def login(user)
25
+ Current.user = user
26
+ reset_session
27
+ session[:user_id] = user.id
28
+ end
29
+
30
+ def logout
31
+ Current.user = nil
32
+ reset_session
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ module HandleInertiaRequests
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ inertia_share do
6
+ { auth: { user: Current.user } }
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,24 @@
1
+ module VerifyCsrfToken
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :set_csrf_cookie
6
+
7
+ rescue_from ActionController::InvalidAuthenticityToken do
8
+ redirect_back fallback_location: "/", notice: "The page expired, please try again."
9
+ end
10
+ end
11
+
12
+ def request_authenticity_tokens
13
+ super << request.headers["HTTP_X_XSRF_TOKEN"]
14
+ end
15
+
16
+ private
17
+
18
+ def set_csrf_cookie
19
+ cookies["XSRF-TOKEN"] = {
20
+ value: form_authenticity_token,
21
+ same_site: "Strict"
22
+ }
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ class DashboardController < ApplicationController
2
+ def show
3
+ render inertia: "Dashboard"
4
+ end
5
+ end
@@ -0,0 +1,11 @@
1
+ class PasswordController < ApplicationController
2
+ def update
3
+ form = UpdatePasswordForm.new params.permit(:current_password, :password, :password_confirmation)
4
+
5
+ return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
6
+
7
+ Current.user.update(password: form.password)
8
+
9
+ redirect_back_or_to profile_edit_path
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ class ProfileController < ApplicationController
2
+ def edit
3
+ render inertia: "Profile/Edit", props: {
4
+ status: session[:status]
5
+ }
6
+ end
7
+
8
+ def update
9
+ form = UpdateProfileInformationForm.new params.permit(:name, :email)
10
+
11
+ return redirect_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
12
+
13
+ Current.user.update(name: form.name, email: form.email)
14
+
15
+ redirect_to profile_edit_path
16
+ end
17
+
18
+ def destroy
19
+ form = DeleteUserForm.new params.permit(:password)
20
+
21
+ return redirect_back_or_to profile_edit_path, inertia: { errors: form.error_messages } if form.invalid?
22
+
23
+ user = Current.user
24
+
25
+ logout
26
+
27
+ user.delete
28
+
29
+ redirect_to "/"
30
+ end
31
+ end
@@ -0,0 +1,10 @@
1
+ class WelcomeController < ApplicationController
2
+ skip_authentication
3
+
4
+ def index
5
+ render inertia: "Welcome", props: {
6
+ railsVersion: Rails.version,
7
+ rubyVersion: RUBY_DESCRIPTION
8
+ }
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ class ApplicationForm
2
+ include ActiveModel::Model
3
+
4
+ def error_messages
5
+ messages = {}
6
+ errors.each { |error| messages[error.attribute] = error.message unless messages.has_key?(error.attribute) }
7
+ messages
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ class Auth::LoginForm < ApplicationForm
2
+ attr_accessor :email, :password
3
+
4
+ validates :email, presence: true, email: true
5
+ validates :password, presence: true
6
+
7
+ def authenticate
8
+ return nil if invalid?
9
+
10
+ user = User.authenticate_by(email: email, password: password)
11
+
12
+ return user if user.present?
13
+
14
+ errors.add(:email, message: "These credentials do not match our records.")
15
+
16
+ nil
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ class Auth::NewPasswordForm < ApplicationForm
2
+ attr_accessor :token, :password, :password_confirmation
3
+
4
+ validates :token, presence: true
5
+ validates :password, presence: true, confirmation: true, length: { minimum: 8 }
6
+
7
+ def reset?
8
+ return false if invalid?
9
+
10
+ user = User.find_by_token_for(:password_reset, token)
11
+
12
+ if user.nil?
13
+ errors.add(:password, message: "This password reset token is invalid.")
14
+ return false
15
+ end
16
+
17
+ user.update(password: password)
18
+
19
+ true
20
+ end
21
+ end
@@ -0,0 +1,7 @@
1
+ class Auth::RegisterForm < ApplicationForm
2
+ attr_accessor :name, :email, :password, :password_confirmation
3
+
4
+ validates :name, presence: true
5
+ validates :email, presence: true, lowercase: true, email: true, uniqueness: { model: User, attribute: :email }
6
+ validates :password, presence: true, confirmation: true, length: { minimum: 8 }
7
+ end
@@ -0,0 +1,22 @@
1
+ class Auth::SendPasswordResetLinkForm < ApplicationForm
2
+ attr_accessor :email
3
+
4
+ validates :email, presence: true, email: true
5
+
6
+ def send_reset_link?
7
+ return false if invalid?
8
+
9
+ user = User.find_by(email: email)
10
+
11
+ if user.nil?
12
+ errors.add(:email, message: "We can't find a user with that email address.")
13
+ return false
14
+ end
15
+
16
+ token = user.generate_token_for(:password_reset)
17
+
18
+ user.send_password_reset_notification(token)
19
+
20
+ true
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ class DeleteUserForm < ApplicationForm
2
+ attr_accessor :password
3
+
4
+ validates :password, presence: true, current_password: true
5
+ end