maquina-generators 0.8.0 → 0.9.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 (24) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/Rakefile +1 -1
  4. data/lib/generators/maquina/app/USAGE +2 -0
  5. data/lib/generators/maquina/app/app_generator.rb +31 -0
  6. data/lib/generators/maquina/app/templates/README.md.erb +6 -0
  7. data/lib/generators/maquina/app/templates/app/views/home/index.html.erb +3 -0
  8. data/lib/generators/maquina/passkeys/passkeys_generator.rb +3 -2
  9. data/lib/generators/maquina/passkeys/templates/app/controllers/passkeys_controller.rb +2 -2
  10. data/lib/generators/maquina/passkeys/templates/app/views/passkeys/index.html.erb +1 -1
  11. data/lib/maquina_generators/version.rb +1 -1
  12. data/test/generators/maquina/app_generator_test.rb +41 -0
  13. data/test/generators/maquina/passkeys_generator_test.rb +14 -0
  14. data/test/tmp/app/views/registration/verifications/new.html.erb +43 -0
  15. data/test/tmp/app/views/registrations/new.html.erb +39 -0
  16. data/test/tmp/app/views/session/verifications/new.html.erb +43 -0
  17. data/test/tmp/app/views/sessions/new.html.erb +40 -0
  18. data/test/tmp/app/views/verification_mailer/verification_code.html.erb +37 -0
  19. data/test/tmp/app/views/verification_mailer/verification_code.text.erb +11 -0
  20. metadata +11 -5
  21. /data/test/tmp/db/migrate/{20261014212613_create_accounts.rb → 20261019191386_create_accounts.rb} +0 -0
  22. /data/test/tmp/db/migrate/{20261014212614_create_users.rb → 20261019191387_create_users.rb} +0 -0
  23. /data/test/tmp/db/migrate/{20261014212615_create_sessions.rb → 20261019191388_create_sessions.rb} +0 -0
  24. /data/test/tmp/db/migrate/{20261014212616_create_email_verifications.rb → 20261019191389_create_email_verifications.rb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8127a7f3efbfaea2bb59f4c0b0977476fd7bc4d176d634d294d9520dd3a10f83
4
- data.tar.gz: ef59f91d08b1f3bb99343f28564e5d4c07972201724e3c58a7ee37b1ab93c07f
3
+ metadata.gz: 76d3dc4690cd7ff89b935e2289d7032084e426bda2e2f2998ed0aa81c0bb5fef
4
+ data.tar.gz: d983ec5b7183219b998a4447215808edf1d492c01cd344c957e9e9674036a3c7
5
5
  SHA512:
6
- metadata.gz: 61d953a6567afb896d605dcf653bd8cd71d192fa81d043262157b32f915847f70e22bc74575cc35468f093b00532a0d910e39108fc6d0224ae7a6b5813fe832b
7
- data.tar.gz: 7e65996ad9c0e7e61bbd8ce5774432df06a8176005741b68ba2c3757cbaffa42439e1bacae486b5966e4b3801470d8d506e5a6c019c2bb4d95e9615cb2beb7a2
6
+ metadata.gz: ea8c573e01d55f1504c0d1b647e805847f67c48042b13580cfec8e301b61151f6ebecb8f3c645a57d937168c48b33f72b1e4e8f35ced0c0bd3a5c36e79d105cd
7
+ data.tar.gz: 329c7a172d226fc0bca18c5cc807c869dd1a3937bb9e6d2f66d315c9ac0b41ef082e44159ae7e2c378cf97f8cfeada7d8cadd9a90afb8465aad0e087101a03b7
data/README.md CHANGED
@@ -423,7 +423,7 @@ Watch the Security page while the policy is report-only, then set `content_secur
423
423
  9. Adds turbo morphing, `yield :head`, and simplifies `<main>` tag in layout
424
424
  10. Optionally installs authentication (`maquina:clave` or `maquina:registration`)
425
425
  11. Invokes sub-generators: `maquina:mission_control_jobs`, `maquina:solid_errors`
426
- 12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules)
426
+ 12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules), and `maquina:passkeys` with `--passkeys`
427
427
  13. Restores custom layouts overwritten by gem installers
428
428
  14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors, security)
429
429
  15. Creates a HomeController with root route
@@ -436,6 +436,7 @@ Watch the Security page while the policy is report-only, then set `content_secur
436
436
  rails g maquina:app
437
437
  rails g maquina:app --prefix /backstage --port 3100
438
438
  rails g maquina:app --auth registration
439
+ rails g maquina:app --auth clave --passkeys
439
440
  ```
440
441
 
441
442
  #### Options
@@ -443,6 +444,7 @@ rails g maquina:app --auth registration
443
444
  - `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs, Security)
