maquina-generators 0.7.0 → 0.8.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +53 -0
  3. data/lib/generators/maquina/clave/templates/app/views/sessions/new.html.erb.tt +2 -0
  4. data/lib/generators/maquina/passkeys/USAGE +18 -0
  5. data/lib/generators/maquina/passkeys/passkeys_generator.rb +259 -0
  6. data/lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_challenge.rb +42 -0
  7. data/lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_reauthentication.rb +26 -0
  8. data/lib/generators/maquina/passkeys/templates/app/controllers/passkey_challenges_controller.rb +24 -0
  9. data/lib/generators/maquina/passkeys/templates/app/controllers/passkeys_controller.rb +68 -0
  10. data/lib/generators/maquina/passkeys/templates/app/controllers/session/passkey_challenges_controller.rb +18 -0
  11. data/lib/generators/maquina/passkeys/templates/app/controllers/session/passkeys_controller.rb +55 -0
  12. data/lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_registration_controller.js +109 -0
  13. data/lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_sign_in_controller.js +113 -0
  14. data/lib/generators/maquina/passkeys/templates/app/models/passkey.rb +36 -0
  15. data/lib/generators/maquina/passkeys/templates/app/views/passkeys/index.html.erb +77 -0
  16. data/lib/generators/maquina/passkeys/templates/app/views/sessions/_passkey.html.erb +23 -0
  17. data/lib/generators/maquina/passkeys/templates/config/initializers/webauthn.rb +23 -0
  18. data/lib/generators/maquina/passkeys/templates/config/locales/passkeys.en.yml +46 -0
  19. data/lib/generators/maquina/passkeys/templates/config/locales/passkeys.es.yml +46 -0
  20. data/lib/generators/maquina/passkeys/templates/migration_add_webauthn_id_to_users.rb.tt +6 -0
  21. data/lib/generators/maquina/passkeys/templates/migration_create_passkeys.rb.tt +20 -0
  22. data/lib/generators/maquina/passkeys/templates/test/integration/passkeys_test.rb +99 -0
  23. data/lib/generators/maquina/passkeys/templates/test/test_helpers/webauthn_test_helper.rb +38 -0
  24. data/lib/maquina_generators/version.rb +1 -1
  25. data/test/generators/maquina/clave_generator_test.rb +13 -2
  26. data/test/generators/maquina/passkeys_generator_test.rb +253 -0
  27. data/test/tmp/Gemfile +1 -14
  28. data/test/tmp/app/controllers/application_controller.rb +3 -0
  29. data/test/tmp/app/controllers/concerns/authentication.rb +63 -0
  30. data/test/tmp/app/controllers/registration/verification_resends_controller.rb +38 -0
  31. data/test/tmp/app/controllers/registration/verifications_controller.rb +64 -0
  32. data/test/tmp/app/controllers/registrations_controller.rb +63 -0
  33. data/test/tmp/app/controllers/session/verification_resends_controller.rb +44 -0
  34. data/test/tmp/app/controllers/session/verifications_controller.rb +64 -0
  35. data/test/tmp/app/controllers/sessions_controller.rb +56 -0
  36. data/test/tmp/app/helpers/authentication_helper.rb +20 -0
  37. data/test/tmp/app/jobs/authentication_cleanup_job.rb +13 -0
  38. data/test/tmp/app/mailers/verification_mailer.rb +15 -0
  39. data/test/tmp/app/models/account.rb +5 -0
  40. data/test/tmp/app/models/current.rb +5 -0
  41. data/test/tmp/app/models/email_verification.rb +58 -0
  42. data/test/tmp/app/models/session.rb +18 -0
  43. data/test/tmp/app/models/user.rb +37 -0
  44. data/test/tmp/config/locales/clave.en.yml +102 -0
  45. data/test/tmp/config/locales/clave.es.yml +102 -0
  46. data/test/tmp/config/routes.rb +10 -1
  47. data/test/tmp/db/migrate/20261014212613_create_accounts.rb +9 -0
  48. data/test/tmp/db/migrate/20261014212614_create_users.rb +17 -0
  49. data/test/tmp/db/migrate/20261014212615_create_sessions.rb +12 -0
  50. data/test/tmp/db/migrate/20261014212616_create_email_verifications.rb +17 -0
  51. data/test/tmp/test/test_helpers/session_test_helper.rb +19 -0
  52. metadata +48 -19
  53. data/test/tmp/.gitignore +0 -5
  54. data/test/tmp/.rubocop.yml +0 -11
  55. data/test/tmp/.standard.yml +0 -1
  56. data/test/tmp/Procfile.dev +0 -3
  57. data/test/tmp/README.md +0 -89
  58. data/test/tmp/app/controllers/home_controller.rb +0 -6
  59. data/test/tmp/app/javascript/application.js +0 -1
  60. data/test/tmp/app/views/home/index.html.erb +0 -204
  61. data/test/tmp/app/views/layouts/application.html.erb +0 -10
  62. data/test/tmp/config/application.rb +0 -15
  63. data/test/tmp/config/database.yml +0 -90
  64. data/test/tmp/config/database.yml.example +0 -90
  65. data/test/tmp/config/environments/development.rb +0 -8
  66. data/test/tmp/config/environments/production.rb +0 -6
  67. data/test/tmp/config/initializers/generators.rb +0 -4
