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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 217f011212f8bd0de1e98fe87fe41afeb48d2171eba4431cb82b219a69239a08
4
+ data.tar.gz: 927e664fc585adbe4a7d1e3f441a2fa49fb2514fe6ac239e742e240bf913ece3
5
+ SHA512:
6
+ metadata.gz: 01b0e678645dca5f7a0a080a9b70d463d0d8da194a3f2fe9ad09d77fa6c0bc673863f4ac525000db990d441508560bee5b2ce3858cb68b61d59432d5bbefcdc8
7
+ data.tar.gz: b6277238ae34b753a3056b6f559ffd5bd4324e8e605568d624a79b1dca6b4a70e54a3c22cce5f4405de1848e837a540f12d6b022d6d2e746d0025d5afa3daba6
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Mario Alberto Chavez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,158 @@
1
+ # Maquina Generators
2
+
3
+ A collection of Rails generators from the Maquina umbrella. Each generator produces standalone code with no runtime gem dependency -- the gem is only needed at generation time.
4
+
5
+ ## Available Generators
6
+
7
+ ### Clave -- Passwordless Email-Code Authentication
8
+
9
+ **Clave** (Spanish: "code/key") generates a complete passwordless authentication system using email verification codes.
10
+
11
+ #### What it generates
12
+
13
+ - **Models:** `User`, `Session`, `EmailVerification`, `Current`
14
+ - **Controllers:** Sign-in/sign-up flows with email code verification
15
+ - **Views:** Minimal, responsive forms styled with Tailwind CSS
16
+ - **Mailer:** Verification code emails (HTML + text)
17
+ - **Job:** Cleanup job for expired sessions and verifications
18
+ - **Locale files:** English and Spanish translations
19
+ - **Migrations:** 3 migrations (users, sessions, email_verifications)
20
+ - **Test helper:** `sign_in_as(user)` and `sign_out` for integration tests
21
+
22
+ #### Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem "maquina_generators", group: :development
28
+ ```
29
+
30
+ Run the generator:
31
+
32
+ ```bash
33
+ rails g maquina:clave
34
+ ```
35
+
36
+ Then:
37
+
38
+ ```bash
39
+ bundle install # Install bcrypt
40
+ rails db:migrate # Run migrations
41
+ ```
42
+
43
+ #### Options
44
+
45
+ ```bash
46
+ rails g maquina:clave # Full install
47
+ rails g maquina:clave --skip-registration # Sign-in only (no sign-up)
48
+ rails g maquina:clave --skip-views # Skip view templates
49
+ ```
50
+
51
+ #### Customization
52
+
53
+ All generated code lives in your app -- edit it directly:
54
+
55
+ - **Redirect after login:** Edit `app/controllers/concerns/authentication.rb` (`after_authentication_url`)
56
+ - **Session duration:** Edit `authentication.rb` (default: 30 days)
57
+ - **Code expiration:** Edit controllers (default: 15 minutes)
58
+ - **Cooldown between codes:** Edit `EmailVerification::COOLDOWN_MINUTES` (default: 15)
59
+ - **Colors/styling:** Edit view templates (default: indigo)
60
+ - **Email sender:** Edit `app/mailers/verification_mailer.rb`
61
+ - **Translations:** Edit `config/locales/clave.*.yml`
62
+
63
+ ### Solid Errors -- Error Tracking Dashboard
64
+
65
+ **Solid Errors** installs the [solid_errors](https://github.com/fractaledmind/solid_errors) gem with HTTP authentication and engine mounting.
66
+
67
+ #### What it generates
68
+
69
+ - **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
70
+ - **Initializer:** Credentials-first auth with ENV variable fallback, database connection config
71
+ - **Route:** Mounts `SolidErrors::Engine` under a configurable prefix
72
+
73
+ #### Usage
74
+
75
+ ```bash
76
+ rails g maquina:solid_errors --prefix /admin
77
+ ```
78
+
79
+ The generator automatically runs `bundle install` and `solid_errors:install`. After running, execute `bin/rails db:migrate`.
80
+
81
+ #### Options
82
+
83
+ ```bash
84
+ rails g maquina:solid_errors --prefix /admin # Default env vars
85
+ rails g maquina:solid_errors --prefix /backstage \
86
+ --user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
87
+ ```
88
+
89
+ #### Authentication
90
+
91
+ Credentials are resolved in order:
92
+
93
+ 1. `Rails.application.credentials.backstage.username` / `.password`
94
+ 2. `ENV["SOLID_ERRORS_USER"]` / `ENV["SOLID_ERRORS_PASSWORD"]` (configurable)
95
+
96
+ ---
97
+
98
+ ### Mission Control Jobs -- Job Queue Dashboard
99
+
100
+ **Mission Control Jobs** installs the [mission_control-jobs](https://github.com/rails/mission_control-jobs) gem with HTTP authentication and engine mounting.
101
+
102
+ #### What it generates
103
+
104
+ - **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
105
+ - **Initializer:** Sets base controller class, credentials-first auth with ENV variable fallback
106
+ - **Route:** Mounts `MissionControl::Jobs::Engine` under a configurable prefix
107
+
108
+ #### Usage
109
+
110
+ ```bash
111
+ rails g maquina:mission_control_jobs --prefix /admin
112
+ ```
113
+
114
+ The generator automatically runs `bundle install`.
115
+
116
+ #### Options
117
+
118
+ ```bash
119
+ rails g maquina:mission_control_jobs --prefix /admin # Default env vars
120
+ rails g maquina:mission_control_jobs --prefix /backstage \
121
+ --user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
122
+ ```
123
+
124
+ #### Authentication
125
+
126
+ Credentials are resolved in order:
127
+
128
+ 1. `Rails.application.credentials.backstage.username` / `.password`
129
+ 2. `ENV["MISSION_CONTROL_JOBS_USER"]` / `ENV["MISSION_CONTROL_JOBS_PASSWORD"]` (configurable)
130
+
131
+ ---
132
+
133
+ ## Adding New Generators
134
+
135
+ Create a new folder under `lib/generators/maquina/`:
136
+
137
+ ```
138
+ lib/generators/maquina/your_generator/
139
+ your_generator_generator.rb
140
+ USAGE
141
+ templates/
142
+ ...
143
+ ```
144
+
145
+ The generator class should be `Maquina::Generators::YourGeneratorGenerator` and it will be available as `rails g maquina:your_generator`.
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ bundle install
151
+ rake test
152
+ bundle exec standardrb # Lint
153
+ bundle exec standardrb --fix # Auto-fix
154
+ ```
155
+
156
+ ## License
157
+
158
+ MIT License. See [LICENSE.txt](LICENSE.txt).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task default: :test
@@ -0,0 +1,11 @@
1
+ Description:
2
+ Generates passwordless email-code authentication (Clave).
3
+
4
+ Creates models, controllers, views, mailer, job, locale files, and
5
+ migrations for a complete passwordless authentication system using
6
+ email verification codes.
7
+
8
+ Examples:
9
+ rails g maquina:clave # Full install
10
+ rails g maquina:clave --skip-registration # Sign-in only (no sign-up flow)
11
+ rails g maquina:clave --skip-views # No view templates (API-only or custom views)
@@ -0,0 +1,184 @@
1
+ require "rails/generators"
2
+ require "rails/generators/migration"
3
+
4
+ module Maquina
5
+ module Generators
6
+ class ClaveGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ source_root File.expand_path("templates", __dir__)
10
+
11
+ class_option :skip_views, type: :boolean, default: false,
12
+ desc: "Skip view templates"
13
+ class_option :skip_registration, type: :boolean, default: false,
14
+ desc: "Skip sign-up flow"
15
+
16
+ def self.next_migration_number(dirname)
17
+ sleep(1) if @prev_migration_nr == Time.now.utc.strftime("%Y%m%d%H%M%S")
18
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S")
19
+ end
20
+
21
+ # 1. Models
22
+ def create_models
23
+ template "app/models/current.rb.tt", "app/models/current.rb"
24
+ template "app/models/session.rb.tt", "app/models/session.rb"
25
+ template "app/models/email_verification.rb.tt", "app/models/email_verification.rb"
26
+ template "app/models/user.rb.tt", "app/models/user.rb"
27
+ end
28
+
29
+ # 2. Controllers
30
+ def create_controllers
31
+ template "app/controllers/concerns/authentication.rb.tt",
32
+ "app/controllers/concerns/authentication.rb"
33
+ template "app/controllers/sessions_controller.rb.tt",
34
+ "app/controllers/sessions_controller.rb"
35
+ template "app/controllers/session/verifications_controller.rb.tt",
36
+ "app/controllers/session/verifications_controller.rb"
37
+ template "app/controllers/session/verification_resends_controller.rb.tt",
38
+ "app/controllers/session/verification_resends_controller.rb"
39
+
40
+ unless options[:skip_registration]
41
+ template "app/controllers/registrations_controller.rb.tt",
42
+ "app/controllers/registrations_controller.rb"
43
+ template "app/controllers/registration/verifications_controller.rb.tt",
44
+ "app/controllers/registration/verifications_controller.rb"
45
+ template "app/controllers/registration/verification_resends_controller.rb.tt",
46
+ "app/controllers/registration/verification_resends_controller.rb"
47
+ end
48
+ end
49
+
50
+ # 3. Mailer
51
+ def create_mailer
52
+ template "app/mailers/verification_mailer.rb.tt",
53
+ "app/mailers/verification_mailer.rb"
54
+ end
55
+
56
+ # 4. Helper
57
+ def create_helper
58
+ template "app/helpers/authentication_helper.rb.tt",
59
+ "app/helpers/authentication_helper.rb"
60
+ end
61
+
62
+ # 5. Job
63
+ def create_job
64
+ template "app/jobs/authentication_cleanup_job.rb.tt",
65
+ "app/jobs/authentication_cleanup_job.rb"
66
+ end
67
+
68
+ # 6. Views
69
+ def create_views
70
+ return if options[:skip_views]
71
+
72
+ template "app/views/sessions/new.html.erb.tt",
73
+ "app/views/sessions/new.html.erb"
74
+ template "app/views/session/verifications/new.html.erb.tt",
75
+ "app/views/session/verifications/new.html.erb"
76
+
77
+ unless options[:skip_registration]
78
+ template "app/views/registrations/new.html.erb.tt",
79
+ "app/views/registrations/new.html.erb"
80
+ template "app/views/registration/verifications/new.html.erb.tt",
81
+ "app/views/registration/verifications/new.html.erb"
82
+ end
83
+
84
+ template "app/views/verification_mailer/verification_code.html.erb.tt",
85
+ "app/views/verification_mailer/verification_code.html.erb"
86
+ template "app/views/verification_mailer/verification_code.text.erb.tt",
87
+ "app/views/verification_mailer/verification_code.text.erb"
88
+ end
89
+
90
+ # 7. Locale files
91
+ def create_locale_files
92
+ copy_file "config/locales/clave.en.yml", "config/locales/clave.en.yml"
93
+ copy_file "config/locales/clave.es.yml", "config/locales/clave.es.yml"
94
+ end
95
+
96
+ # 9. Test helper
97
+ def create_test_helper
98
+ template "test/test_helpers/session_test_helper.rb.tt",
99
+ "test/test_helpers/session_test_helper.rb"
100
+ end
101
+
102
+ # 10. Include Authentication in ApplicationController
103
+ def configure_application_controller
104
+ sentinel = "class ApplicationController < ActionController::Base\n"
105
+ inject_into_file "app/controllers/application_controller.rb",
106
+ " include Authentication\n",
107
+ after: sentinel
108
+ end
109
+
110
+ # 11. Routes
111
+ def add_routes
112
+ route_content = <<~RUBY
113
+ # Authentication routes (generated by maquina:clave)
114
+ resource :session, only: [:new, :create, :destroy] do
115
+ resource :verification, only: [:new, :create], controller: "session/verifications"
116
+ resource :verification_resend, only: [:create], controller: "session/verification_resends"
117
+ end
118
+ RUBY
119
+
120
+ unless options[:skip_registration]
121
+ route_content += <<~RUBY
122
+
123
+ resource :registration, only: [:new, :create] do
124
+ resource :verification, only: [:new, :create], controller: "registration/verifications"
125
+ resource :verification_resend, only: [:create], controller: "registration/verification_resends"
126
+ end
127
+ RUBY
128
+ end
129
+
130
+ route route_content
131
+ end
132
+
133
+ # 12. Enable bcrypt
134
+ def enable_bcrypt
135
+ gemfile_path = File.join(destination_root, "Gemfile")
136
+ if File.exist?(gemfile_path)
137
+ content = File.read(gemfile_path)
138
+ if content.include?('# gem "bcrypt"')
139
+ gsub_file "Gemfile", '# gem "bcrypt"', 'gem "bcrypt"'
140
+ elsif !content.include?('gem "bcrypt"')
141
+ append_to_file "Gemfile", "\ngem \"bcrypt\"\n"
142
+ end
143
+ end
144
+ end
145
+
146
+ # 13. Migrations
147
+ def add_migrations
148
+ migration_template "migration_create_users.rb.tt",
149
+ "db/migrate/create_users.rb"
150
+ migration_template "migration_create_sessions.rb.tt",
151
+ "db/migrate/create_sessions.rb"
152
+ migration_template "migration_create_email_verifications.rb.tt",
153
+ "db/migrate/create_email_verifications.rb"
154
+ end
155
+
156
+ # 14. Post-install message
157
+ def show_post_install
158
+ say ""
159
+ say "Clave authentication has been installed!", :green
160
+ say ""
161
+ say "Next steps:", :yellow
162
+ say " 1. bundle install # Install bcrypt"
163
+ say " 2. rails db:migrate # Run migrations"
164
+ say " 3. rails server # Start the app"
165
+ say " 4. Visit /session/new # Sign-in page"
166
+ unless options[:skip_registration]
167
+ say " 5. Visit /registration/new # Sign-up page"
168
+ end
169
+ say ""
170
+ say "Optional:", :yellow
171
+ say " - Edit app/controllers/concerns/authentication.rb to customize redirect URLs"
172
+ say " - Edit config/locales/clave.*.yml to customize messages"
173
+ say " - Set up Action Mailer for email delivery (letter_opener for development)"
174
+ say ""
175
+ end
176
+
177
+ private
178
+
179
+ def migration_version
180
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,63 @@
1
+ module Authentication
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :require_authentication
6
+ helper_method :authenticated?, :session_expires_at
7
+ end
8
+
9
+ class_methods do
10
+ def allow_unauthenticated_access(**options)
11
+ skip_before_action :require_authentication, **options
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ def authenticated?
18
+ resume_session
19
+ end
20
+
21
+ def require_authentication
22
+ resume_session || request_authentication
23
+ end
24
+
25
+ def resume_session
26
+ Current.session ||= find_session_by_cookie
27
+ end
28
+
29
+ def find_session_by_cookie
30
+ return nil unless cookies.signed[:session_id]
31
+
32
+ Session.active.find_by(id: cookies.signed[:session_id])
33
+ end
34
+
35
+ def request_authentication
36
+ session[:return_to_after_authenticating] = request.url
37
+ redirect_to new_session_path
38
+ end
39
+
40
+ def after_authentication_url
41
+ session.delete(:return_to_after_authenticating) || root_url
42
+ end
43
+
44
+ def start_new_session_for(user)
45
+ user.sessions.create!(
46
+ user_agent: request.user_agent,
47
+ ip_address: request.remote_ip,
48
+ expires_at: 30.days.from_now
49
+ ).tap do |session|
50
+ Current.session = session
51
+ cookies.signed.permanent[:session_id] = {value: session.id, httponly: true, same_site: :lax}
52
+ end
53
+ end
54
+
55
+ def terminate_session
56
+ Current.session.destroy
57
+ cookies.delete(:session_id)
58
+ end
59
+
60
+ def session_expires_at
61
+ Current.session&.expires_at
62
+ end
63
+ end
@@ -0,0 +1,38 @@
1
+ class Registration::VerificationResendsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 5, within: 3.minutes, only: :create, with: -> { redirect_to new_registration_verification_path, alert: t("flash.general.rate_limited") }
4
+
5
+ # POST /registration/verification_resend
6
+ def create
7
+ old_verification = EmailVerification.find_by(id: session[:pending_verification_id])
8
+
9
+ unless old_verification
10
+ redirect_to new_registration_path
11
+ return
12
+ end
13
+
14
+ email = old_verification.email
15
+
16
+ if EmailVerification.for_email(email).recent.exists?
17
+ recent = EmailVerification.for_email(email).recent.first
18
+ redirect_to new_registration_verification_path,
19
+ alert: t("flash.registrations.cooldown", minutes: recent.minutes_until_resend)
20
+ return
21
+ end
22
+
23
+ verification = EmailVerification.create!(
24
+ email: email,
25
+ code: SecureRandom.hex(3).upcase,
26
+ verification_type: "signup",
27
+ locale: old_verification.locale || I18n.locale.to_s,
28
+ expires_at: 15.minutes.from_now
29
+ )
30
+
31
+ Rails.logger.info "[AUTH] Verification code resent for registration #{email} from #{request.remote_ip}"
32
+ VerificationMailer.verification_code(verification).deliver_later
33
+ session[:pending_verification_id] = verification.id
34
+
35
+ redirect_to new_registration_verification_path,
36
+ notice: t("flash.registrations.code_resent")
37
+ end
38
+ end
@@ -0,0 +1,51 @@
1
+ class Registration::VerificationsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_registration_verification_path, alert: t("flash.general.rate_limited") }
4
+
5
+ # GET /registration/verification/new
6
+ def new
7
+ @verification = EmailVerification.find_by(id: session[:pending_verification_id])
8
+ @email = @verification&.email || session[:pending_signup_email]
9
+ redirect_to new_registration_path unless @email
10
+ end
11
+
12
+ # POST /registration/verification
13
+ def create
14
+ @verification = EmailVerification.find_by(id: session[:pending_verification_id])
15
+ @email = @verification&.email || session[:pending_signup_email]
16
+
17
+ unless @verification
18
+ flash.now[:alert] = t("flash.registrations.invalid_code")
19
+ render :new, status: :unprocessable_entity
20
+ return
21
+ end
22
+
23
+ if @verification.expired?
24
+ flash.now[:alert] = t("flash.registrations.code_expired")
25
+ render :new, status: :unprocessable_entity
26
+ return
27
+ end
28
+
29
+ if @verification.code != params[:code]&.upcase&.strip
30
+ @verification.increment!(:attempts)
31
+ Rails.logger.warn "[AUTH] Invalid verification code attempt for #{@email} from #{request.remote_ip} (attempt #{@verification.attempts})"
32
+ flash.now[:alert] = t("flash.registrations.invalid_code")
33
+ render :new, status: :unprocessable_entity
34
+ return
35
+ end
36
+
37
+ user = User.create!(
38
+ email_address: @verification.email,
39
+ password: User.generate_secure_password,
40
+ locale: @verification.locale || I18n.default_locale.to_s
41
+ )
42
+
43
+ @verification.mark_verified!
44
+ session.delete(:pending_verification_id)
45
+ session.delete(:pending_signup_email)
46
+
47
+ Rails.logger.info "[AUTH] Successful registration for #{user.email_address} from #{request.remote_ip}"
48
+ start_new_session_for(user)
49
+ redirect_to root_path, notice: t("flash.registrations.create.success")
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ class RegistrationsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_registration_path, alert: t("flash.general.rate_limited") }
4
+
5
+ def new
6
+ end
7
+
8
+ def create
9
+ email = registration_params[:email_address]&.downcase&.strip
10
+
11
+ if email&.include?("+")
12
+ Rails.logger.warn "[AUTH] Registration attempted with + in email #{email} from #{request.remote_ip}"
13
+ flash.now[:alert] = t("flash.registrations.email_plus_not_allowed")
14
+ render :new, status: :unprocessable_entity
15
+ return
16
+ end
17
+
18
+ existing_user = User.find_by(email_address: email)
19
+
20
+ if existing_user
21
+ if existing_user.blocked?
22
+ # Fail silently - show same UI but don't send email
23
+ Rails.logger.info "[AUTH] Registration attempted for blocked user #{email} from #{request.remote_ip}"
24
+ session[:pending_signup_email] = email
25
+ redirect_to new_registration_verification_path
26
+ else
27
+ Rails.logger.info "[AUTH] Registration attempted for existing email #{email} from #{request.remote_ip}"
28
+ flash.now[:alert] = t("flash.registrations.email_taken")
29
+ render :new, status: :unprocessable_entity
30
+ end
31
+ return
32
+ end
33
+
34
+ recent_verification = EmailVerification.for_email(email).recent.first
35
+ if recent_verification
36
+ session[:pending_verification_id] = recent_verification.id
37
+ session[:pending_signup_email] = email
38
+ redirect_to new_registration_verification_path,
39
+ notice: t("flash.registrations.code_already_sent", minutes: recent_verification.minutes_until_resend)
40
+ return
41
+ end
42
+
43
+ verification = EmailVerification.create!(
44
+ email: email,
45
+ code: SecureRandom.hex(3).upcase,
46
+ verification_type: "signup",
47
+ locale: I18n.locale.to_s,
48
+ expires_at: 15.minutes.from_now
49
+ )
50
+
51
+ Rails.logger.info "[AUTH] Verification code sent for new registration #{email} from #{request.remote_ip}"
52
+ VerificationMailer.verification_code(verification).deliver_later
53
+ session[:pending_verification_id] = verification.id
54
+
55
+ redirect_to new_registration_verification_path
56
+ end
57
+
58
+ private
59
+
60
+ def registration_params
61
+ params.permit(:email_address)
62
+ end
63
+ end
@@ -0,0 +1,44 @@
1
+ class Session::VerificationResendsController < ApplicationController
2
+ allow_unauthenticated_access
3
+ rate_limit to: 5, within: 3.minutes, only: :create, with: -> { redirect_to new_session_verification_path, alert: t("flash.general.rate_limited") }
4
+
5
+ # POST /session/verification_resend
6
+ def create
7
+ old_verification = EmailVerification.find_by(id: session[:pending_verification_id])
8
+ email = old_verification&.email || session[:pending_login_email]
9
+
10
+ unless email
11
+ redirect_to new_session_path
12
+ return
13
+ end
14
+
15
+ if EmailVerification.for_email(email).recent.exists?
16
+ recent = EmailVerification.for_email(email).recent.first
17
+ redirect_to new_session_verification_path,
18
+ alert: t("flash.sessions.cooldown", minutes: recent.minutes_until_resend)
19
+ return
20
+ end
21
+
22
+ user = User.find_by(email_address: email)
23
+
24
+ if user && !user.blocked?
25
+ verification = EmailVerification.create!(
26
+ email: email,
27
+ code: SecureRandom.hex(3).upcase,
28
+ verification_type: "login",
29
+ locale: user.locale || I18n.default_locale.to_s,
30
+ expires_at: 15.minutes.from_now
31
+ )
32
+
33
+ Rails.logger.info "[AUTH] Verification code resent for login #{email} from #{request.remote_ip}"
34
+ VerificationMailer.verification_code(verification).deliver_later
35
+ session[:pending_verification_id] = verification.id
36
+ else
37
+ Rails.logger.info "[AUTH] Verification resend attempted for non-existent/blocked email #{email} from #{request.remote_ip}"
38
+ end
39
+
40
+ # Always show success (don't reveal if user exists)
41
+ redirect_to new_session_verification_path,
42
+ notice: t("flash.sessions.code_resent")
43
+ end
44
+ end