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.
- checksums.yaml +4 -4
- data/README.md +53 -0
- data/lib/generators/maquina/clave/templates/app/views/sessions/new.html.erb.tt +2 -0
- data/lib/generators/maquina/passkeys/USAGE +18 -0
- data/lib/generators/maquina/passkeys/passkeys_generator.rb +259 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_challenge.rb +42 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/concerns/passkey_reauthentication.rb +26 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/passkey_challenges_controller.rb +24 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/passkeys_controller.rb +68 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/session/passkey_challenges_controller.rb +18 -0
- data/lib/generators/maquina/passkeys/templates/app/controllers/session/passkeys_controller.rb +55 -0
- data/lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_registration_controller.js +109 -0
- data/lib/generators/maquina/passkeys/templates/app/javascript/controllers/passkey_sign_in_controller.js +113 -0
- data/lib/generators/maquina/passkeys/templates/app/models/passkey.rb +36 -0
- data/lib/generators/maquina/passkeys/templates/app/views/passkeys/index.html.erb +77 -0
- data/lib/generators/maquina/passkeys/templates/app/views/sessions/_passkey.html.erb +23 -0
- data/lib/generators/maquina/passkeys/templates/config/initializers/webauthn.rb +23 -0
- data/lib/generators/maquina/passkeys/templates/config/locales/passkeys.en.yml +46 -0
- data/lib/generators/maquina/passkeys/templates/config/locales/passkeys.es.yml +46 -0
- data/lib/generators/maquina/passkeys/templates/migration_add_webauthn_id_to_users.rb.tt +6 -0
- data/lib/generators/maquina/passkeys/templates/migration_create_passkeys.rb.tt +20 -0
- data/lib/generators/maquina/passkeys/templates/test/integration/passkeys_test.rb +99 -0
- data/lib/generators/maquina/passkeys/templates/test/test_helpers/webauthn_test_helper.rb +38 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/clave_generator_test.rb +13 -2
- data/test/generators/maquina/passkeys_generator_test.rb +253 -0
- data/test/tmp/Gemfile +1 -14
- data/test/tmp/app/controllers/application_controller.rb +3 -0
- data/test/tmp/app/controllers/concerns/authentication.rb +63 -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 +58 -0
- data/test/tmp/app/models/session.rb +18 -0
- data/test/tmp/app/models/user.rb +37 -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 +10 -1
- data/test/tmp/db/migrate/20261014212613_create_accounts.rb +9 -0
- data/test/tmp/db/migrate/20261014212614_create_users.rb +17 -0
- data/test/tmp/db/migrate/20261014212615_create_sessions.rb +12 -0
- data/test/tmp/db/migrate/20261014212616_create_email_verifications.rb +17 -0
- data/test/tmp/test/test_helpers/session_test_helper.rb +19 -0
- metadata +48 -19
- data/test/tmp/.gitignore +0 -5
- data/test/tmp/.rubocop.yml +0 -11
- data/test/tmp/.standard.yml +0 -1
- data/test/tmp/Procfile.dev +0 -3
- data/test/tmp/README.md +0 -89
- data/test/tmp/app/controllers/home_controller.rb +0 -6
- data/test/tmp/app/javascript/application.js +0 -1
- data/test/tmp/app/views/home/index.html.erb +0 -204
- data/test/tmp/app/views/layouts/application.html.erb +0 -10
- data/test/tmp/config/application.rb +0 -15
- data/test/tmp/config/database.yml +0 -90
- data/test/tmp/config/database.yml.example +0 -90
- data/test/tmp/config/environments/development.rb +0 -8
- data/test/tmp/config/environments/production.rb +0 -6
- 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,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."
|
data/test/tmp/config/routes.rb
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
1
|
Rails.application.routes.draw do
|
|
2
|
-
|
|
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,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.
|
|
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
|
|
70
|
-
dashboard, Content Security Policy reports, Solid Errors, Mission
|
|
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/
|
|
277
|
-
- test/tmp/
|
|
278
|
-
- test/tmp/app/controllers/
|
|
279
|
-
- test/tmp/app/
|
|
280
|
-
- test/tmp/app/
|
|
281
|
-
- test/tmp/app/
|
|
282
|
-
- test/tmp/
|
|
283
|
-
- test/tmp/
|
|
284
|
-
- test/tmp/
|
|
285
|
-
- test/tmp/
|
|
286
|
-
- test/tmp/
|
|
287
|
-
- test/tmp/
|
|
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
data/test/tmp/.rubocop.yml
DELETED
|
@@ -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
|
data/test/tmp/.standard.yml
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ruby_version: 3.2
|