@@ -0,0 +1,56 @@
1
+ class SessionsController < ApplicationController
2
+ allow_unauthenticated_access only: %i[new create]
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_path, alert: t("flash.general.rate_limited") }
4
+
5
+ # GET /session/new
6
+ def new
7
+ end
8
+
9
+ # POST /session
10
+ def create
11
+ email = params[:email_address]&.downcase&.strip
12
+ user = User.find_by(email_address: email)
13
+
14
+ # Check cooldown regardless of user existence
15
+ recent_verification = EmailVerification.for_email(email).recent.first
16
+ if recent_verification
17
+ session[:pending_verification_id] = recent_verification.id
18
+ session[:pending_login_email] = email
19
+ redirect_to new_session_verification_path,
20
+ notice: t("flash.sessions.code_already_sent", minutes: recent_verification.minutes_until_resend)
21
+ return
22
+ end
23
+
24
+ if user && !user.blocked?
25
+ # User exists and not blocked - send verification
26
+ verification = EmailVerification.create!(
27
+ email: email,
28
+ code: SecureRandom.hex(3).upcase,
29
+ verification_type: "login",
30
+ locale: user.locale || I18n.default_locale.to_s,
31
+ expires_at: 15.minutes.from_now
32
+ )
33
+
34
+ Rails.logger.info "[AUTH] Verification code sent for login #{email} from #{request.remote_ip}"
35
+ VerificationMailer.verification_code(verification).deliver_later
36
+ session[:pending_verification_id] = verification.id
37
+ else
38
+ # No user OR blocked - don't send email, but show same UI
39
+ if user&.blocked?
40
+ Rails.logger.info "[AUTH] Login attempted for blocked user #{email} from #{request.remote_ip}"
41
+ else
42
+ Rails.logger.info "[AUTH] Login attempted for non-existent email #{email} from #{request.remote_ip}"
43
+ end
44
+ session[:pending_login_email] = email
45
+ end
46
+
47
+ redirect_to new_session_verification_path
48
+ end
49
+
50
+ # DELETE /session
51
+ def destroy
52
+ Rails.logger.info "[AUTH] Logout for user #{Current.session.user.email_address} from #{request.remote_ip}"
53
+ terminate_session
54
+ redirect_to new_session_path, notice: t("flash.sessions.destroy.success"), status: :see_other
55
+ end
56
+ end
@@ -0,0 +1,20 @@
1
+ module AuthenticationHelper
2
+ # Masks an email address for privacy display
3
+ # "mario@example.com" -> "ma***@example.com"
4
+ # "jo@example.com" -> "j*@example.com"
5
+ # "a@example.com" -> "a@example.com"
6
+ def mask_email(email)
7
+ return "" if email.blank?
8
+
9
+ local, domain = email.split("@")
10
+ return email if local.nil? || domain.nil?
11
+
12
+ masked_local = if local.length <= 2
13
+ local[0] + "*" * (local.length - 1)
14
+ else
15
+ local[0..1] + "*" * (local.length - 2)
16
+ end
17
+
18
+ "#{masked_local}@#{domain}"
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ class AuthenticationCleanupJob < ApplicationJob
2
+ queue_as :default
3
+
4
+ def perform
5
+ # Delete email verifications older than 24 hours
6
+ old_verifications_count = EmailVerification.where("created_at < ?", 24.hours.ago).delete_all
7
+ Rails.logger.info "AuthenticationCleanupJob: Deleted #{old_verifications_count} old email verifications"
8
+
9
+ # Delete expired sessions
10
+ expired_sessions_count = Session.cleanup_expired!
11
+ Rails.logger.info "AuthenticationCleanupJob: Deleted #{expired_sessions_count} expired sessions"
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class VerificationMailer < ApplicationMailer
2
+ default from: "App <noreply@example.com>"
3
+
4
+ def verification_code(email_verification)
5
+ @verification = email_verification
6
+ locale = email_verification.locale || I18n.default_locale.to_s
7
+
8
+ I18n.with_locale(locale) do
9
+ mail(
10
+ to: email_verification.email,
11
+ subject: I18n.t("verification_mailer.verification_code.subject")
12
+ )
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ class Account < ApplicationRecord
2
+ has_many :users, dependent: :destroy
3
+
4
+ validates :name, allow_blank: true, length: { maximum: 100 }
5
+ end
@@ -0,0 +1,5 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+ delegate :user, to: :session, allow_nil: true
4
+ delegate :account, to: :user, allow_nil: true
5
+ end
@@ -0,0 +1,58 @@
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
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
@@ -0,0 +1,37 @@
1
+ class User < ApplicationRecord
2
+ belongs_to :account
3
+ has_many :sessions, dependent: :destroy
4
+
5
+ enum :role, {member: "member", admin: "admin"}, default: "member"
6
+
7
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
8
+
9
+ validates :email_address, uniqueness: true
10
+ validates :name, allow_blank: true, length: { maximum: 100 }
11
+ validate :email_cannot_contain_plus
12
+
13
+ scope :active, -> { where(blocked_at: nil) }
14
+ scope :blocked, -> { where.not(blocked_at: nil) }
15
+
16
+ def blocked?
17
+ blocked_at.present?
18
+ end
19
+
20
+ def block!(reason: nil)
21
+ update!(blocked_at: Time.current, blocked_reason: reason)
22
+ end
23
+
24
+ def unblock!
25
+ update!(blocked_at: nil, blocked_reason: nil)
26
+ end
27
+
28
+ private
29
+
30
+ def email_cannot_contain_plus
31
+ return if email_address.blank?
32
+
33
+ if email_address.include?("+")
34
+ errors.add(:email_address, :plus_not_allowed)
35
+ end
36
+ end
37
+ end
@@ -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,3 +1,12 @@
1
1
  Rails.application.routes.draw do
