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
@@ -0,0 +1,6 @@
1
+ class UpdatePasswordForm < ApplicationForm
2
+ attr_accessor :current_password, :password, :password_confirmation
3
+
4
+ validates :current_password, presence: true, current_password: true
5
+ validates :password, presence: true, confirmation: true, length: { minimum: 8 }
6
+ end
@@ -0,0 +1,6 @@
1
+ class UpdateProfileInformationForm < ApplicationForm
2
+ attr_accessor :name, :email
3
+
4
+ validates :name, presence: true
5
+ validates :email, presence: true, lowercase: true, email: true, uniqueness: { model: User, attribute: :email, conditions: -> { where.not(id: Current.user.id) } }
6
+ end
@@ -0,0 +1,11 @@
1
+ class ApplicationMailer < ActionMailer::Base
2
+ default from: email_address_with_name(
3
+ ENV.fetch("MAIL_FROM_ADDRESS", "hello@example.com"),
4
+ ENV.fetch("MAIL_FROM_NAME", "Example")
5
+ )
6
+ layout "mailer"
7
+
8
+ def default_url_options
9
+ { host: ENV.fetch("APP_URL", "http://localhost") }
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ class UserMailer < ApplicationMailer
2
+ def reset_password
3
+ mail(
4
+ to: params[:user].email,
5
+ subject: "Reset Password Notification"
6
+ )
7
+ end
8
+ end
@@ -0,0 +1,3 @@
1
+ class ApplicationRecord < ActiveRecord::Base
2
+ primary_abstract_class
3
+ end
@@ -0,0 +1,5 @@
1
+ module CanResetPassword
2
+ def send_password_reset_notification(token)
3
+ UserMailer.with(user: self, token: token).reset_password.deliver_later
4
+ end
5
+ end
@@ -0,0 +1,3 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :user
3
+ end
@@ -0,0 +1,11 @@
1
+ class User < ApplicationRecord
2
+ include CanResetPassword
3
+
4
+ has_secure_password
5
+
6
+ normalizes :email, with: ->(email) { email.strip.downcase }
7
+
8
+ generates_token_for :password_reset, expires_in: 60.minutes do
9
+ password_salt&.last(10)
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class CurrentPasswordValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ record.errors.add attribute, "The password is incorrect." unless Current.user&.authenticate(value)
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ class EmailValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ unless URI::MailTo::EMAIL_REGEXP.match?(value)
4
+ record.errors.add attribute, (options[:message] || "is not an email")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ class LowercaseValidator < ActiveModel::EachValidator
2
+ def validate_each(record, attribute, value)
3
+ record.errors.add attribute, (options[:message] || "must be lowercase") unless value == value.downcase
4
+ end
5
+ end
@@ -0,0 +1,24 @@
1
+ class UniquenessValidator < ActiveRecord::Validations::UniquenessValidator
2
+ def initialize(klass)
3
+ super
4
+ @klass = options[:model] if options[:model]
5
+ end
6
+
7
+ def validate_each(record, attribute, value)
8
+ if !options[:model] && !record.class.ancestors.include?(ActiveRecord::Base)
9
+ raise ArgumentError, "Unknown validator: 'UniquenessValidator'"
10
+ end
11
+
12
+ super unless options[:model]
13
+
14
+ record_org = record
15
+ attribute_org = attribute
16
+
17
+ attribute = options[:attribute].to_sym if options[:attribute]
18
+ record = options[:model].new(attribute => value)
19
+
20
+ super
21
+
22
+ record_org.errors.add(attribute_org, :taken) if record.errors.any?
23
+ end
24
+ end
@@ -0,0 +1,374 @@
1
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
2
+ <html xmlns="http://www.w3.org/1999/xhtml">
3
+ <head>
4
+ <title><%= ENV.fetch("APP_NAME", "Rails") %></title>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
7
+ <meta name="color-scheme" content="light">
8
+ <meta name="supported-color-schemes" content="light">
9
+ <style>
10
+ /* Base */
11
+
12
+ body,
13
+ body *:not(html):not(style):not(br):not(tr):not(code) {
14
+ box-sizing: border-box;
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif,
16
+ 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
17
+ position: relative;
18
+ }
19
+
20
+ body {
21
+ -webkit-text-size-adjust: none;
22
+ background-color: #ffffff;
23
+ color: #718096;
24
+ height: 100%;
25
+ line-height: 1.4;
26
+ margin: 0;
27
+ padding: 0;
28
+ width: 100% !important;
29
+ }
30
+
31
+ p,
32
+ ul,
33
+ ol,
34
+ blockquote {
35
+ line-height: 1.4;
36
+ text-align: left;
37
+ }
38
+
39
+ a {
40
+ color: #3869d4;
41
+ }
42
+
43
+ a img {
44
+ border: none;
45
+ }
46
+
47
+ /* Typography */
48
+
49
+ h1 {
50
+ color: #3d4852;
51
+ font-size: 18px;
52
+ font-weight: bold;
53
+ margin-top: 0;
54
+ text-align: left;
55
+ }
56
+
57
+ h2 {
58
+ font-size: 16px;
59
+ font-weight: bold;
60
+ margin-top: 0;
61
+ text-align: left;
62
+ }
63
+
64
+ h3 {
65
+ font-size: 14px;
66
+ font-weight: bold;
67
+ margin-top: 0;
68
+ text-align: left;
69
+ }
70
+
71
+ p {
72
+ font-size: 16px;
73
+ line-height: 1.5em;
74
+ margin-top: 0;
75
+ text-align: left;
76
+ }
77
+
78
+ p.sub {
79
+ font-size: 12px;
80
+ }
81
+
82
+ img {
83
+ max-width: 100%;
84
+ }
85
+
86
+ /* Layout */
87
+
88
+ .wrapper {
89
+ -premailer-cellpadding: 0;
90
+ -premailer-cellspacing: 0;
91
+ -premailer-width: 100%;
92
+ background-color: #edf2f7;
93
+ margin: 0;
94
+ padding: 0;
95
+ width: 100%;
96
+ }
97
+
98
+ .content {
99
+ -premailer-cellpadding: 0;
100
+ -premailer-cellspacing: 0;
101
+ -premailer-width: 100%;
102
+ margin: 0;
103
+ padding: 0;
104
+ width: 100%;
105
+ }
106
+
107
+ /* Header */
108
+
109
+ .header {
110
+ padding: 25px 0;
111
+ text-align: center;
112
+ }
113
+
114
+ .header a {
115
+ color: #3d4852;
116
+ font-size: 19px;
117
+ font-weight: bold;
118
+ text-decoration: none;
119
+ }
120
+
121
+ /* Logo */
122
+
123
+ .logo {
124
+ height: 75px;
125
+ max-height: 75px;
126
+ width: 75px;
127
+ }
128
+
129
+ /* Body */
130
+
131
+ .body {
132
+ -premailer-cellpadding: 0;
133
+ -premailer-cellspacing: 0;
134
+ -premailer-width: 100%;
135
+ background-color: #edf2f7;
136
+ border-bottom: 1px solid #edf2f7;
137
+ border-top: 1px solid #edf2f7;
138
+ margin: 0;
139
+ padding: 0;
140
+ width: 100%;
141
+ }
142
+
143
+ .inner-body {
144
+ -premailer-cellpadding: 0;
145
+ -premailer-cellspacing: 0;
146
+ -premailer-width: 570px;
147
+ background-color: #ffffff;
148
+ border-color: #e8e5ef;
149
+ border-radius: 2px;
150
+ border-width: 1px;
151
+ box-shadow: 0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015);
152
+ margin: 0 auto;
153
+ padding: 0;
154
+ width: 570px;
155
+ }
156
+
157
+ /* Subcopy */
158
+
159
+ .subcopy {
160
+ border-top: 1px solid #e8e5ef;
161
+ margin-top: 25px;
162
+ padding-top: 25px;
163
+ }
164
+
165
+ .subcopy p {
166
+ font-size: 14px;
167
+ }
168
+
169
+ /* Footer */
170
+
171
+ .footer {
172
+ -premailer-cellpadding: 0;
173
+ -premailer-cellspacing: 0;
174
+ -premailer-width: 570px;
175
+ margin: 0 auto;
176
+ padding: 0;
177
+ text-align: center;
178
+ width: 570px;
179
+ }
180
+
181
+ .footer p {
182
+ color: #b0adc5;
183
+ font-size: 12px;
184
+ text-align: center;
185
+ }
186
+
187
+ .footer a {
188
+ color: #b0adc5;
189
+ text-decoration: underline;
190
+ }
191
+
192
+ /* Tables */
193
+
194
+ .table table {
195
+ -premailer-cellpadding: 0;
196
+ -premailer-cellspacing: 0;
197
+ -premailer-width: 100%;
198
+ margin: 30px auto;
199
+ width: 100%;
200
+ }
201
+
202
+ .table th {
203
+ border-bottom: 1px solid #edeff2;
204
+ margin: 0;
205
+ padding-bottom: 8px;
206
+ }
207
+
208
+ .table td {
209
+ color: #74787e;
210
+ font-size: 15px;
211
+ line-height: 18px;
212
+ margin: 0;
213
+ padding: 10px 0;
214
+ }
215
+
216
+ .content-cell {
217
+ max-width: 100vw;
218
+ padding: 32px;
219
+ }
220
+
221
+ /* Buttons */
222
+
223
+ .action {
224
+ -premailer-cellpadding: 0;
225
+ -premailer-cellspacing: 0;
226
+ -premailer-width: 100%;
227
+ margin: 30px auto;
228
+ padding: 0;
229
+ text-align: center;
230
+ width: 100%;
231
+ }
232
+
233
+ .button {
234
+ -webkit-text-size-adjust: none;
235
+ border-radius: 4px;
236
+ color: #fff;
237
+ display: inline-block;
238
+ overflow: hidden;
239
+ text-decoration: none;
240
+ }
241
+
242
+ .button-blue,
243
+ .button-primary {
244
+ background-color: #2d3748;
245
+ border-bottom: 8px solid #2d3748;
246
+ border-left: 18px solid #2d3748;
247
+ border-right: 18px solid #2d3748;
248
+ border-top: 8px solid #2d3748;
249
+ }
250
+
251
+ .button-green,
252
+ .button-success {
253
+ background-color: #48bb78;
254
+ border-bottom: 8px solid #48bb78;
255
+ border-left: 18px solid #48bb78;
256
+ border-right: 18px solid #48bb78;
257
+ border-top: 8px solid #48bb78;
258
+ }
259
+
260
+ .button-red,
261
+ .button-error {
262
+ background-color: #e53e3e;
263
+ border-bottom: 8px solid #e53e3e;
264
+ border-left: 18px solid #e53e3e;
265
+ border-right: 18px solid #e53e3e;
266
+ border-top: 8px solid #e53e3e;
267
+ }
268
+
269
+ /* Panels */
270
+
271
+ .panel {
272
+ border-left: #2d3748 solid 4px;
273
+ margin: 21px 0;
274
+ }
275
+
276
+ .panel-content {
277
+ background-color: #edf2f7;
278
+ color: #718096;
279
+ padding: 16px;
280
+ }
281
+
282
+ .panel-content p {
283
+ color: #718096;
284
+ }
285
+
286
+ .panel-item {
287
+ padding: 0;
288
+ }
289
+
290
+ .panel-item p:last-of-type {
291
+ margin-bottom: 0;
292
+ padding-bottom: 0;
293
+ }
294
+
295
+ /* Utilities */
296
+
297
+ .break-all {
298
+ word-break: break-all;
299
+ }
300
+
301
+ @media only screen and (max-width: 600px) {
302
+ .inner-body {
303
+ width: 100% !important;
304
+ }
305
+
306
+ .footer {
307
+ width: 100% !important;
308
+ }
309
+ }
310
+
311
+ @media only screen and (max-width: 500px) {
312
+ .button {
313
+ width: 100% !important;
314
+ }
315
+ }
316
+ </style>
317
+ </head>
318
+ <body>
319
+
320
+ <table class="wrapper" width="100%" cellpadding="0" cellspacing="0" role="presentation">
321
+ <tr>
322
+ <td align="center">
323
+ <table class="content" width="100%" cellpadding="0" cellspacing="0" role="presentation">
324
+ <!-- Header -->
325
+ <tr>
326
+ <td class="header">
327
+ <a href="<%= root_url %>" style="display: inline-block;">
328
+ <%= ENV.fetch("APP_NAME", "Rails") %>
329
+ </a>
330
+ </td>
331
+ </tr>
332
+
333
+ <!-- Email Body -->
334
+ <tr>
335
+ <td class="body" width="100%" cellpadding="0" cellspacing="0" style="border: hidden !important;">
336
+ <table class="inner-body" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
337
+ <!-- Body content -->
338
+ <tr>
339
+ <td class="content-cell">
340
+ <%= yield %>
341
+
342
+ <!-- Subcopy -->
343
+ <table class="subcopy" width="100%" cellpadding="0" cellspacing="0" role="presentation">
344
+ <tr>
345
+ <td>
346
+ <%= yield(:subcopy) %>
347
+ </td>
348
+ </tr>
349
+ </table>
350
+
351
+ </td>
352
+ </tr>
353
+ </table>
354
+ </td>
355
+ </tr>
356
+
357
+ <!-- Footer -->
358
+ <tr>
359
+ <td>
360
+ <table class="footer" align="center" width="570" cellpadding="0" cellspacing="0" role="presentation">
361
+ <tr>
362
+ <td class="content-cell" align="center">
363
+ © <%= Time.new.year %> <%= ENV.fetch("APP_NAME", "Rails") %>. All rights reserved.
364
+ </td>
365
+ </tr>
366
+ </table>
367
+ </td>
368
+ </tr>
369
+ </table>
370
+ </td>
371
+ </tr>
372
+ </table>
373
+ </body>
374
+ </html>
@@ -0,0 +1,11 @@
1
+ <!-- Header -->
2
+ <%= ENV.fetch("APP_NAME", "Rails") %>: <%= root_url %>
3
+
4
+ <!-- Body -->
5
+ <%= yield %>
6
+
7
+ <!-- Subcopy -->
8
+ <%= yield(:subcopy) %>
9
+
10
+ <!-- Footer -->
11
+ © <%= Time.new.year %> <%= ENV.fetch("APP_NAME", "Rails") %>. All rights reserved.
@@ -0,0 +1,39 @@
1
+ <!-- Greeting -->
2
+ <h1>Hello!</h1>
3
+
4
+ <!-- Intro Lines -->
5
+ <p>You are receiving this email because we received a password reset request for your account.</p>
6
+
7
+ <!-- Action Button -->
8
+ <table class="action" align="center" width="100%" cellpadding="0" cellspacing="0" role="presentation">
9
+ <tr>
10
+ <td align="center">
11
+ <table width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation">
12
+ <tr>
13
+ <td align="center">
14
+ <table border="0" cellpadding="0" cellspacing="0" role="presentation">
15
+ <tr>
16
+ <td>
17
+ <a href="<%= password_reset_url(token: params[:token]) %>" class="button button-primary" target="_blank" rel="noopener">Reset Password</a>
18
+ </td>
19
+ </tr>
20
+ </table>
21
+ </td>
22
+ </tr>
23
+ </table>
24
+ </td>
25
+ </tr>
26
+ </table>
27
+
28
+ <!-- Outro Lines -->
29
+ <p>This password reset link will expire in 60 minutes.</p>
30
+
31
+ <p>If you did not request a password reset, no further action is required.</p>
32
+
33
+ <!-- Salutation -->
34
+ <p>Regards,<br><%= ENV.fetch("APP_NAME", "Rails") %></p>
35
+
36
+ <!-- Subcopy -->
37
+ <% content_for :subcopy do %>
38
+ <p>If you're having trouble clicking the "Reset Password" button, copy and paste the URL below into your web browser: <span class="break-all"><%= link_to password_reset_url(token: params[:token]), password_reset_url(token: params[:token]) %><span></p>
39
+ <% end %>
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env sh
2
+
3
+ if ! gem list foreman -i --silent; then
4
+ echo "Installing foreman..."
5
+ gem install foreman
6
+ fi
7
+
8
+ # Default to port 3000 if not specified
9
+ export PORT="${PORT:-3000}"
10
+
11
+ # Let the debug gem allow remote connections,
12
+ # but avoid loading until `debugger` is called
13
+ export RUBY_DEBUG_OPEN="true"
14
+ export RUBY_DEBUG_LAZY="true"
15
+
16
+ exec foreman start -f Procfile.dev "$@"
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ #
5
+ # This file was generated by Bundler.
6
+ #
7
+ # The application 'vite' is installed as part of a gem, and
8
+ # this file is here to facilitate running it.
9
+ #
10
+
11
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
12
+
13
+ bundle_binstub = File.expand_path("bundle", __dir__)
14
+
15
+ if File.file?(bundle_binstub)
16
+ if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
17
+ load(bundle_binstub)
18
+ else
19
+ abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
20
+ Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
21
+ end
22
+ end
23
+
24
+ require "rubygems"
25
+ require "bundler/setup"
26
+
27
+ load Gem.bin_path("vite_ruby", "vite")
@@ -0,0 +1,27 @@
1
+ Rails.application.routes.draw do
2
+ get "up" => "rails/health#show", as: :rails_health_check
3
+
4
+ root "welcome#index"
5
+
6
+ get "register", to: "auth/registered_user#new", as: :register
7
+ post "register", to: "auth/registered_user#create"
8
+
9
+ get "login", to: "auth/authenticated_session#new", as: :login
10
+ post "login", to: "auth/authenticated_session#create"
11
+
12
+ get "forgot-password", to: "auth/password_reset_link#new", as: :password_request
13
+ post "forgot-password", to: "auth/password_reset_link#create", as: :password_email
14
+
15
+ get "reset-password/:token", to: "auth/new_password#new", as: :password_reset
16
+ post "reset-password", to: "auth/new_password#create", as: :password_store
17
+
18
+ post "logout", to: "auth/authenticated_session#destroy", as: :logout
19
+
20
+ get "dashboard", to: "dashboard#show", as: :dashboard
21
+
22
+ get "profile", to: "profile#edit", as: :profile_edit
23
+ patch "profile", to: "profile#update", as: :profile_update
24
+ delete "profile", to: "profile#destroy", as: :profile_destroy
25
+
26
+ put "password", to: "password#update", as: :password_update
27
+ end
@@ -0,0 +1,16 @@
1
+ {
2
+ "all": {
3
+ "sourceCodeDir": "app/javascript",
4
+ "watchAdditionalPaths": []
5
+ },
6
+ "development": {
7
+ "autoBuild": true,
8
+ "publicOutputDir": "vite-dev",
9
+ "port": 3036
10
+ },
11
+ "test": {
12
+ "autoBuild": true,
13
+ "publicOutputDir": "vite-test",
14
+ "port": 3037
15
+ }
16
+ }
@@ -0,0 +1,14 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :users do |table|
4
+ table.string :name
5
+ table.string :email
6
+ table.string :password_digest
7
+ table.timestamps
8
+ end
9
+ end
10
+
11
+ def self.down
12
+ drop_table :users
13
+ end
14
+ end
@@ -0,0 +1,22 @@
1
+ class CreateDelayedJobs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :delayed_jobs do |table|
4
+ table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
5
+ table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
6
+ table.text :handler, null: false # YAML-encoded string of the object that will do work
7
+ table.text :last_error # reason for last failure (See Note below)
8
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
9
+ table.datetime :locked_at # Set when a client is working on this object
10
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
11
+ table.string :locked_by # Who is working on this object (if locked)
12
+ table.string :queue # The name of the queue this job is in
13
+ table.timestamps null: true
14
+ end
15
+
16
+ add_index :delayed_jobs, [ :priority, :run_at ], name: "delayed_jobs_priority"
17
+ end
18
+
19
+ def self.down
20
+ drop_table :delayed_jobs
21
+ end
22
+ end
@@ -0,0 +1,12 @@
1
+ import { SVGAttributes } from 'react';
2
+
3
+ export default function ApplicationLogo(props: SVGAttributes<SVGElement>) {
4
+ return (
5
+ <svg {...props} viewBox="0 -6 32 32" xmlns="http://www.w3.org/2000/svg">
6
+ <g fill="none" fillRule="evenodd">
7
+ <path d="M0-6h32v32H0z"/>
8
+ <path fill="#c00" fillRule="nonzero" d="M.985 19.636s.422-4.163 3.375-9.087c2.954-4.924 7.99-8.65 12.083-9.017 8.144-.816 15.46 6.485 15.46 6.485s-.24.168-.494.38C23.42 2.49 18.54 5.274 17.005 6.02c-7.033 3.925-4.91 13.616-4.91 13.616H.987zM24.137 2.32c-.45-.182-.9-.35-1.364-.505l.056-.93c.885.254 1.237.423 1.363.493l-.056.943zM22.8 5.304c.45.028.915.084 1.393.183l-.056.872c-.464-.1-.928-.155-1.392-.17l.056-.885zM17.597.913c-.407 0-.815.015-1.223.058l-.268-.83c.465-.056.915-.084 1.35-.084l.282.858h-.14zm.676 5.178c.35-.154.76-.31 1.237-.45l.31.93c-.41.125-.817.294-1.225.49l-.323-.97zm-6.386-3.7c-.366.184-.718.395-1.083.62l-.647-.985c.38-.225.745-.42 1.097-.604l.633.97zm2.883 6.33c.252-.323.548-.646.87-.942l.634.957c-.31.323-.59.647-.83 1L14.77 8.72zm-2.04 4.53c.112-.506.24-1.027.422-1.547l1.012.802c-.14.548-.24 1.097-.295 1.645l-1.14-.9zM6.57 6.57c-.34.35-.662.73-.958 1.11L4.53 6.752c.323-.352.674-.704 1.04-1.055l1 .872zm-4.25 6.286c-.224.52-.52 1.21-.702 1.688L0 13.954c.14-.38.436-1.084.703-1.69l1.618.592zm10.2 3.967l1.518.548c.084.663.21 1.28.337 1.83l-1.688-.605c-.07-.422-.14-1.027-.168-1.772z"/>
9
+ </g>
10
+ </svg>
11
+ );
12
+ }