444
445
  - `--port` (default: `3000`) -- Default port for the development server
445
446
  - `--auth` (default: `none`) -- Authentication type: `none`, `clave`, or `registration`
447
+ - `--passkeys` (default: `false`) -- Adds passkey sign-in with `maquina:passkeys`. It needs `--auth clave` and refuses to run without it. The passkey origin follows `--port`, and the home page links to `/passkeys`.
446
448
 
447
449
  #### After running
448
450
 
data/Rakefile CHANGED
@@ -4,7 +4,7 @@ require "rake/testtask"
4
4
  Rake::TestTask.new(:test) do |t|
5
5
  t.libs << "test"
6
6
  t.libs << "lib"
7
- t.test_files = FileList["test/**/*_test.rb"]
7
+ t.test_files = FileList["test/**/*_test.rb"].exclude("test/tmp/**/*")
8
8
  end
9
9
 
10
10
  task default: :test
@@ -9,3 +9,5 @@ Examples:
9
9
  rails g maquina:app
10
10
 
11
11
  rails g maquina:app --prefix /backstage --port 3100
12
+
13
+ rails g maquina:app --auth clave --passkeys
@@ -11,6 +11,15 @@ module Maquina
11
11
  desc: "Default port for development server"
12
12
  class_option :auth, type: :string, default: "none",
13
13
  desc: "Authentication type: none, clave, or registration"
14
+ class_option :passkeys, type: :boolean, default: false,
15
+ desc: "Add passkey sign-in on top of clave (requires --auth clave)"
16
+
17
+ # 0. Check options before touching anything
18
+ def check_options
19
+ return unless options[:passkeys] && options[:auth] != "clave"
20
+
21
+ raise Thor::Error, "--passkeys builds on clave. Run with `--auth clave --passkeys`."
22
+ end
14
23
 
15
24
  # 1. Add gems
16
25
  def add_gems
@@ -215,6 +224,24 @@ module Maquina
215
224
  generate "maquina:security", "--prefix", options[:prefix], "--quiet"
216
225
  end
217
226
 
227
+ # 12b. Install passkeys (after security, so its Rack::Attack throttle lands)
228
+ def install_passkeys
229
+ return unless options[:passkeys] && rails_app?
230
+
231
+ generate "maquina:passkeys", "--quiet"
232
+ end
233
+
234
+ # 12c. Point the passkey origin at the development port
235
+ def configure_passkeys_origin
236
+ return unless options[:passkeys] && options[:port] != "3000"
237
+
238
+ initializer = File.join(destination_root, "config/initializers/webauthn.rb")
239
+ return unless File.exist?(initializer)
240
+
241
+ gsub_file "config/initializers/webauthn.rb",
242
+ '"http://localhost:3000"', %("http://localhost:#{options[:port]}")
243
+ end
244
+
218
245
  # 13. Restore custom layouts overwritten by gem installers
219
246
  def restore_custom_layouts
220
247
  return unless rails_app?
@@ -339,6 +366,10 @@ module Maquina
339
366
  say " - Visit /session/new to sign in"
340
367
  say " - Visit /registrations/new to sign up" if options[:auth] == "registration"
341
368
  say " - Visit /registration/new to sign up" if options[:auth] == "clave"
