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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -0
- data/Rakefile +10 -0
- data/lib/generators/maquina/clave/USAGE +11 -0
- data/lib/generators/maquina/clave/clave_generator.rb +184 -0
- data/lib/generators/maquina/clave/templates/app/controllers/concerns/authentication.rb.tt +63 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registration/verification_resends_controller.rb.tt +38 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +51 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registrations_controller.rb.tt +63 -0
- data/lib/generators/maquina/clave/templates/app/controllers/session/verification_resends_controller.rb.tt +44 -0
- data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt +55 -0
- data/lib/generators/maquina/clave/templates/app/controllers/sessions_controller.rb.tt +56 -0
- data/lib/generators/maquina/clave/templates/app/helpers/authentication_helper.rb.tt +20 -0
- data/lib/generators/maquina/clave/templates/app/jobs/authentication_cleanup_job.rb.tt +13 -0
- data/lib/generators/maquina/clave/templates/app/mailers/verification_mailer.rb.tt +15 -0
- data/lib/generators/maquina/clave/templates/app/models/current.rb.tt +4 -0
- data/lib/generators/maquina/clave/templates/app/models/email_verification.rb.tt +40 -0
- data/lib/generators/maquina/clave/templates/app/models/session.rb.tt +18 -0
- data/lib/generators/maquina/clave/templates/app/models/user.rb.tt +38 -0
- data/lib/generators/maquina/clave/templates/app/views/registration/verifications/new.html.erb.tt +42 -0
- data/lib/generators/maquina/clave/templates/app/views/registrations/new.html.erb.tt +38 -0
- data/lib/generators/maquina/clave/templates/app/views/session/verifications/new.html.erb.tt +42 -0
- data/lib/generators/maquina/clave/templates/app/views/sessions/new.html.erb.tt +39 -0
- data/lib/generators/maquina/clave/templates/app/views/verification_mailer/verification_code.html.erb.tt +37 -0
- data/lib/generators/maquina/clave/templates/app/views/verification_mailer/verification_code.text.erb.tt +11 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.en.yml +101 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.es.yml +101 -0
- data/lib/generators/maquina/clave/templates/migration_create_email_verifications.rb.tt +17 -0
- data/lib/generators/maquina/clave/templates/migration_create_sessions.rb.tt +12 -0
- data/lib/generators/maquina/clave/templates/migration_create_users.rb.tt +16 -0
- data/lib/generators/maquina/clave/templates/test/test_helpers/session_test_helper.rb.tt +19 -0
- data/lib/generators/maquina/mission_control_jobs/USAGE +14 -0
- data/lib/generators/maquina/mission_control_jobs/mission_control_jobs_generator.rb +75 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/controllers/backstage_controller.rb.tt +4 -0
- data/lib/generators/maquina/mission_control_jobs/templates/config/initializers/mission_control.rb.tt +10 -0
- data/lib/generators/maquina/solid_errors/USAGE +15 -0
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +85 -0
- data/lib/generators/maquina/solid_errors/templates/app/controllers/backstage_controller.rb.tt +4 -0
- data/lib/generators/maquina/solid_errors/templates/config/initializers/solid_errors.rb.tt +10 -0
- data/lib/maquina_generators/version.rb +3 -0
- data/lib/maquina_generators.rb +1 -0
- data/test/generators/maquina/clave_generator_test.rb +187 -0
- data/test/generators/maquina/mission_control_jobs_generator_test.rb +97 -0
- data/test/generators/maquina/solid_errors_generator_test.rb +97 -0
- data/test/test_helper.rb +7 -0
- data/test/tmp/Gemfile +3 -0
- data/test/tmp/app/controllers/backstage_controller.rb +4 -0
- data/test/tmp/config/initializers/solid_errors.rb +10 -0
- data/test/tmp/config/routes.rb +3 -0
- 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,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
|