2
- root "home#index"
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"
11
+ end
3
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
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.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Alberto Chavez
@@ -66,9 +66,9 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0.8'
68
68
  description: 'Rails generators that write standalone code into your app: passwordless
69
- (clave) and password (registration) authentication, Rack::Attack rules with a Security
70
- dashboard, Content Security Policy reports, Solid Errors, Mission Control Jobs,
71
- Solid Queue, and a full app setup.'
69
+ (clave) and password (registration) authentication, passkeys, Rack::Attack rules
70
+ with a Security dashboard, Content Security Policy reports, Solid Errors, Mission
71
+ Control Jobs, Solid Queue, and a full app setup.'
72
72
  email:
73
73
  - mario.chavez@gmail.com
74
74
  executables: []
@@ -180,6 +180,26 @@ files:
180
180
  - lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/workers/index.html.erb
181
181
  - lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/workers/show.html.erb
182
182
  - lib/generators/maquina/mission_control_jobs/templates/config/initializers/mission_control.rb.tt
183
+ - lib/generators/maquina/passkeys/USAGE
184
+ - lib/generators/maquina/passkeys/passkeys_generator.rb
185
+ - lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_challenge.rb
186
+ - lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_reauthentication.rb
187
+ - lib/generators/maquina/passkeys/templates/app/controllers/passkey_challenges_controller.rb
188
+ - lib/generators/maquina/passkeys/templates/app/controllers/passkeys_controller.rb
189
+ - lib/generators/maquina/passkeys/templates/app/controllers/session/passkey_challenges_controller.rb
190
+ - lib/generators/maquina/passkeys/templates/app/controllers/session/passkeys_controller.rb
191
+ - lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_registration_controller.js
192
+ - lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_sign_in_controller.js
193
+ - lib/generators/maquina/passkeys/templates/app/models/passkey.rb
194
+ - lib/generators/maquina/passkeys/templates/app/views/passkeys/index.html.erb
195
+ - lib/generators/maquina/passkeys/templates/app/views/sessions/_passkey.html.erb
196
+ - lib/generators/maquina/passkeys/templates/config/initializers/webauthn.rb
197
+ - lib/generators/maquina/passkeys/templates/config/locales/passkeys.en.yml
198
+ - lib/generators/maquina/passkeys/templates/config/locales/passkeys.es.yml
199
+ - lib/generators/maquina/passkeys/templates/migration_add_webauthn_id_to_users.rb.tt
200
+ - lib/generators/maquina/passkeys/templates/migration_create_passkeys.rb.tt
201
+ - lib/generators/maquina/passkeys/templates/test/integration/passkeys_test.rb
202
+ - lib/generators/maquina/passkeys/templates/test/test_helpers/webauthn_test_helper.rb
183
203
  - lib/generators/maquina/rack_attack/USAGE
