kaze 0.2.0

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