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.
- checksums.yaml +4 -4
- data/README.md +3 -1
- data/Rakefile +1 -1
- data/lib/generators/maquina/app/USAGE +2 -0
- data/lib/generators/maquina/app/app_generator.rb +31 -0
- data/lib/generators/maquina/app/templates/README.md.erb +6 -0
- data/lib/generators/maquina/app/templates/app/views/home/index.html.erb +3 -0
- data/lib/generators/maquina/passkeys/passkeys_generator.rb +3 -2
- data/lib/generators/maquina/passkeys/templates/app/controllers/passkeys_controller.rb +2 -2
- data/lib/generators/maquina/passkeys/templates/app/views/passkeys/index.html.erb +1 -1
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +41 -0
- data/test/generators/maquina/passkeys_generator_test.rb +14 -0
- data/test/tmp/app/views/registration/verifications/new.html.erb +43 -0
- data/test/tmp/app/views/registrations/new.html.erb +39 -0
- data/test/tmp/app/views/session/verifications/new.html.erb +43 -0
- data/test/tmp/app/views/sessions/new.html.erb +40 -0
- data/test/tmp/app/views/verification_mailer/verification_code.html.erb +37 -0
- data/test/tmp/app/views/verification_mailer/verification_code.text.erb +11 -0
- metadata +11 -5
- /data/test/tmp/db/migrate/{20261014212613_create_accounts.rb → 20261019191386_create_accounts.rb} +0 -0
- /data/test/tmp/db/migrate/{20261014212614_create_users.rb → 20261019191387_create_users.rb} +0 -0
- /data/test/tmp/db/migrate/{20261014212615_create_sessions.rb → 20261019191388_create_sessions.rb} +0 -0
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 76d3dc4690cd7ff89b935e2289d7032084e426bda2e2f2998ed0aa81c0bb5fef
|
|
4
|
+
data.tar.gz: d983ec5b7183219b998a4447215808edf1d492c01cd344c957e9e9674036a3c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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: "
|
|
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 %>
|
|
@@ -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.
|
|
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/
|
|
314
|
-
- test/tmp/db/migrate/
|
|
315
|
-
- test/tmp/db/migrate/
|
|
316
|
-
- test/tmp/db/migrate/
|
|
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:
|
/data/test/tmp/db/migrate/{20261014212613_create_accounts.rb → 20261019191386_create_accounts.rb}
RENAMED
|
File without changes
|
|
File without changes
|
/data/test/tmp/db/migrate/{20261014212615_create_sessions.rb → 20261019191388_create_sessions.rb}
RENAMED
|
File without changes
|