369
+ if options[:passkeys]
370
+ say " - Visit /passkeys to add a passkey"
371
+ say " - Production: set WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS before the first passkey"
372
+ end
342
373
  say " - Customize: app/controllers/concerns/authentication.rb"
343
374
  say " - Locales: config/locales/"
344
375
  end
@@ -26,6 +26,9 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
26
26
  <% if options[:auth] != "none" -%>
27
27
  | **Authentication** | <%= options[:auth] == "clave" ? "Clave passwordless email-code authentication" : "Registration-based email/password authentication" %> |
28
28
  <% end -%>
29
+ <% if options[:passkeys] -%>
30
+ | **Passkeys** | Passkey sign-in with Face ID, a fingerprint or a device PIN; the email code stays as the way in without one |
31
+ <% end -%>
29
32
  | **Background Jobs** | Solid Queue with Mission Control Jobs dashboard |
30
33
  | **Error Tracking** | Solid Errors with custom UI |
31
34
  | **Rate Limiting** | Rack::Attack with sensible defaults |
@@ -52,6 +55,9 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
52
55
  <% elsif options[:auth] == "clave" -%>
53
56
  | http://localhost:<%= options[:port] %>/registration/new | Sign up |
54
57
  <% end -%>
58
+ <% if options[:passkeys] -%>
59
+ | http://localhost:<%= options[:port] %>/passkeys | Passkeys |
60
+ <% end -%>
55
61
  <% end -%>
56
62
 
57
63
  ## Scripts
@@ -15,6 +15,9 @@
15
15
  <p class="text-sm text-muted-foreground">
16
16
  Signed in as <span class="font-medium text-foreground"><%%= Current.user.email_address %></span>
17
17
  </p>
18
+ <% if options[:passkeys] -%>
19
+ <%%= link_to "Passkeys", passkeys_path, data: { component: "button", variant: "outline", size: "sm" } %>
20
+ <% end -%>
18
21
  <%%= button_to t(".sign_out", default: "Sign Out"), session_path, method: :delete, data: { component: "button", variant: "outline", size: "sm" } %>
19
22
  </div>
20
23
  <%% else %>
@@ -248,9 +248,10 @@ module Maquina
248
248
  <<~RUBY
249
249
 
250
250
  # Passkey sign-in (maquina:passkeys): the challenge and the assertion,
