maquina-generators 0.1.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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +158 -0
  4. data/Rakefile +10 -0
  5. data/lib/generators/maquina/clave/USAGE +11 -0
  6. data/lib/generators/maquina/clave/clave_generator.rb +184 -0
  7. data/lib/generators/maquina/clave/templates/app/controllers/concerns/authentication.rb.tt +63 -0
  8. data/lib/generators/maquina/clave/templates/app/controllers/registration/verification_resends_controller.rb.tt +38 -0
  9. data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +51 -0
  10. data/lib/generators/maquina/clave/templates/app/controllers/registrations_controller.rb.tt +63 -0
  11. data/lib/generators/maquina/clave/templates/app/controllers/session/verification_resends_controller.rb.tt +44 -0
  12. data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt +55 -0
  13. data/lib/generators/maquina/clave/templates/app/controllers/sessions_controller.rb.tt +56 -0
  14. data/lib/generators/maquina/clave/templates/app/helpers/authentication_helper.rb.tt +20 -0
  15. data/lib/generators/maquina/clave/templates/app/jobs/authentication_cleanup_job.rb.tt +13 -0
  16. data/lib/generators/maquina/clave/templates/app/mailers/verification_mailer.rb.tt +15 -0
  17. data/lib/generators/maquina/clave/templates/app/models/current.rb.tt +4 -0
  18. data/lib/generators/maquina/clave/templates/app/models/email_verification.rb.tt +40 -0
  19. data/lib/generators/maquina/clave/templates/app/models/session.rb.tt +18 -0
  20. data/lib/generators/maquina/clave/templates/app/models/user.rb.tt +38 -0
  21. data/lib/generators/maquina/clave/templates/app/views/registration/verifications/new.html.erb.tt +42 -0
  22. data/lib/generators/maquina/clave/templates/app/views/registrations/new.html.erb.tt +38 -0
  23. data/lib/generators/maquina/clave/templates/app/views/session/verifications/new.html.erb.tt +42 -0
  24. data/lib/generators/maquina/clave/templates/app/views/sessions/new.html.erb.tt +39 -0
  25. data/lib/generators/maquina/clave/templates/app/views/verification_mailer/verification_code.html.erb.tt +37 -0
  26. data/lib/generators/maquina/clave/templates/app/views/verification_mailer/verification_code.text.erb.tt +11 -0
  27. data/lib/generators/maquina/clave/templates/config/locales/clave.en.yml +101 -0
  28. data/lib/generators/maquina/clave/templates/config/locales/clave.es.yml +101 -0
  29. data/lib/generators/maquina/clave/templates/migration_create_email_verifications.rb.tt +17 -0
  30. data/lib/generators/maquina/clave/templates/migration_create_sessions.rb.tt +12 -0
  31. data/lib/generators/maquina/clave/templates/migration_create_users.rb.tt +16 -0
  32. data/lib/generators/maquina/clave/templates/test/test_helpers/session_test_helper.rb.tt +19 -0
  33. data/lib/generators/maquina/mission_control_jobs/USAGE +14 -0
  34. data/lib/generators/maquina/mission_control_jobs/mission_control_jobs_generator.rb +75 -0
  35. data/lib/generators/maquina/mission_control_jobs/templates/app/controllers/backstage_controller.rb.tt +4 -0
  36. data/lib/generators/maquina/mission_control_jobs/templates/config/initializers/mission_control.rb.tt +10 -0
  37. data/lib/generators/maquina/solid_errors/USAGE +15 -0
  38. data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +85 -0
  39. data/lib/generators/maquina/solid_errors/templates/app/controllers/backstage_controller.rb.tt +4 -0
  40. data/lib/generators/maquina/solid_errors/templates/config/initializers/solid_errors.rb.tt +10 -0
  41. data/lib/maquina_generators/version.rb +3 -0
  42. data/lib/maquina_generators.rb +1 -0
  43. data/test/generators/maquina/clave_generator_test.rb +187 -0
  44. data/test/generators/maquina/mission_control_jobs_generator_test.rb +97 -0
  45. data/test/generators/maquina/solid_errors_generator_test.rb +97 -0
  46. data/test/test_helper.rb +7 -0
  47. data/test/tmp/Gemfile +3 -0
  48. data/test/tmp/app/controllers/backstage_controller.rb +4 -0
  49. data/test/tmp/config/initializers/solid_errors.rb +10 -0
  50. data/test/tmp/config/routes.rb +3 -0
  51. metadata +134 -0