184
204
  - lib/generators/maquina/rack_attack/rack_attack_generator.rb
185
205
  - lib/generators/maquina/rack_attack/templates/config/initializers/rack_attack.rb.tt
@@ -263,29 +283,38 @@ files:
263
283
  - test/generators/maquina/csp_reports_generator_test.rb
264
284
  - test/generators/maquina/failures_mcp_tools_test.rb
265
285
  - test/generators/maquina/mission_control_jobs_generator_test.rb
286
+ - test/generators/maquina/passkeys_generator_test.rb
266
287
  - test/generators/maquina/rack_attack_generator_test.rb
267
288
  - test/generators/maquina/registration_generator_test.rb
268
289
  - test/generators/maquina/security_generator_test.rb
269
290
  - test/generators/maquina/solid_errors_generator_test.rb
270
291
  - test/generators/maquina/solid_queue_generator_test.rb
271
292
  - test/test_helper.rb
272
- - test/tmp/.gitignore
273
- - test/tmp/.rubocop.yml
274
- - test/tmp/.standard.yml
275
293
  - test/tmp/Gemfile
276
- - test/tmp/Procfile.dev
277
- - test/tmp/README.md
278
- - test/tmp/app/controllers/home_controller.rb
279
- - test/tmp/app/javascript/application.js
280
- - test/tmp/app/views/home/index.html.erb
281
- - test/tmp/app/views/layouts/application.html.erb
282
- - test/tmp/config/application.rb
283
- - test/tmp/config/database.yml
284
- - test/tmp/config/database.yml.example
285
- - test/tmp/config/environments/development.rb
286
- - test/tmp/config/environments/production.rb
287
- - test/tmp/config/initializers/generators.rb
294
+ - test/tmp/app/controllers/application_controller.rb
295
+ - test/tmp/app/controllers/concerns/authentication.rb
296
+ - test/tmp/app/controllers/registration/verification_resends_controller.rb
297
+ - test/tmp/app/controllers/registration/verifications_controller.rb
298
+ - test/tmp/app/controllers/registrations_controller.rb
299
+ - test/tmp/app/controllers/session/verification_resends_controller.rb
300
+ - test/tmp/app/controllers/session/verifications_controller.rb
301
+ - test/tmp/app/controllers/sessions_controller.rb
302
+ - test/tmp/app/helpers/authentication_helper.rb
303
+ - test/tmp/app/jobs/authentication_cleanup_job.rb
304
+ - test/tmp/app/mailers/verification_mailer.rb
305
+ - test/tmp/app/models/account.rb
306
+ - test/tmp/app/models/current.rb
307
+ - test/tmp/app/models/email_verification.rb
308
+ - test/tmp/app/models/session.rb
309
+ - test/tmp/app/models/user.rb
310
+ - test/tmp/config/locales/clave.en.yml
311
+ - test/tmp/config/locales/clave.es.yml
288
312
  - 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
317
+ - test/tmp/test/test_helpers/session_test_helper.rb
289
318
  homepage: https://maquina.app
290
319
  licenses:
291
320
  - MIT
data/test/tmp/.gitignore DELETED
@@ -1,5 +0,0 @@
1
- /log/*
2
- /tmp/*
3
-
4
- # Ignore database configuration
5
- config/database.yml
@@ -1,11 +0,0 @@
1
- # standard.rb's runner bridge, not custom RuboCop rules.
2
- #
3
- # This is the entry point RuboCop (and editors/LSP) need to run the Standard
4
- # ruleset — all style comes from the `standard` gem via config/base.yml below.
5
- # Keep this file even though general guidance says "no .rubocop.yml": for
6
- # standard.rb this IS the runner config, not a custom rule set.
7
- require:
8
- - standard
9
-
10
- inherit_gem:
11
- standard: config/base.yml
@@ -1 +0,0 @@
1
- ruby_version: 3.2