251
- # counted like the email-code login above.
251
+ # counted like the email-code login above. Keyed on the address Rails
252
+ # resolves through trusted proxies, not the proxy hop in req.ip.
252
253
  Rack::Attack.throttle("passkey/ip", limit: #{THROTTLE_LIMIT}, period: #{THROTTLE_PERIOD}) do |req|
253
- req.ip if req.path.start_with?("/session/passkey") && req.post?
254
+ (req.env["action_dispatch.remote_ip"]&.to_s || req.ip) if req.path.start_with?("/session/passkey") && req.post?
254
255
  end
255
256
  RUBY
256
257
  end
@@ -43,9 +43,9 @@ class PasskeysController < ApplicationController
43
43
  # PATCH /passkeys/:id
44
44
  def update
45
45
  if @passkey.update(params.expect(passkey: [:name]))
46
- redirect_to passkeys_path, notice: t("flash.passkeys.updated")
46
+ redirect_to passkeys_path, notice: t("flash.passkeys.updated"), status: :see_other
47
47
  else
48
- redirect_to passkeys_path, alert: @passkey.errors.full_messages.to_sentence
48
+ redirect_to passkeys_path, alert: @passkey.errors.full_messages.to_sentence, status: :see_other
49
49
  end
50
50
  end
51
51
 
@@ -61,7 +61,7 @@
61
61
 
62
62
  <%= button_to t(".remove"), passkey_path(passkey), method: :delete,
63
63
  form: { data: { turbo_confirm: t(".remove_confirm") } },
64
- data: { component: "button", variant: "destructive" },
64
+ data: { component: "button", variant: "ghost" },
65
65
  class: "px-3 py-2 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-lg cursor-pointer" %>
66
66
  </li>
67
67
  <% end %>
@@ -1,3 +1,3 @@
1
1
  module MaquinaGenerators
2
- VERSION = "0.8.0"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -364,6 +364,47 @@ class Maquina::Generators::AppGeneratorTest < Rails::Generators::TestCase
364
364
  assert_match(/registrations\/new/, output)
365
365
  end
366
366
 
367
+ test "passkeys need clave" do
368
+ %w[none registration].each do |auth|
369
+ error = assert_raises(Thor::Error) { run_generator ["--auth", auth, "--passkeys"], debug: true }
370
+ assert_match(/--auth clave --passkeys/, error.message)
371
+ end
372
+ assert_no_file "Procfile.dev"
373
+ end
374
+
375
+ test "passkeys show up in the home page, README and post-install" do
376
+ output = run_generator %w[--auth clave --passkeys]
377
+
378
+ assert_file "app/views/home/index.html.erb", /link_to "Passkeys", passkeys_path/
379
+ assert_file "README.md" do |content|
380
+ assert_match(/\*\*Passkeys\*\*/, content)
381
+ assert_match(%r{localhost:3000/passkeys \| Passkeys}, content)
382
+ end
383
+ assert_match(%r{Visit /passkeys to add a passkey}, output)
384
+ assert_match(/WEBAUTHN_RP_ID and WEBAUTHN_ORIGINS/, output)
385
+ end
386
+
387
+ test "without passkeys nothing mentions them" do
388
+ output = run_generator %w[--auth clave]
389
+
390
+ assert_file "app/views/home/index.html.erb" do |content|
391
+ assert_no_match(/passkeys_path/, content)
392
+ end
393
+ assert_file "README.md" do |content|
394
+ assert_no_match(/Passkeys/, content)
395
+ end
396
+ assert_no_match(/passkey/, output)
397
+ end
398
+
399
+ test "points the passkey origin at a custom development port" do
400
+ initializer = File.join(destination_root, "config/initializers/webauthn.rb")
401
+ File.write(initializer, %(config.allowed_origins = Array(ENV["WEBAUTHN_ORIGINS"]&.split(",") || "http://localhost:3000")\n))
402
+
403
+ run_generator %w[--auth clave --passkeys --port 3100]
404
+
405
+ assert_match(/"http:\/\/localhost:3100"\)\n\z/, File.read(initializer))
406
+ end
407
+
367
408
  test "injects database.yml restore into bin/setup" do
368
409
  write_bin_setup
369
410
 
@@ -124,6 +124,19 @@ class Maquina::Generators::PasskeysGeneratorTest < Rails::Generators::TestCase
124
124
  end
125
125
  end
126
126
 
127
+ test "renaming and removing redirect with 303 and the remove button stays readable" do
128
+ run_generator
129
+
130
+ assert_file "app/controllers/passkeys_controller.rb" do |content|
131
+ assert_equal 3, content.scan(/redirect_to passkeys_path, .*status: :see_other$/).size
132
+ assert_no_match(/redirect_to(?!.*see_other)/, content)
133
+ end
134
+ assert_file "app/views/passkeys/index.html.erb" do |content|
135
+ assert_no_match(/variant: "destructive"/, content)
136
+ assert_match(/variant: "ghost" },\n\s+class: "[^"]*text-destructive hover:bg-destructive\/10/, content)
137
+ end
138
+ end
139
+
127
140
  test "adds the passkey button and autofill to the sign-in page" do
128
141
  run_generator
129
142
 
@@ -197,6 +210,7 @@ class Maquina::Generators::PasskeysGeneratorTest < Rails::Generators::TestCase
197
210
  run_generator
198
211
 
199
212
  assert_file "config/initializers/rack_attack.rb", /Rack::Attack\.throttle\("passkey\/ip", limit: 20, period: 1\.minute\)/
213
+ assert_file "config/initializers/rack_attack.rb", /\(req\.env\["action_dispatch\.remote_ip"\]&\.to_s \|\| req\.ip\) if req\.path\.start_with\?\("\/session\/passkey"\)/
200
214
  end
201
215
 
