maquina-generators 0.8.1 → 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/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +41 -0
- data/test/tmp/Gemfile +1 -2
- data/test/tmp/app/controllers/application_controller.rb +3 -0
- data/test/tmp/app/controllers/concerns/authentication.rb +61 -0
- data/test/tmp/app/controllers/registration/verification_resends_controller.rb +38 -0
- data/test/tmp/app/controllers/registration/verifications_controller.rb +64 -0
- data/test/tmp/app/controllers/registrations_controller.rb +63 -0
- data/test/tmp/app/controllers/session/verification_resends_controller.rb +44 -0
- data/test/tmp/app/controllers/session/verifications_controller.rb +64 -0
- data/test/tmp/app/controllers/sessions_controller.rb +56 -0
- data/test/tmp/app/helpers/authentication_helper.rb +20 -0
- data/test/tmp/app/jobs/authentication_cleanup_job.rb +13 -0
- data/test/tmp/app/mailers/verification_mailer.rb +15 -0
- data/test/tmp/app/models/account.rb +5 -0
- data/test/tmp/app/models/current.rb +5 -0
- data/test/tmp/app/models/email_verification.rb +56 -0
- data/test/tmp/app/models/session.rb +18 -0
- data/test/tmp/app/models/user.rb +0 -10
- 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 +5 -2
- 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
- data/test/tmp/config/locales/clave.en.yml +102 -0
- data/test/tmp/config/locales/clave.es.yml +102 -0
- data/test/tmp/config/routes.rb +9 -6
- data/test/tmp/db/migrate/20261019191386_create_accounts.rb +9 -0
- data/test/tmp/db/migrate/20261019191387_create_users.rb +17 -0
- data/test/tmp/db/migrate/20261019191388_create_sessions.rb +12 -0
- data/test/tmp/db/migrate/20261019191389_create_email_verifications.rb +17 -0
- data/test/tmp/test/test_helpers/session_test_helper.rb +19 -0
- metadata +26 -21
- data/test/tmp/app/controllers/concerns/passkey_challenge.rb +0 -42
- data/test/tmp/app/controllers/concerns/passkey_reauthentication.rb +0 -26
- data/test/tmp/app/controllers/passkey_challenges_controller.rb +0 -24
- data/test/tmp/app/controllers/passkeys_controller.rb +0 -68
- data/test/tmp/app/controllers/session/passkey_challenges_controller.rb +0 -18
- data/test/tmp/app/controllers/session/passkeys_controller.rb +0 -55
- data/test/tmp/app/javascript/controllers/passkey_registration_controller.js +0 -109
- data/test/tmp/app/javascript/controllers/passkey_sign_in_controller.js +0 -113
- data/test/tmp/app/models/passkey.rb +0 -36
- data/test/tmp/app/views/passkeys/index.html.erb +0 -77
- data/test/tmp/app/views/sessions/_passkey.html.erb +0 -23
- data/test/tmp/config/initializers/rack_attack.rb +0 -9
- data/test/tmp/config/initializers/webauthn.rb +0 -23
- data/test/tmp/config/locales/passkeys.en.yml +0 -46
- data/test/tmp/config/locales/passkeys.es.yml +0 -46
- data/test/tmp/db/migrate/20261014213621_add_webauthn_id_to_users.rb +0 -6
- data/test/tmp/db/migrate/20261014213622_create_passkeys.rb +0 -20
- data/test/tmp/test/integration/passkeys_test.rb +0 -99
- data/test/tmp/test/test_helper.rb +0 -4
- data/test/tmp/test/test_helpers/webauthn_test_helper.rb +0 -38
|
@@ -1,2 +1,58 @@
|
|
|
1
1
|
class EmailVerification < ApplicationRecord
|
|
2
|
+
VERIFICATION_TYPES = %w[signup login].freeze
|
|
3
|
+
COOLDOWN_MINUTES = 15
|
|
4
|
+
MAX_ATTEMPTS = 5
|
|
5
|
+
|
|
6
|
+
validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
|
|
7
|
+
validates :code, presence: true
|
|
8
|
+
validates :verification_type, presence: true, inclusion: {in: VERIFICATION_TYPES}
|
|
9
|
+
validates :expires_at, presence: true
|
|
10
|
+
|
|
11
|
+
normalizes :email, with: ->(email) { email.strip.downcase }
|
|
12
|
+
normalizes :code, with: ->(code) { code.strip.upcase }
|
|
13
|
+
|
|
14
|
+
# Encrypted at rest, not digested: the mail goes out through deliver_later,
|
|
15
|
+
# whose job reloads this row by GlobalID and must read the code back.
|
|
16
|
+
encrypts :code
|
|
17
|
+
|
|
18
|
+
scope :pending, -> { where(verified_at: nil) }
|
|
19
|
+
scope :expired, -> { where("expires_at < ?", Time.current) }
|
|
20
|
+
scope :active, -> { pending.where("expires_at >= ?", Time.current) }
|
|
21
|
+
scope :for_email, ->(email) { where("LOWER(email) = ?", email.to_s.downcase) }
|
|
22
|
+
scope :recent, -> { pending.where("created_at > ?", COOLDOWN_MINUTES.minutes.ago) }
|
|
23
|
+
|
|
24
|
+
def expired?
|
|
25
|
+
expires_at < Time.current
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# A code is single-use against a bounded number of guesses: once the wrong-code
|
|
29
|
+
# count hits the ceiling the record is dead regardless of how many IPs an
|
|
30
|
+
# attacker rotates through, since each code only ever maps to one row.
|
|
31
|
+
def locked?
|
|
32
|
+
attempts >= MAX_ATTEMPTS
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Constant-time, so response timing reveals nothing about how close a guess
|
|
36
|
+
# came. Normalized the same way the stored code is.
|
|
37
|
+
def code_matches?(candidate)
|
|
38
|
+
ActiveSupport::SecurityUtils.secure_compare(code, candidate.to_s.strip.upcase)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def verified?
|
|
42
|
+
verified_at.present?
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def can_resend?
|
|
46
|
+
created_at <= COOLDOWN_MINUTES.minutes.ago
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def minutes_until_resend
|
|
50
|
+
return 0 if can_resend?
|
|
51
|
+
|
|
52
|
+
((created_at + COOLDOWN_MINUTES.minutes - Time.current) / 60).ceil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def mark_verified!
|
|
56
|
+
update!(verified_at: Time.current)
|
|
57
|
+
end
|
|
2
58
|
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class Session < ApplicationRecord
|
|
2
|
+
belongs_to :user
|
|
3
|
+
|
|
4
|
+
scope :active, -> { where("expires_at > ? OR expires_at IS NULL", Time.current) }
|
|
5
|
+
scope :expired, -> { where("expires_at <= ? AND expires_at IS NOT NULL", Time.current) }
|
|
6
|
+
|
|
7
|
+
def self.cleanup_expired!
|
|
8
|
+
expired.delete_all
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def expired?
|
|
12
|
+
expires_at.present? && expires_at < Time.current
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def active?
|
|
16
|
+
!expired?
|
|
17
|
+
end
|
|
18
|
+
end
|
data/test/tmp/app/models/user.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
class User < ApplicationRecord
|
|
2
2
|
belongs_to :account
|
|
3
3
|
has_many :sessions, dependent: :destroy
|
|
4
|
-
has_many :passkeys, dependent: :destroy
|
|
5
4
|
|
|
6
5
|
enum :role, {member: "member", admin: "admin"}, default: "member"
|
|
7
6
|
|
|
@@ -26,15 +25,6 @@ class User < ApplicationRecord
|
|
|
26
25
|
update!(blocked_at: nil, blocked_reason: nil)
|
|
27
26
|
end
|
|
28
27
|
|
|
29
|
-
# The WebAuthn user handle: random and stable, never the id or the email,
|
|
30
|
-
# since authenticators store it and hand it back on every sign-in.
|
|
31
|
-
def webauthn_id
|
|
32
|
-
super || begin
|
|
33
|
-
update_column(:webauthn_id, WebAuthn.generate_user_id)
|
|
34
|
-
super
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
28
|
private
|
|
39
29
|
|
|
40
30
|
def email_cannot_contain_plus
|
|
@@ -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>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
<%= form.email_field :email_address,
|
|
12
12
|
required: true,
|
|
13
13
|
autofocus: true,
|
|
14
|
-
autocomplete: "
|
|
14
|
+
autocomplete: "email",
|
|
15
15
|
placeholder: t(".email_placeholder"),
|
|
16
16
|
value: params[:email_address],
|
|
17
17
|
data: { component: "input" },
|
|
@@ -31,7 +31,10 @@
|
|
|
31
31
|
</div>
|
|
32
32
|
<% end %>
|
|
33
33
|
|
|
34
|
-
|
|
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>
|
|
35
38
|
</div>
|
|
36
39
|
</div>
|
|
37
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") %>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
en:
|
|
2
|
+
flash:
|
|
3
|
+
sessions:
|
|
4
|
+
create:
|
|
5
|
+
success: "You have signed in successfully."
|
|
6
|
+
destroy:
|
|
7
|
+
success: "You have signed out successfully."
|
|
8
|
+
code_sent: "We sent you a verification code."
|
|
9
|
+
invalid_code: "The code is incorrect. Please try again."
|
|
10
|
+
code_expired: "The code has expired. Request a new one."
|
|
11
|
+
code_locked: "Too many incorrect tries. Request a new code to keep going."
|
|
12
|
+
code_resent: "We sent you a new code."
|
|
13
|
+
cooldown: "You must wait %{minutes} minutes before requesting another code."
|
|
14
|
+
code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
|
|
15
|
+
registrations:
|
|
16
|
+
create:
|
|
17
|
+
success: "Your account has been created. Welcome!"
|
|
18
|
+
code_sent: "We sent a verification code to your email."
|
|
19
|
+
email_plus_not_allowed: "Email addresses with '+' are not allowed. Use your main email without aliases."
|
|
20
|
+
email_taken: "An account with this email already exists. Would you like to sign in?"
|
|
21
|
+
invalid_code: "The code is incorrect. Please try again."
|
|
22
|
+
code_expired: "The code has expired. Request a new one."
|
|
23
|
+
code_locked: "Too many incorrect tries. Request a new code to keep going."
|
|
24
|
+
code_resent: "We sent you a new code."
|
|
25
|
+
cooldown: "You must wait %{minutes} minutes before requesting another code."
|
|
26
|
+
code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
|
|
27
|
+
general:
|
|
28
|
+
unauthorized: "You must sign in to access this page."
|
|
29
|
+
forbidden: "You are not authorized to perform this action."
|
|
30
|
+
not_found: "The resource you're looking for doesn't exist."
|
|
31
|
+
rate_limited: "Too many attempts. Please wait a few minutes and try again."
|
|
32
|
+
|
|
33
|
+
sessions:
|
|
34
|
+
new:
|
|
35
|
+
title: "Sign in"
|
|
36
|
+
email: "Email address"
|
|
37
|
+
email_placeholder: "you@example.com"
|
|
38
|
+
email_instructions: "Enter your email and we'll send you a 6-digit code."
|
|
39
|
+
spam_note: "Check your spam folder if you don't receive the email."
|
|
40
|
+
submit: "Send code"
|
|
41
|
+
no_account: "Don't have an account?"
|
|
42
|
+
sign_up: "Sign up"
|
|
43
|
+
|
|
44
|
+
session:
|
|
45
|
+
verifications:
|
|
46
|
+
new:
|
|
47
|
+
title: "Verify your identity"
|
|
48
|
+
sent_to: "If you have an account, we sent a code to %{email}"
|
|
49
|
+
enter_code: "Enter the code"
|
|
50
|
+
submit: "Sign in"
|
|
51
|
+
check_spam: "Check your spam folder."
|
|
52
|
+
didnt_receive: "Didn't receive the code?"
|
|
53
|
+
resend: "Resend code"
|
|
54
|
+
|
|
55
|
+
registrations:
|
|
56
|
+
new:
|
|
57
|
+
title: "Create account"
|
|
58
|
+
email: "Email address"
|
|
59
|
+
email_placeholder: "you@example.com"
|
|
60
|
+
email_instructions: "Enter your email address. We'll send you a 6-digit code to verify your account."
|
|
61
|
+
spam_note: "Check your spam folder if you don't receive the email."
|
|
62
|
+
submit: "Send code"
|
|
63
|
+
have_account: "Already have an account?"
|
|
64
|
+
sign_in: "Sign in"
|
|
65
|
+
|
|
66
|
+
registration:
|
|
67
|
+
verifications:
|
|
68
|
+
new:
|
|
69
|
+
title: "Verify your email"
|
|
70
|
+
sent_to: "We sent a 6-digit code to %{email}"
|
|
71
|
+
enter_code: "Enter the code"
|
|
72
|
+
submit: "Verify"
|
|
73
|
+
check_spam: "Check your spam folder if you don't receive it."
|
|
74
|
+
didnt_receive: "Didn't receive the code?"
|
|
75
|
+
resend: "Resend code"
|
|
76
|
+
|
|
77
|
+
verification_mailer:
|
|
78
|
+
verification_code:
|
|
79
|
+
subject: "Your verification code"
|
|
80
|
+
title: "Verification code"
|
|
81
|
+
instructions: "Enter this code to continue:"
|
|
82
|
+
expires_in: "This code expires in 15 minutes."
|
|
83
|
+
ignore_if_not_you: "If you didn't request this code, you can ignore this email."
|
|
84
|
+
|
|
85
|
+
activerecord:
|
|
86
|
+
models:
|
|
87
|
+
user:
|
|
88
|
+
one: "User"
|
|
89
|
+
other: "Users"
|
|
90
|
+
attributes:
|
|
91
|
+
user:
|
|
92
|
+
email_address: "Email address"
|
|
93
|
+
name: "Name"
|
|
94
|
+
locale: "Language"
|
|
95
|
+
blocked_at: "Blocked at"
|
|
96
|
+
blocked_reason: "Blocked reason"
|
|
97
|
+
errors:
|
|
98
|
+
models:
|
|
99
|
+
user:
|
|
100
|
+
attributes:
|
|
101
|
+
email_address:
|
|
102
|
+
plus_not_allowed: "Email addresses with '+' are not allowed. Please use your main email without aliases."
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
es:
|
|
2
|
+
flash:
|
|
3
|
+
sessions:
|
|
4
|
+
create:
|
|
5
|
+
success: "Has iniciado sesion correctamente."
|
|
6
|
+
destroy:
|
|
7
|
+
success: "Has cerrado sesion correctamente."
|
|
8
|
+
code_sent: "Te enviamos un codigo de verificacion."
|
|
9
|
+
invalid_code: "El codigo es incorrecto. Por favor, intenta de nuevo."
|
|
10
|
+
code_expired: "El codigo ha expirado. Solicita uno nuevo."
|
|
11
|
+
code_locked: "Demasiados intentos incorrectos. Solicita un codigo nuevo para continuar."
|
|
12
|
+
code_resent: "Te enviamos un nuevo codigo."
|
|
13
|
+
cooldown: "Debes esperar %{minutes} minutos antes de solicitar otro codigo."
|
|
14
|
+
code_already_sent: "Ya te enviamos un codigo. Revisa tu correo (incluyendo spam). Podras solicitar uno nuevo en %{minutes} minutos."
|
|
15
|
+
registrations:
|
|
16
|
+
create:
|
|
17
|
+
success: "Tu cuenta ha sido creada. Bienvenido!"
|
|
18
|
+
code_sent: "Te enviamos un codigo de verificacion a tu correo."
|
|
19
|
+
email_plus_not_allowed: "No se permiten correos con '+'. Usa tu direccion principal sin alias."
|
|
20
|
+
email_taken: "Ya existe una cuenta con este correo. Quieres iniciar sesion?"
|
|
21
|
+
invalid_code: "El codigo es incorrecto. Por favor, intenta de nuevo."
|
|
22
|
+
code_expired: "El codigo ha expirado. Solicita uno nuevo."
|
|
23
|
+
code_locked: "Demasiados intentos incorrectos. Solicita un codigo nuevo para continuar."
|
|
24
|
+
code_resent: "Te enviamos un nuevo codigo."
|
|
25
|
+
cooldown: "Debes esperar %{minutes} minutos antes de solicitar otro codigo."
|
|
26
|
+
code_already_sent: "Ya te enviamos un codigo. Revisa tu correo (incluyendo spam). Podras solicitar uno nuevo en %{minutes} minutos."
|
|
27
|
+
general:
|
|
28
|
+
unauthorized: "Debes iniciar sesion para acceder a esta pagina."
|
|
29
|
+
forbidden: "No tienes permiso para realizar esta accion."
|
|
30
|
+
not_found: "El recurso que buscas no existe."
|
|
31
|
+
rate_limited: "Demasiados intentos. Por favor, espera unos minutos e intenta de nuevo."
|
|
32
|
+
|
|
33
|
+
sessions:
|
|
34
|
+
new:
|
|
35
|
+
title: "Iniciar sesion"
|
|
36
|
+
email: "Correo electronico"
|
|
37
|
+
email_placeholder: "tu@correo.com"
|
|
38
|
+
email_instructions: "Ingresa tu correo y te enviaremos un codigo de 6 digitos."
|
|
39
|
+
spam_note: "Revisa tu carpeta de spam si no recibes el correo."
|
|
40
|
+
submit: "Enviar codigo"
|
|
41
|
+
no_account: "No tienes cuenta?"
|
|
42
|
+
sign_up: "Registrate"
|
|
43
|
+
|
|
44
|
+
session:
|
|
45
|
+
verifications:
|
|
46
|
+
new:
|
|
47
|
+
title: "Verifica tu identidad"
|
|
48
|
+
sent_to: "Si tienes cuenta, enviamos un codigo a %{email}"
|
|
49
|
+
enter_code: "Ingresa el codigo"
|
|
50
|
+
submit: "Iniciar sesion"
|
|
51
|
+
check_spam: "Revisa tu carpeta de spam."
|
|
52
|
+
didnt_receive: "No recibiste el codigo?"
|
|
53
|
+
resend: "Reenviar codigo"
|
|
54
|
+
|
|
55
|
+
registrations:
|
|
56
|
+
new:
|
|
57
|
+
title: "Crear cuenta"
|
|
58
|
+
email: "Correo electronico"
|
|
59
|
+
email_placeholder: "tu@correo.com"
|
|
60
|
+
email_instructions: "Ingresa tu correo electronico. Te enviaremos un codigo de 6 digitos para verificar tu cuenta."
|
|
61
|
+
spam_note: "Revisa tu carpeta de spam si no recibes el correo."
|
|
62
|
+
submit: "Enviar codigo"
|
|
63
|
+
have_account: "Ya tienes cuenta?"
|
|
64
|
+
sign_in: "Inicia sesion"
|
|
65
|
+
|
|
66
|
+
registration:
|
|
67
|
+
verifications:
|
|
68
|
+
new:
|
|
69
|
+
title: "Verifica tu correo"
|
|
70
|
+
sent_to: "Enviamos un codigo de 6 digitos a %{email}"
|
|
71
|
+
enter_code: "Ingresa el codigo"
|
|
72
|
+
submit: "Verificar"
|
|
73
|
+
check_spam: "Revisa tu carpeta de spam si no lo recibes."
|
|
74
|
+
didnt_receive: "No recibiste el codigo?"
|
|
75
|
+
resend: "Reenviar codigo"
|
|
76
|
+
|
|
77
|
+
verification_mailer:
|
|
78
|
+
verification_code:
|
|
79
|
+
subject: "Tu codigo de verificacion"
|
|
80
|
+
title: "Codigo de verificacion"
|
|
81
|
+
instructions: "Ingresa este codigo para continuar:"
|
|
82
|
+
expires_in: "Este codigo expira en 15 minutos."
|
|
83
|
+
ignore_if_not_you: "Si no solicitaste este codigo, puedes ignorar este correo."
|
|
84
|
+
|
|
85
|
+
activerecord:
|
|
86
|
+
models:
|
|
87
|
+
user:
|
|
88
|
+
one: "Usuario"
|
|
89
|
+
other: "Usuarios"
|
|
90
|
+
attributes:
|
|
91
|
+
user:
|
|
92
|
+
email_address: "Correo electronico"
|
|
93
|
+
name: "Nombre"
|
|
94
|
+
locale: "Idioma"
|
|
95
|
+
blocked_at: "Bloqueado en"
|
|
96
|
+
blocked_reason: "Razon de bloqueo"
|
|
97
|
+
errors:
|
|
98
|
+
models:
|
|
99
|
+
user:
|
|
100
|
+
attributes:
|
|
101
|
+
email_address:
|
|
102
|
+
plus_not_allowed: "No se permiten direcciones con '+'. Por favor, usa tu correo principal sin alias."
|
data/test/tmp/config/routes.rb
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
Rails.application.routes.draw do
|
|
2
|
-
#
|
|
3
|
-
resource :session, only: [] do
|
|
4
|
-
resource :
|
|
5
|
-
resource :
|
|
2
|
+
# Authentication routes (generated by maquina:clave)
|
|
3
|
+
resource :session, only: [:new, :create, :destroy] do
|
|
4
|
+
resource :verification, only: [:new, :create], controller: "session/verifications"
|
|
5
|
+
resource :verification_resend, only: [:create], controller: "session/verification_resends"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
resource :registration, only: [:new, :create] do
|
|
9
|
+
resource :verification, only: [:new, :create], controller: "registration/verifications"
|
|
10
|
+
resource :verification_resend, only: [:create], controller: "registration/verification_resends"
|
|
6
11
|
end
|
|
7
|
-
resource :passkey_challenge, only: :create
|
|
8
|
-
resources :passkeys, only: [:index, :create, :update, :destroy]
|
|
9
12
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateUsers < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :users do |t|
|
|
4
|
+
t.references :account, null: false, foreign_key: true
|
|
5
|
+
t.string :email_address, null: false
|
|
6
|
+
t.string :name
|
|
7
|
+
t.string :role, null: false, default: "member"
|
|
8
|
+
t.string :locale
|
|
9
|
+
t.datetime :blocked_at
|
|
10
|
+
t.text :blocked_reason
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :users, :email_address, unique: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class CreateSessions < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :sessions do |t|
|
|
4
|
+
t.references :user, null: false, foreign_key: true
|
|
5
|
+
t.string :ip_address
|
|
6
|
+
t.string :user_agent
|
|
7
|
+
t.datetime :expires_at
|
|
8
|
+
|
|
9
|
+
t.timestamps
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class CreateEmailVerifications < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :email_verifications do |t|
|
|
4
|
+
t.string :email, null: false
|
|
5
|
+
t.string :code, null: false
|
|
6
|
+
t.string :verification_type, null: false
|
|
7
|
+
t.datetime :expires_at, null: false
|
|
8
|
+
t.datetime :verified_at
|
|
9
|
+
t.integer :attempts, default: 0
|
|
10
|
+
t.string :locale
|
|
11
|
+
|
|
12
|
+
t.timestamps
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
add_index :email_verifications, [:email, :created_at]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module SessionTestHelper
|
|
2
|
+
def sign_in_as(user)
|
|
3
|
+
Current.session = user.sessions.create!
|
|
4
|
+
|
|
5
|
+
ActionDispatch::TestRequest.create.cookie_jar.tap do |cookie_jar|
|
|
6
|
+
cookie_jar.signed[:session_id] = Current.session.id
|
|
7
|
+
cookies["session_id"] = cookie_jar[:session_id]
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def sign_out
|
|
12
|
+
Current.session&.destroy!
|
|
13
|
+
cookies.delete("session_id")
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
18
|
+
include SessionTestHelper
|
|
19
|
+
end
|