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.
Files changed (61) 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/maquina_generators/version.rb +1 -1
  9. data/test/generators/maquina/app_generator_test.rb +41 -0
  10. data/test/tmp/Gemfile +1 -2
  11. data/test/tmp/app/controllers/application_controller.rb +3 -0
  12. data/test/tmp/app/controllers/concerns/authentication.rb +61 -0
  13. data/test/tmp/app/controllers/registration/verification_resends_controller.rb +38 -0
  14. data/test/tmp/app/controllers/registration/verifications_controller.rb +64 -0
  15. data/test/tmp/app/controllers/registrations_controller.rb +63 -0
  16. data/test/tmp/app/controllers/session/verification_resends_controller.rb +44 -0
  17. data/test/tmp/app/controllers/session/verifications_controller.rb +64 -0
  18. data/test/tmp/app/controllers/sessions_controller.rb +56 -0
  19. data/test/tmp/app/helpers/authentication_helper.rb +20 -0
  20. data/test/tmp/app/jobs/authentication_cleanup_job.rb +13 -0
  21. data/test/tmp/app/mailers/verification_mailer.rb +15 -0
  22. data/test/tmp/app/models/account.rb +5 -0
  23. data/test/tmp/app/models/current.rb +5 -0
  24. data/test/tmp/app/models/email_verification.rb +56 -0
  25. data/test/tmp/app/models/session.rb +18 -0
  26. data/test/tmp/app/models/user.rb +0 -10
  27. data/test/tmp/app/views/registration/verifications/new.html.erb +43 -0
  28. data/test/tmp/app/views/registrations/new.html.erb +39 -0
  29. data/test/tmp/app/views/session/verifications/new.html.erb +43 -0
  30. data/test/tmp/app/views/sessions/new.html.erb +5 -2
  31. data/test/tmp/app/views/verification_mailer/verification_code.html.erb +37 -0
  32. data/test/tmp/app/views/verification_mailer/verification_code.text.erb +11 -0
  33. data/test/tmp/config/locales/clave.en.yml +102 -0
  34. data/test/tmp/config/locales/clave.es.yml +102 -0
  35. data/test/tmp/config/routes.rb +9 -6
  36. data/test/tmp/db/migrate/20261019191386_create_accounts.rb +9 -0
  37. data/test/tmp/db/migrate/20261019191387_create_users.rb +17 -0
  38. data/test/tmp/db/migrate/20261019191388_create_sessions.rb +12 -0
  39. data/test/tmp/db/migrate/20261019191389_create_email_verifications.rb +17 -0
  40. data/test/tmp/test/test_helpers/session_test_helper.rb +19 -0
  41. metadata +26 -21
  42. data/test/tmp/app/controllers/concerns/passkey_challenge.rb +0 -42
  43. data/test/tmp/app/controllers/concerns/passkey_reauthentication.rb +0 -26
  44. data/test/tmp/app/controllers/passkey_challenges_controller.rb +0 -24
  45. data/test/tmp/app/controllers/passkeys_controller.rb +0 -68
  46. data/test/tmp/app/controllers/session/passkey_challenges_controller.rb +0 -18
  47. data/test/tmp/app/controllers/session/passkeys_controller.rb +0 -55
  48. data/test/tmp/app/javascript/controllers/passkey_registration_controller.js +0 -109
  49. data/test/tmp/app/javascript/controllers/passkey_sign_in_controller.js +0 -113
  50. data/test/tmp/app/models/passkey.rb +0 -36
  51. data/test/tmp/app/views/passkeys/index.html.erb +0 -77
  52. data/test/tmp/app/views/sessions/_passkey.html.erb +0 -23
  53. data/test/tmp/config/initializers/rack_attack.rb +0 -9
  54. data/test/tmp/config/initializers/webauthn.rb +0 -23
  55. data/test/tmp/config/locales/passkeys.en.yml +0 -46
  56. data/test/tmp/config/locales/passkeys.es.yml +0 -46
  57. data/test/tmp/db/migrate/20261014213621_add_webauthn_id_to_users.rb +0 -6
  58. data/test/tmp/db/migrate/20261014213622_create_passkeys.rb +0 -20
  59. data/test/tmp/test/integration/passkeys_test.rb +0 -99
  60. data/test/tmp/test/test_helper.rb +0 -4
  61. 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
@@ -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: "username webauthn",
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
- <%= render "sessions/passkey" %>
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."
@@ -1,9 +1,12 @@
1
1
  Rails.application.routes.draw do
2
- # Passkeys (generated by maquina:passkeys)
3
- resource :session, only: [] do
4
- resource :passkey_challenge, only: :create, controller: "session/passkey_challenges"
5
- resource :passkey, only: :create, controller: "session/passkeys"
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,9 @@
1
+ class CreateAccounts < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :accounts do |t|
4
+ t.string :name, null: false
5
+
6
+ t.timestamps
7
+ end
8
+ end
9
+ 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