202
216
  test "running twice changes nothing" do
@@ -0,0 +1,43 @@
1
+ <div class="flex flex-1 items-center justify-center px-4 py-8">
2
+ <div class="w-full max-w-md">
3
+ <div class="bg-card rounded-xl shadow-sm border border-border p-8">
4
+ <h1 class="text-2xl font-bold text-foreground text-center mb-4">
5
+ <%= t(".title") %>
6
+ </h1>
7
+
8
+ <p class="text-center text-muted-foreground mb-8">
9
+ <%= t(".sent_to", email: mask_email(@email)) %>
10
+ </p>
11
+
12
+ <%= form_with url: registration_verification_path, method: :post, class: "space-y-6" do |form| %>
13
+ <div>
14
+ <%= form.label :code, t(".enter_code"), class: "block text-sm font-medium text-foreground mb-2 text-center", data: { component: "label" } %>
15
+ <%= form.text_field :code,
16
+ required: true,
17
+ autofocus: true,
18
+ maxlength: 6,
19
+ autocomplete: "one-time-code",
20
+ inputmode: "text",
21
+ placeholder: "ABC123",
22
+ data: { component: "input" },
23
+ class: "w-full px-4 py-4 text-center text-2xl font-mono tracking-widest uppercase border border-input rounded-lg focus:ring-2 focus:ring-ring focus:border-ring transition-colors" %>
24
+ </div>
25
+
26
+ <p class="text-sm text-muted-foreground text-center">
27
+ <%= t(".check_spam") %>
28
+ </p>
29
+
30
+ <div>
31
+ <%= form.submit t(".submit"), data: { component: "button", variant: "primary" }, class: "w-full px-4 py-3 text-base font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors cursor-pointer" %>
32
+ </div>
33
+ <% end %>
34
+
35
+ <div class="mt-6 text-center">
36
+ <p class="text-sm text-muted-foreground mb-2">
37
+ <%= t(".didnt_receive") %>
38
+ </p>
39
+ <%= button_to t(".resend"), registration_verification_resend_path, method: :post, class: "text-sm font-medium text-primary hover:text-primary/80" %>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
@@ -0,0 +1,39 @@
1
+ <div class="flex flex-1 items-center justify-center px-4 py-8">
2
+ <div class="w-full max-w-md">
3
+ <div class="bg-card rounded-xl shadow-sm border border-border p-8">
4
+ <h1 class="text-2xl font-bold text-foreground text-center mb-8">
5
+ <%= t(".title") %>
6
+ </h1>
7
+
8
+ <%= form_with url: registration_path, method: :post, class: "space-y-6" do |form| %>
9
+ <div>
10
+ <%= form.label :email_address, t(".email"), class: "block text-sm font-medium text-foreground mb-2", data: { component: "label" } %>
11
+ <%= form.email_field :email_address,
12
+ required: true,
13
+ autofocus: true,
14
+ autocomplete: "email",
15
+ placeholder: t(".email_placeholder"),
16
+ data: { component: "input" },
17
+ class: "w-full px-4 py-3 border border-input rounded-lg focus:ring-2 focus:ring-ring focus:border-ring transition-colors" %>
18
+ </div>
19
+
20
+ <p class="text-sm text-muted-foreground">
21
+ <%= t(".email_instructions") %>
22
+ </p>
23
+
24
+ <p class="text-sm text-muted-foreground">
25
+ <%= t(".spam_note") %>
26
+ </p>
27
+
28
+ <div>
29
+ <%= form.submit t(".submit"), data: { component: "button", variant: "primary" }, class: "w-full px-4 py-3 text-base font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors cursor-pointer" %>
30
+ </div>
31
+ <% end %>
32
+
33
+ <p class="mt-6 text-center text-sm text-muted-foreground">
34
+ <%= t(".have_account") %>
35
+ <%= link_to t(".sign_in"), new_session_path, class: "font-medium text-primary hover:text-primary/80" %>
36
+ </p>
37
+ </div>
38
+ </div>
39
+ </div>
@@ -0,0 +1,43 @@
1
+ <div class="flex flex-1 items-center justify-center px-4 py-8">
2
+ <div class="w-full max-w-md">
3
+ <div class="bg-card rounded-xl shadow-sm border border-border p-8">
4
+ <h1 class="text-2xl font-bold text-foreground text-center mb-4">
5
+ <%= t(".title") %>
6
+ </h1>
7
+
8
+ <p class="text-center text-muted-foreground mb-8">
9
+ <%= t(".sent_to", email: mask_email(@email)) %>
10
+ </p>
11
+
12
+ <%= form_with url: session_verification_path, method: :post, class: "space-y-6" do |form| %>
13
+ <div>
14
+ <%= form.label :code, t(".enter_code"), class: "block text-sm font-medium text-foreground mb-2 text-center", data: { component: "label" } %>
15
+ <%= form.text_field :code,
16
+ required: true,
17
+ autofocus: true,
18
+ maxlength: 6,
19
+ autocomplete: "one-time-code",
20
+ inputmode: "text",
21
+ placeholder: "ABC123",
22
+ data: { component: "input" },
23
+ class: "w-full px-4 py-4 text-center text-2xl font-mono tracking-widest uppercase border border-input rounded-lg focus:ring-2 focus:ring-ring focus:border-ring transition-colors" %>
24
+ </div>
25
+
26
+ <p class="text-sm text-muted-foreground text-center">
27
+ <%= t(".check_spam") %>
28
+ </p>
29
+
30
+ <div>
31
+ <%= form.submit t(".submit"), data: { component: "button", variant: "primary" }, class: "w-full px-4 py-3 text-base font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors cursor-pointer" %>
32
+ </div>
33
+ <% end %>
34
+
35
+ <div class="mt-6 text-center">
36
+ <p class="text-sm text-muted-foreground mb-2">
37
+ <%= t(".didnt_receive") %>
38
+ </p>
39
+ <%= button_to t(".resend"), session_verification_resend_path, method: :post, class: "text-sm font-medium text-primary hover:text-primary/80" %>
40
+ </div>
41
+ </div>
42
+ </div>
43
+ </div>
@@ -0,0 +1,40 @@
1
+ <div class="flex flex-1 items-center justify-center px-4 py-8">
2
+ <div class="w-full max-w-md">
3
+ <div class="bg-card rounded-xl shadow-sm border border-border p-8">
4
+ <h1 class="text-2xl font-bold text-foreground text-center mb-8">
5
+ <%= t(".title") %>
6
+ </h1>
7
+
8
+ <%= form_with url: session_path, method: :post, class: "space-y-6" do |form| %>
9
+ <div>
10
+ <%= form.label :email_address, t(".email"), class: "block text-sm font-medium text-foreground mb-2", data: { component: "label" } %>
11
+ <%= form.email_field :email_address,
12
+ required: true,
13
+ autofocus: true,
14
+ autocomplete: "email",
15
+ placeholder: t(".email_placeholder"),
16
+ value: params[:email_address],
17
+ data: { component: "input" },
18
+ class: "w-full px-4 py-3 border border-input rounded-lg focus:ring-2 focus:ring-ring focus:border-ring transition-colors" %>
19
+ </div>
20
+
21
+ <p class="text-sm text-muted-foreground">
22
+ <%= t(".email_instructions") %>
23
+ </p>
24
+
25
+ <p class="text-sm text-muted-foreground">
26
+ <%= t(".spam_note") %>
27
+ </p>
28
+
29
+ <div>
30
+ <%= form.submit t(".submit"), data: { component: "button", variant: "primary" }, class: "w-full px-4 py-3 text-base font-medium text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg transition-colors cursor-pointer" %>
31
+ </div>
32
+ <% end %>
33
+
34
+ <p class="mt-6 text-center text-sm text-muted-foreground">
35
+ <%= t(".no_account") %>
36
+ <%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-primary hover:text-primary/80" %>
37
+ </p>
38
+ </div>
39
+ </div>
40
+ </div>
@@ -0,0 +1,37 @@
1
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
2
+ <div style="background-color: #ffffff; border-radius: 8px; padding: 40px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);">
3
+ <!-- Logo -->
4
+ <div style="text-align: center; margin-bottom: 32px;">
5
+ <span style="font-size: 28px; font-weight: 700; color: #ff2058;"><%= Rails.application.class.module_parent_name %></span>
6
+ </div>
7
+
8
+ <!-- Title -->
9
+ <h1 style="text-align: center; font-size: 24px; font-weight: 600; color: #0a0a0a; margin-bottom: 24px;">
10
+ <%= t("verification_mailer.verification_code.title") %>
11
+ </h1>
12
+
13
+ <!-- Instructions -->
14
+ <p style="text-align: center; font-size: 16px; color: #737373; margin-bottom: 32px;">
15
+ <%= t("verification_mailer.verification_code.instructions") %>
16
+ </p>
17
+
18
+ <!-- Code Display -->
19
+ <div style="background-color: #f5f5f5; border-radius: 8px; padding: 24px; text-align: center; margin-bottom: 32px;">
20
+ <span style="font-family: 'Courier New', Courier, monospace; font-size: 36px; font-weight: 700; letter-spacing: 8px; color: #0a0a0a;">
21
+ <%= @verification.code %>
22
+ </span>
23
+ </div>
24
+
25
+ <!-- Expiry Note -->
26
+ <p style="text-align: center; font-size: 14px; color: #737373; margin-bottom: 32px;">
27
+ <%= t("verification_mailer.verification_code.expires_in") %>
28
+ </p>
29
+
30
+ <!-- Ignore Note -->
31
+ <div style="border-top: 1px solid #e5e5e5; padding-top: 24px;">
32
+ <p style="text-align: center; font-size: 13px; color: #737373;">
33
+ <%= t("verification_mailer.verification_code.ignore_if_not_you") %>
34
+ </p>
35
+ </div>
36
+ </div>
37
+ </div>
@@ -0,0 +1,11 @@
1
+ <%= t("verification_mailer.verification_code.title") %>
2
+
3
+ <%= t("verification_mailer.verification_code.instructions") %>
4
+
5
+ <%= @verification.code %>
6
+
7
+ <%= t("verification_mailer.verification_code.expires_in") %>
8
+
9
+ ---
10
+
11
+ <%= t("verification_mailer.verification_code.ignore_if_not_you") %>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: maquina-generators
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chavez
@@ -307,13 +307,19 @@ files:
307
307
  - test/tmp/app/models/email_verification.rb