@@ -0,0 +1,55 @@
1
+ class Session::VerificationsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_verification_path, alert: t("flash.general.rate_limited") }
4
+
5
+ # GET /session/verification/new
6
+ def new
7
+ @verification = EmailVerification.find_by(id: session[:pending_verification_id])
8
+ @email = @verification&.email || session[:pending_login_email]
9
+ redirect_to new_session_path unless @email
10
+ end
11
+
12
+ # POST /session/verification
13
+ def create
14
+ @verification = EmailVerification.find_by(id: session[:pending_verification_id])
15
+ @email = @verification&.email || session[:pending_login_email]
16
+
17
+ # Generic error if no verification (user doesn't exist or blocked)
18
+ unless @verification
19
+ flash.now[:alert] = t("flash.sessions.invalid_code")
20
+ render :new, status: :unprocessable_entity
21
+ return
22
+ end
23
+
24
+ if @verification.expired?
25
+ flash.now[:alert] = t("flash.sessions.code_expired")
26
+ render :new, status: :unprocessable_entity
27
+ return
28
+ end
29
+
30
+ if @verification.code != params[:code]&.upcase&.strip
31
+ @verification.increment!(:attempts)
32
+ Rails.logger.warn "[AUTH] Invalid verification code attempt for #{@email} from #{request.remote_ip} (attempt #{@verification.attempts})"
33
+ flash.now[:alert] = t("flash.sessions.invalid_code")
34
+ render :new, status: :unprocessable_entity
35
+ return
36
+ end
37
+
38
+ user = User.find_by(email_address: @verification.email)
39
+
40
+ # Double-check user exists and not blocked
41
+ if user.nil? || user.blocked?
42
+ flash.now[:alert] = t("flash.sessions.invalid_code")
43
+ render :new, status: :unprocessable_entity
44
+ return
45
+ end
46
+
47
+ @verification.mark_verified!
48
+ session.delete(:pending_verification_id)
49
+ session.delete(:pending_login_email)
50
+
51
+ Rails.logger.info "[AUTH] Successful login for #{user.email_address} from #{request.remote_ip}"
52
+ start_new_session_for(user)
53
+ redirect_to after_authentication_url, notice: t("flash.sessions.create.success")
54
+ end
55
+ end
@@ -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,4 @@
1
+ class Current < ActiveSupport::CurrentAttributes
2
+ attribute :session
3
+ delegate :user, to: :session, allow_nil: true
4
+ end
@@ -0,0 +1,40 @@
1
+ class EmailVerification < ApplicationRecord
2
+ VERIFICATION_TYPES = %w[signup login].freeze
3
+ COOLDOWN_MINUTES = 15
4
+
5
+ validates :email, presence: true, format: {with: URI::MailTo::EMAIL_REGEXP}
6
+ validates :code, presence: true
7
+ validates :verification_type, presence: true, inclusion: {in: VERIFICATION_TYPES}
8
+ validates :expires_at, presence: true
9
+
10
+ normalizes :email, with: ->(email) { email.strip.downcase }
11
+ normalizes :code, with: ->(code) { code.strip.upcase }
12
+
13
+ scope :pending, -> { where(verified_at: nil) }
14
+ scope :expired, -> { where("expires_at < ?", Time.current) }
15
+ scope :active, -> { pending.where("expires_at >= ?", Time.current) }
16
+ scope :for_email, ->(email) { where("LOWER(email) = ?", email.to_s.downcase) }
17
+ scope :recent, -> { pending.where("created_at > ?", COOLDOWN_MINUTES.minutes.ago) }
18
+
19
+ def expired?
20
+ expires_at < Time.current
21
+ end
22
+
23
+ def verified?
24
+ verified_at.present?
25
+ end
26
+
27
+ def can_resend?
28
+ created_at <= COOLDOWN_MINUTES.minutes.ago
29
+ end
30
+
31
+ def minutes_until_resend
32
+ return 0 if can_resend?
33
+
34
+ ((created_at + COOLDOWN_MINUTES.minutes - Time.current) / 60).ceil
35
+ end
36
+
37
+ def mark_verified!
38
+ update!(verified_at: Time.current)
39
+ end
40
+ 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,38 @@
1
+ class User < ApplicationRecord
2
+ has_secure_password
3
+ has_many :sessions, dependent: :destroy
4
+
5
+ normalizes :email_address, with: ->(e) { e.strip.downcase }
6
+
7
+ validates :email_address, uniqueness: true
8
+ validate :email_cannot_contain_plus
9
+
10
+ scope :active, -> { where(blocked_at: nil) }
11
+ scope :blocked, -> { where.not(blocked_at: nil) }
12
+
13
+ def self.generate_secure_password
14
+ SecureRandom.hex(10)
15
+ end
16
+
17
+ def blocked?
18
+ blocked_at.present?
19
+ end
20
+
21
+ def block!(reason: nil)
22
+ update!(blocked_at: Time.current, blocked_reason: reason)
23
+ end
24
+
25
+ def unblock!
26
+ update!(blocked_at: nil, blocked_reason: nil)
27
+ end
28
+
29
+ private
30
+
31
+ def email_cannot_contain_plus
32
+ return if email_address.blank?
33
+
34
+ if email_address.include?("+")
35
+ errors.add(:email_address, :plus_not_allowed)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
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-white rounded-xl shadow-sm border border-slate-200 p-8">
4
+ <h1 class="text-2xl font-bold text-slate-800 text-center mb-4">
5
+ <%%= t(".title") %>
6
+ </h1>
7
+
8
+ <p class="text-center text-slate-600 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-slate-700 mb-2 text-center" %>
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
+ class: "w-full px-4 py-4 text-center text-2xl font-mono tracking-widest uppercase border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors" %>
23
+ </div>
24
+
25
+ <p class="text-sm text-slate-500 text-center">
26
+ <%%= t(".check_spam") %>
27
+ </p>
28
+
29
+ <div>
30
+ <%%= form.submit t(".submit"), class: "w-full px-4 py-3 text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors cursor-pointer" %>
31
+ </div>
32
+ <%% end %>
33
+
34
+ <div class="mt-6 text-center">
35
+ <p class="text-sm text-slate-500 mb-2">
36
+ <%%= t(".didnt_receive") %>
37
+ </p>
38
+ <%%= button_to t(".resend"), registration_verification_resend_path, method: :post, class: "text-sm font-medium text-indigo-600 hover:text-indigo-500" %>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </div>
@@ -0,0 +1,38 @@
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-white rounded-xl shadow-sm border border-slate-200 p-8">
4
+ <h1 class="text-2xl font-bold text-slate-800 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-slate-700 mb-2" %>
11
+ <%%= form.email_field :email_address,
12
+ required: true,
13
+ autofocus: true,
14
+ autocomplete: "email",
15
+ placeholder: t(".email_placeholder"),
16
+ class: "w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors" %>
17
+ </div>
18
+
19
+ <p class="text-sm text-slate-600">
20
+ <%%= t(".email_instructions") %>
21
+ </p>
22
+
23
+ <p class="text-sm text-slate-500">
24
+ <%%= t(".spam_note") %>
25
+ </p>
26
+
27
+ <div>
28
+ <%%= form.submit t(".submit"), class: "w-full px-4 py-3 text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors cursor-pointer" %>
29
+ </div>
30
+ <%% end %>
31
+
32
+ <p class="mt-6 text-center text-sm text-slate-500">
33
+ <%%= t(".have_account") %>
34
+ <%%= link_to t(".sign_in"), new_session_path, class: "font-medium text-indigo-600 hover:text-indigo-500" %>
35
+ </p>
36
+ </div>
37
+ </div>
38
+ </div>
@@ -0,0 +1,42 @@
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-white rounded-xl shadow-sm border border-slate-200 p-8">
4
+ <h1 class="text-2xl font-bold text-slate-800 text-center mb-4">
5
+ <%%= t(".title") %>
6
+ </h1>
7
+
8
+ <p class="text-center text-slate-600 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-slate-700 mb-2 text-center" %>
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
+ class: "w-full px-4 py-4 text-center text-2xl font-mono tracking-widest uppercase border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors" %>
23
+ </div>
24
+
25
+ <p class="text-sm text-slate-500 text-center">
26
+ <%%= t(".check_spam") %>
27
+ </p>
28
+
29
+ <div>
30
+ <%%= form.submit t(".submit"), class: "w-full px-4 py-3 text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors cursor-pointer" %>
31
+ </div>
32
+ <%% end %>
33
+
34
+ <div class="mt-6 text-center">
35
+ <p class="text-sm text-slate-500 mb-2">
36
+ <%%= t(".didnt_receive") %>
37
+ </p>
38
+ <%%= button_to t(".resend"), session_verification_resend_path, method: :post, class: "text-sm font-medium text-indigo-600 hover:text-indigo-500" %>
39
+ </div>
40
+ </div>
41
+ </div>
42
+ </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-white rounded-xl shadow-sm border border-slate-200 p-8">
4
+ <h1 class="text-2xl font-bold text-slate-800 text-center mb-8">
5
+ <%%= t(".title") %>
6
+ </h1>
7
+
8
+ <%%= form_with url: session_path, method: :post, class: "space-y-6" do |form| %>
9
+ <div>
10
+ <%%= form.label :email_address, t(".email"), class: "block text-sm font-medium text-slate-700 mb-2" %>
11
+ <%%= form.email_field :email_address,
12
+ required: true,
13
+ autofocus: true,
14
+ autocomplete: "email",
15
+ placeholder: t(".email_placeholder"),
16
+ value: params[:email_address],
17
+ class: "w-full px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-colors" %>
18
+ </div>
19
+
20
+ <p class="text-sm text-slate-600">
21
+ <%%= t(".email_instructions") %>
22
+ </p>
23
+
24
+ <p class="text-sm text-slate-500">
25
+ <%%= t(".spam_note") %>
26
+ </p>
27
+
28
+ <div>
29
+ <%%= form.submit t(".submit"), class: "w-full px-4 py-3 text-base font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-lg transition-colors cursor-pointer" %>
30
+ </div>
31
+ <%% end %>
32
+
33
+ <p class="mt-6 text-center text-sm text-slate-500">
34
+ <%%= t(".no_account") %>
35
+ <%%= link_to t(".sign_up"), new_registration_path, class: "font-medium text-indigo-600 hover:text-indigo-500" %>
36
+ </p>
37
+ </div>
38
+ </div>
39
+ </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: #4F46E5;"><%%= 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: #1f2937; 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: #4b5563; margin-bottom: 32px;">
15
+ <%%= t("verification_mailer.verification_code.instructions") %>
16
+ </p>
17
+
18
+ <!-- Code Display -->
19
+ <div style="background-color: #f3f4f6; 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: #1f2937;">
21
+ <%%= @verification.code %>
22
+ </span>
23
+ </div>
24
+
25
+ <!-- Expiry Note -->
26
+ <p style="text-align: center; font-size: 14px; color: #6b7280; margin-bottom: 32px;">
27
+ <%%= t("verification_mailer.verification_code.expires_in") %>
28
+ </p>
29
+
30
+ <!-- Ignore Note -->
31
+ <div style="border-top: 1px solid #e5e7eb; padding-top: 24px;">
32
+ <p style="text-align: center; font-size: 13px; color: #9ca3af;">
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,101 @@
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_resent: "We sent you a new code."
12
+ cooldown: "You must wait %{minutes} minutes before requesting another code."
13
+ code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
14
+ registrations:
15
+ create:
16
+ success: "Your account has been created. Welcome!"
17
+ code_sent: "We sent a verification code to your email."
18
+ email_plus_not_allowed: "Email addresses with '+' are not allowed. Use your main email without aliases."
19
+ email_taken: "An account with this email already exists. Would you like to sign in?"
20
+ invalid_code: "The code is incorrect. Please try again."
21
+ code_expired: "The code has expired. Request a new one."
22
+ code_resent: "We sent you a new code."
23
+ cooldown: "You must wait %{minutes} minutes before requesting another code."
24
+ code_already_sent: "We already sent you a code. Check your email (including spam). You can request a new one in %{minutes} minutes."
25
+ general:
26
+ unauthorized: "You must sign in to access this page."
27
+ forbidden: "You are not authorized to perform this action."
28
+ not_found: "The resource you're looking for doesn't exist."
29
+ rate_limited: "Too many attempts. Please wait a few minutes and try again."
30
+
31
+ sessions:
32
+ new:
33
+ title: "Sign in"
34
+ email: "Email address"
35
+ email_placeholder: "you@example.com"
36
+ email_instructions: "Enter your email and we'll send you a 6-digit code."
37
+ spam_note: "Check your spam folder if you don't receive the email."
38
+ submit: "Send code"
39
+ no_account: "Don't have an account?"
40
+ sign_up: "Sign up"
41
+
42
+ session:
43
+ verifications:
44
+ new:
45
+ title: "Verify your identity"
46
+ sent_to: "If you have an account, we sent a code to %{email}"
47
+ enter_code: "Enter the code"
48
+ submit: "Sign in"
49
+ check_spam: "Check your spam folder."
50
+ didnt_receive: "Didn't receive the code?"
51
+ resend: "Resend code"
52
+
53
+ registrations:
54
+ new:
55
+ title: "Create account"
56
+ email: "Email address"
57
+ email_placeholder: "you@example.com"
58
+ email_instructions: "Enter your email address. We'll send you a 6-digit code to verify your account."
59
+ spam_note: "Check your spam folder if you don't receive the email."
60
+ submit: "Send code"
61
+ have_account: "Already have an account?"
62
+ sign_in: "Sign in"
63
+
64
+ registration:
65
+ verifications:
66
+ new:
67
+ title: "Verify your email"
68
+ sent_to: "We sent a 6-digit code to %{email}"
69
+ enter_code: "Enter the code"
70
+ submit: "Verify"
71
+ check_spam: "Check your spam folder if you don't receive it."
72
+ didnt_receive: "Didn't receive the code?"
73
+ resend: "Resend code"
74
+
75
+ verification_mailer:
76
+ verification_code:
77
+ subject: "Your verification code"
78
+ title: "Verification code"
79
+ instructions: "Enter this code to continue:"
80
+ expires_in: "This code expires in 15 minutes."
81
+ ignore_if_not_you: "If you didn't request this code, you can ignore this email."
82
+
83
+ activerecord:
84
+ models:
85
+ user:
86
+ one: "User"
87
+ other: "Users"
88
+ attributes:
89
+ user:
90
+ email_address: "Email address"
91
+ name: "Name"
92
+ password: "Password"
93
+ locale: "Language"
94
+ blocked_at: "Blocked at"
95
+ blocked_reason: "Blocked reason"
96
+ errors:
97
+ models:
98
+ user:
99
+ attributes:
100
+ email_address:
101
+ plus_not_allowed: "Email addresses with '+' are not allowed. Please use your main email without aliases."