308
308
  - test/tmp/app/models/session.rb
309
309
  - test/tmp/app/models/user.rb
310
+ - test/tmp/app/views/registration/verifications/new.html.erb
311
+ - test/tmp/app/views/registrations/new.html.erb
312
+ - test/tmp/app/views/session/verifications/new.html.erb
313
+ - test/tmp/app/views/sessions/new.html.erb
314
+ - test/tmp/app/views/verification_mailer/verification_code.html.erb
315
+ - test/tmp/app/views/verification_mailer/verification_code.text.erb
310
316
  - test/tmp/config/locales/clave.en.yml
311
317
  - test/tmp/config/locales/clave.es.yml
312
318
  - test/tmp/config/routes.rb
313
- - test/tmp/db/migrate/20261014212613_create_accounts.rb
314
- - test/tmp/db/migrate/20261014212614_create_users.rb
315
- - test/tmp/db/migrate/20261014212615_create_sessions.rb
316
- - test/tmp/db/migrate/20261014212616_create_email_verifications.rb
319
+ - test/tmp/db/migrate/20261019191386_create_accounts.rb
320
+ - test/tmp/db/migrate/20261019191387_create_users.rb
321
+ - test/tmp/db/migrate/20261019191388_create_sessions.rb
322
+ - test/tmp/db/migrate/20261019191389_create_email_verifications.rb
317
323
  - test/tmp/test/test_helpers/session_test_helper.rb
318
324
  homepage: https://maquina.app
319
325
  licenses: