maquina-generators 0.4.1 → 0.6.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 +96 -17
- data/lib/generators/maquina/app/app_generator.rb +75 -19
- data/lib/generators/maquina/app/templates/.rubocop.yml +6 -0
- data/lib/generators/maquina/app/templates/README.md.erb +6 -4
- data/lib/generators/maquina/app/templates/app/views/home/index.html.erb +18 -5
- data/lib/generators/maquina/app/templates/config/database.yml.tt +12 -0
- data/lib/generators/maquina/mission_control_jobs/mission_control_jobs_generator.rb +32 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/layouts/_admin_navigation.html.erb.tt +18 -6
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/layouts/admin.html.erb +21 -0
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/_general_information.html.erb +1 -1
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/jobs/failed/_job.html.erb +2 -2
- data/lib/generators/maquina/mission_control_jobs/templates/app/views/mission_control/jobs/shared/_job.html.erb +1 -1
- data/lib/generators/maquina/rack_attack/USAGE +8 -6
- data/lib/generators/maquina/rack_attack/rack_attack_generator.rb +6 -2
- data/lib/generators/maquina/rack_attack/templates/config/initializers/rack_attack.rb.tt +139 -57
- data/lib/generators/maquina/security/USAGE +15 -0
- data/lib/generators/maquina/security/security_generator.rb +278 -0
- data/lib/generators/maquina/security/templates/app/controllers/backstage/security_controller.rb.tt +45 -0
- data/lib/generators/maquina/security/templates/app/controllers/backstage_controller.rb.tt +7 -0
- data/lib/generators/maquina/security/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/security/templates/app/models/security/abuse_event.rb +36 -0
- data/lib/generators/maquina/security/templates/app/models/security/abuse_report.rb +112 -0
- data/lib/generators/maquina/security/templates/app/models/security/record.rb +6 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_addresses.html.erb +37 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_blocked.html.erb +53 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_by_day.html.erb +33 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_hosts.html.erb +31 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_paths.html.erb +33 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_recent.html.erb +37 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/_utc_time.html.erb +2 -0
- data/lib/generators/maquina/security/templates/app/views/backstage/security/show.html.erb +47 -0
- data/lib/generators/maquina/security/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/security/templates/app/views/layouts/_admin_navigation.html.erb.tt +40 -0
- data/lib/generators/maquina/security/templates/app/views/layouts/admin.html.erb +21 -0
- data/lib/generators/maquina/security/templates/app/views/layouts/backstage/security.html.erb +23 -0
- data/lib/generators/maquina/security/templates/config/initializers/rack_attack_events.rb +34 -0
- data/lib/generators/maquina/security/templates/db/security_schema.rb.tt +27 -0
- data/lib/generators/maquina/solid_errors/USAGE +24 -0
- data/lib/generators/maquina/solid_errors/solid_errors_generator.rb +171 -14
- data/lib/generators/maquina/solid_errors/templates/app/agents/error_redactor.rb +34 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/errors_query.rb +84 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/failure_triage.rb +21 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/get_exception_tool.rb +26 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/list_failures_tool.rb +20 -0
- data/lib/generators/maquina/solid_errors/templates/app/agents/top_exceptions_tool.rb +18 -0
- data/lib/generators/maquina/solid_errors/templates/app/controllers/backstage_dashboard_controller.rb +47 -0
- data/lib/generators/maquina/solid_errors/templates/app/controllers/failures_mcp_controller.rb.tt +65 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/backstage_dashboard/index.html.erb.tt +93 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/layouts/_admin_navigation.html.erb.tt +18 -6
- data/lib/generators/maquina/solid_errors/templates/app/views/layouts/admin.html.erb +21 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/error_mailer/error_occurred.html.erb +65 -0
- data/lib/generators/maquina/solid_errors/templates/app/views/solid_errors/error_mailer/error_occurred.text.erb +30 -0
- data/lib/generators/maquina/solid_errors/templates/bin/failures +37 -0
- data/lib/generators/maquina/solid_errors/templates/bin/failures-mcp +21 -0
- data/lib/generators/maquina/solid_errors/templates/config/initializers/solid_errors.rb.tt +12 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/app_generator_test.rb +142 -5
- data/test/generators/maquina/failures_mcp_tools_test.rb +37 -0
- data/test/generators/maquina/mission_control_jobs_generator_test.rb +28 -4
- data/test/generators/maquina/rack_attack_generator_test.rb +41 -19
- data/test/generators/maquina/security_generator_test.rb +380 -0
- data/test/generators/maquina/solid_errors_generator_test.rb +199 -4
- data/test/tmp/Gemfile +1 -1
- data/test/tmp/app/controllers/application_controller.rb +2 -0
- data/test/tmp/app/controllers/registrations_controller.rb +37 -0
- data/test/tmp/app/models/account.rb +5 -0
- data/test/tmp/app/models/current.rb +5 -0
- data/test/tmp/app/models/user.rb +12 -0
- data/test/tmp/app/views/registrations/new.html.erb +72 -0
- data/test/tmp/app/views/sessions/new.html.erb +46 -0
- data/test/tmp/config/locales/registration.en.yml +44 -0
- data/test/tmp/config/locales/registration.es.yml +44 -0
- data/test/tmp/config/routes.rb +4 -0
- data/test/tmp/db/migrate/20260927024163_create_accounts.rb +9 -0
- data/test/tmp/db/migrate/20260927024164_add_account_fields_to_users.rb +7 -0
- metadata +72 -7
- data/test/tmp/Procfile.dev +0 -3
- data/test/tmp/config/application.rb +0 -9
- data/test/tmp/config/solid_queue.yml +0 -21
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8023146b756cacff577544a89e379d751c5f4f1e1df5fd1cb61932a514049bd
|
|
4
|
+
data.tar.gz: 1bff652af72d0e15d9dbd66e90e758b8f900200d5ae5dbd5c783157cfaa49f3a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 882a19b2e1d69f38e9acc0e73a312bb5a9750155b7521c6a2dc9597ddb93c1934c83536d5525a74916b7750a49f3bfd94543bd09dcd01a43a2aa2d8b0525d794
|
|
7
|
+
data.tar.gz: b9a6a548f3b6dbf7f6871e68588066f97bad710e2fc5a978dc9dcb32636b2448c60c86928dbc874b87cc4b3012030f94becc893049635418fb0df6e29e19b474
|
data/README.md
CHANGED
|
@@ -10,13 +10,13 @@ A collection of Rails generators from the Maquina umbrella. Each generator produ
|
|
|
10
10
|
|
|
11
11
|
#### What it generates
|
|
12
12
|
|
|
13
|
-
- **Models:** `User`, `Session`, `EmailVerification`, `Current`
|
|
13
|
+
- **Models:** `Account`, `User`, `Session`, `EmailVerification`, `Current`
|
|
14
14
|
- **Controllers:** Sign-in/sign-up flows with email code verification
|
|
15
15
|
- **Views:** Minimal, responsive forms styled with Tailwind CSS
|
|
16
16
|
- **Mailer:** Verification code emails (HTML + text)
|
|
17
17
|
- **Job:** Cleanup job for expired sessions and verifications
|
|
18
18
|
- **Locale files:** English and Spanish translations
|
|
19
|
-
- **Migrations:**
|
|
19
|
+
- **Migrations:** 4 migrations (accounts, users, sessions, email_verifications)
|
|
20
20
|
- **Test helper:** `sign_in_as(user)` and `sign_out` for integration tests
|
|
21
21
|
|
|
22
22
|
#### Installation
|
|
@@ -24,7 +24,7 @@ A collection of Rails generators from the Maquina umbrella. Each generator produ
|
|
|
24
24
|
Add to your Gemfile:
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
|
-
gem "
|
|
27
|
+
gem "maquina-generators", group: :development
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
Run the generator:
|
|
@@ -114,8 +114,10 @@ All generated code lives in your app -- edit it directly:
|
|
|
114
114
|
|
|
115
115
|
- **BackstageController:** Inherits from `ActionController::Base` (bypasses app's ApplicationController concerns)
|
|
116
116
|
- **Initializer:** Credentials-first auth with ENV variable fallback, database connection config
|
|
117
|
-
- **Route:** Mounts `SolidErrors::Engine`
|
|
118
|
-
- **
|
|
117
|
+
- **Route:** Mounts `SolidErrors::Engine` at `<prefix>/errors`
|
|
118
|
+
- **Mailer templates:** Self-contained `error_occurred` HTML and text templates, so `send_emails` works without the dashboard's helpers
|
|
119
|
+
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
120
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
119
121
|
- **Custom layout:** Tailwind-styled layout with admin navigation and toast flash messages
|
|
120
122
|
- **Stimulus controllers:** `clipboard_controller.js` and `backtrace_filter_controller.js`
|
|
121
123
|
- **Custom views:** Tailwind-styled views to override the gem defaults (included by default, use `--no-copy-views` to skip)
|
|
@@ -156,8 +158,9 @@ Credentials are resolved in order:
|
|
|
156
158
|
- **BackstageController:** Inherits from `ActionController::Base` with maquina_components helpers (bypasses app's ApplicationController concerns)
|
|
157
159
|
- **Helper:** `MissionControlHelper` with `job_status_badge_variant` and `nav_icon_for_section`
|
|
158
160
|
- **Initializer:** Sets base controller class, credentials-first auth with ENV variable fallback
|
|
159
|
-
- **Route:** Mounts `MissionControl::Jobs::Engine`
|
|
160
|
-
- **Admin navigation:** Shared navigation bar
|
|
161
|
+
- **Route:** Mounts `MissionControl::Jobs::Engine` at `<prefix>/jobs`
|
|
162
|
+
- **Admin navigation:** Shared navigation bar with Overview, Errors, Jobs and Security tabs
|
|
163
|
+
- **Admin overview:** `BackstageDashboardController` at the prefix root with cards for each installed dashboard and a `@metrics` slot for your own aggregate counts; answers 503 until backstage credentials are set
|
|
161
164
|
- **Custom layout:** Tailwind-styled layout with admin navigation, toast flash messages, application/server selection, and tab navigation
|
|
162
165
|
- **Custom views:** Tailwind-styled views for jobs, queues, workers, and recurring tasks (included by default, use `--no-copy-views` to skip)
|
|
163
166
|
|
|
@@ -218,28 +221,104 @@ rails g maquina:solid_queue --database postgresql # PostgreSQL
|
|
|
218
221
|
|
|
219
222
|
### Rack Attack -- Request Protection
|
|
220
223
|
|
|
221
|
-
**Rack Attack** installs the [rack-attack](https://github.com/rack/rack-attack) gem with
|
|
224
|
+
**Rack Attack** installs the [rack-attack](https://github.com/rack/rack-attack) gem with rules that ban vulnerability scanners and addresses that ignore the throttle.
|
|
222
225
|
|
|
223
226
|
#### What it generates
|
|
224
227
|
|
|
225
|
-
- **Initializer:** `config/initializers/rack_attack.rb` with
|
|
228
|
+
- **Initializer:** `config/initializers/rack_attack.rb` with the bans, throttles, safelist, 403 responder and an `[ATTACK]` log line per refusal. Its knobs are constants on `Rack::Attack` (`SCANNER_*`, `GENERAL_*`, `FLOOD_*`, `LOGIN_*`) and `Rack::Attack.banned?(ip)` answers whether an address is banned now.
|
|
226
229
|
|
|
227
230
|
#### Usage
|
|
228
231
|
|
|
229
232
|
```bash
|
|
230
233
|
rails g maquina:rack_attack
|
|
234
|
+
rails g maquina:rack_attack --login-path /sign_in # Throttle a different sign-in path
|
|
231
235
|
```
|
|
232
236
|
|
|
233
237
|
The generator automatically runs `bundle install`.
|
|
234
238
|
|
|
235
239
|
#### Default Protections
|
|
236
240
|
|
|
237
|
-
- **
|
|
241
|
+
- **Scanner ban (Fail2Ban):** three scanner paths in 10 minutes bans the IP for 7 days. Scanner paths are PHP files (`*.php`), WordPress paths (`wp-admin`, `wp-login`, etc.), sensitive files anywhere in the path (`.env`, `.git`, `/etc/passwd`, etc.), backup archives (`.zip`, `.sql`, `.tar.gz`, `.bak`, etc.) and scanner targets (`phpmyadmin`, `cgi-bin`, etc.). `/rails/active_storage` is exempt.
|
|
242
|
+
- **Flood ban (Allow2Ban):** 600 requests in 5 minutes (twice the general throttle) bans the IP for 1 day.
|
|
243
|
+
- **Throttles:** 300 requests/5min per IP (general; `/assets` and `/rails/active_storage` exempt), 5 `POST /session`/20s per IP
|
|
238
244
|
- **Safelists:** Localhost (`127.0.0.1`, `::1`)
|
|
239
|
-
- **
|
|
240
|
-
- **Responses:** 403 Forbidden for blocklisted, 429 Too Many Requests for throttled
|
|
245
|
+
- **Responses:** 403 Forbidden for blocklisted and banned, 429 Too Many Requests for throttled
|
|
241
246
|
|
|
242
|
-
Customize rules in `config/initializers/rack_attack.rb`.
|
|
247
|
+
Bans live in `Rails.cache`, so they need a shared cache store (Solid Cache) in production. Customize rules in `config/initializers/rack_attack.rb`. To see the refusals, add `maquina:security`.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
### Security -- Rack::Attack with an Abuse Dashboard
|
|
252
|
+
|
|
253
|
+
**Security** records every Rack::Attack refusal in its own database and summarises the last week on a backstage page next to Solid Errors and Mission Control Jobs.
|
|
254
|
+
|
|
255
|
+
#### What it generates
|
|
256
|
+
|
|
257
|
+
- **Rack::Attack rules:** runs `maquina:rack_attack` unless `config/initializers/rack_attack.rb` already defines `Rack::Attack.banned?` (an older initializer without it is replaced)
|
|
258
|
+
- **Subscriber:** `config/initializers/rack_attack_events.rb` writes a `Security::AbuseEvent` per throttled, blocked or banned request; a failed write never turns a 403 into a 500
|
|
259
|
+
- **Models:** `Security::Record` (`connects_to` the `security` database), `Security::AbuseEvent` (30-day retention), `Security::AbuseReport` (the page's figures)
|
|
260
|
+
- **Database:** `db/security_schema.rb` and a `security:` entry in every multi-database environment of `config/database.yml` (and `config/database.yml.example`)
|
|
261
|
+
- **Retention:** a daily `purge_abuse_events` task in `config/recurring.yml`
|
|
262
|
+
- **BackstageController**, **controller** (`Backstage::SecurityController`), **route** (`<prefix>/security`), **layout** and **views**: stats, throttled addresses, blocked addresses (banned now, expired or not yet banned), scanner paths, sign-in throttles, targeted hosts, refusals by day, recent refusals (views skipped with `--no-copy-views`)
|
|
263
|
+
- **Admin navigation:** a Security tab, added to an existing `_admin_navigation` partial too
|
|
264
|
+
- **Admin overview:** the shared dashboard at the prefix root, if Solid Errors or Mission Control Jobs have not installed it already
|
|
265
|
+
|
|
266
|
+
#### What the page shows
|
|
267
|
+
|
|
268
|
+
The last 7 days of refusals, top five of each, read from `Security::AbuseEvent` through `Security::AbuseReport`. It is a briefing, not a log viewer: no filters, no pagination. Times are UTC.
|
|
269
|
+
|
|
270
|
+
- **Stats:** refused requests, throttled, blocked or banned, and distinct addresses
|
|
271
|
+
- **Throttled addresses:** over the general or sign-in limit, busiest first, with the rules they hit
|
|
272
|
+
- **Blocked addresses:** every address a blocklist refused, marked **Banned now** (Rack::Attack's own answer at render time), **Expired**, or **Not banned** -- an address with a strike or two on the scanner rule, or a slow scanner that stays under three paths in ten minutes
|
|
273
|
+
- **Most requested blocked paths:** what the scanners were after. A banned address is refused on every path, its ordinary pages included, so only the paths `Rack::Attack.scanner_path?` recognises are listed
|
|
274
|
+
- **Sign-in throttles:** addresses refused on `POST <login-path>`, kept apart so scanner noise never buries them
|
|
275
|
+
- **Targeted hosts:** which hostnames the refusals were aimed at
|
|
276
|
+
- **By day:** throttled, blocked and banned per day, zeros kept so a spike reads against its neighbours
|
|
277
|
+
- **Recent refusals:** the last 20, newest first
|
|
278
|
+
|
|
279
|
+
#### Usage
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
rails g maquina:security --prefix /admin
|
|
283
|
+
bin/rails db:prepare
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
The generator automatically runs `bundle install`. The page needs maquina_components.
|
|
287
|
+
|
|
288
|
+
#### Options
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
rails g maquina:security --prefix /admin # Default
|
|
292
|
+
rails g maquina:security --prefix /admin --login-path /sign_in # Different sign-in path
|
|
293
|
+
rails g maquina:security --prefix /admin --no-copy-views # Without views
|
|
294
|
+
rails g maquina:security --prefix /backstage \
|
|
295
|
+
--user-env-var ADMIN_USER --password-env-var ADMIN_PASSWORD # Custom env vars
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### Authentication
|
|
299
|
+
|
|
300
|
+
HTTP Basic Auth, with credentials resolved in order:
|
|
301
|
+
|
|
302
|
+
1. `Rails.application.credentials.backstage.username` / `.password`
|
|
303
|
+
2. `ENV["SECURITY_USER"]` / `ENV["SECURITY_PASSWORD"]` (configurable)
|
|
304
|
+
|
|
305
|
+
The page answers 503 until one of them is set, so it is never left open.
|
|
306
|
+
|
|
307
|
+
#### Trying it locally
|
|
308
|
+
|
|
309
|
+
Localhost is safelisted, so your own requests are never refused. Rack::Attack trusts `X-Forwarded-For` from `127.0.0.1`, so send one to stand in for another address:
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
for path in /wp-login.php /.env /backup.zip /; do
|
|
313
|
+
curl -s -o /dev/null -w "%{http_code} $path\n" -H "X-Forwarded-For: 203.0.113.10" http://localhost:3000$path
|
|
314
|
+
done
|
|
315
|
+
# 403 /wp-login.php
|
|
316
|
+
# 403 /.env
|
|
317
|
+
# 403 /backup.zip <- third scanner path: banned for 7 days
|
|
318
|
+
# 403 / <- banned, so every path is refused
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Development's `:memory_store` keeps bans inside the server process, so they reset on restart and a `bin/rails runner` script cannot see them. Production needs a shared store (Solid Cache).
|
|
243
322
|
|
|
244
323
|
---
|
|
245
324
|
|
|
@@ -259,10 +338,10 @@ Customize rules in `config/initializers/rack_attack.rb`.
|
|
|
259
338
|
8. Sets up ActiveStorage JavaScript imports
|
|
260
339
|
9. Adds turbo morphing, `yield :head`, and simplifies `<main>` tag in layout
|
|
261
340
|
10. Optionally installs authentication (`maquina:clave` or `maquina:registration`)
|
|
262
|
-
11. Invokes sub-generators: `maquina:
|
|
263
|
-
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`
|
|
341
|
+
11. Invokes sub-generators: `maquina:mission_control_jobs`, `maquina:solid_errors`
|
|
342
|
+
12. Runs external installers: `solid_queue:install`, `solid_errors:install`, `solid_cache:install`, `solid_cable:install`, `maquina_components:install`, then `maquina:security` (which installs the `maquina:rack_attack` rules)
|
|
264
343
|
13. Restores custom layouts overwritten by gem installers
|
|
265
|
-
14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors)
|
|
344
|
+
14. Configures multi-database `database.yml` (primary, queue, cache, cable, errors, security)
|
|
266
345
|
15. Creates a HomeController with root route
|
|
267
346
|
16. Generates a README and `database.yml.example`
|
|
268
347
|
17. Runs `db:prepare`
|
|
@@ -277,7 +356,7 @@ rails g maquina:app --auth registration
|
|
|
277
356
|
|
|
278
357
|
#### Options
|
|
279
358
|
|
|
280
|
-
- `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs)
|
|
359
|
+
- `--prefix` (default: `/admin`) -- Base path prefix for backstage tools (Solid Errors, Mission Control Jobs, Security)
|
|
281
360
|
- `--port` (default: `3000`) -- Default port for the development server
|
|
282
361
|
- `--auth` (default: `none`) -- Authentication type: `none`, `clave`, or `registration`
|
|
283
362
|
|
|
@@ -20,7 +20,16 @@ module Maquina
|
|
|
20
20
|
content = File.read(gemfile_path)
|
|
21
21
|
|
|
22
22
|
remove_gems = ["rubocop-rails-omakase"]
|
|
23
|
-
|
|
23
|
+
# standard >= 1.54: avoids the inoperative placeholder releases
|
|
24
|
+
# (1.34.0.1 / 1.35.0.1) whose too-loose `rubocop ~> 1.62` requirement
|
|
25
|
+
# makes bundler backtrack onto them and pair them with an incompatible
|
|
26
|
+
# rubocop, so the `standard` binary refuses to run.
|
|
27
|
+
#
|
|
28
|
+
# standard.rb runs through the generated .rubocop.yml (see
|
|
29
|
+
# create_config_files) — that file is standard.rb's runner bridge, not
|
|
30
|
+
# custom RuboCop rules, so it ships even though guidance says "no
|
|
31
|
+
# .rubocop.yml".
|
|
32
|
+
dev_gems = {"brakeman" => nil, "bundle-audit" => nil, "letter_opener" => nil, "standard" => ">= 1.54"}
|
|
24
33
|
runtime_gems = {"rails-i18n" => nil, "maquina-components" => nil}
|
|
25
34
|
production_gems = {"aws-sdk-s3" => nil}
|
|
26
35
|
|
|
@@ -28,22 +37,19 @@ module Maquina
|
|
|
28
37
|
gsub_file "Gemfile", /^\s*gem\s+["']#{gem_name}["'].*\n/, ""
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
dev_gems.each do |gem_name,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
end
|
|
40
|
+
dev_gems.each do |gem_name, version|
|
|
41
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
42
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version, group: :development)}\n"
|
|
35
43
|
end
|
|
36
44
|
|
|
37
|
-
runtime_gems.each do |gem_name,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
end
|
|
45
|
+
runtime_gems.each do |gem_name, version|
|
|
46
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
47
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version)}\n"
|
|
41
48
|
end
|
|
42
49
|
|
|
43
|
-
production_gems.each do |gem_name,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
end
|
|
50
|
+
production_gems.each do |gem_name, version|
|
|
51
|
+
next if content.include?("gem \"#{gem_name}\"")
|
|
52
|
+
append_to_file "Gemfile", "\n#{gem_line(gem_name, version, group: :production)}\n"
|
|
47
53
|
end
|
|
48
54
|
|
|
49
55
|
return unless rails_app?
|
|
@@ -193,7 +199,6 @@ module Maquina
|
|
|
193
199
|
def install_maquina_generators
|
|
194
200
|
return unless rails_app?
|
|
195
201
|
|
|
196
|
-
generate "maquina:rack_attack", "--quiet"
|
|
197
202
|
generate "maquina:mission_control_jobs", "--prefix", options[:prefix], "--quiet"
|
|
198
203
|
generate "maquina:solid_errors", "--prefix", options[:prefix], "--quiet"
|
|
199
204
|
|
|
@@ -204,6 +209,10 @@ module Maquina
|
|
|
204
209
|
system("bin/rails generate solid_cable:install", chdir: destination_root)
|
|
205
210
|
system("bin/rails generate maquina_components:install", chdir: destination_root)
|
|
206
211
|
end
|
|
212
|
+
|
|
213
|
+
# After solid_queue:install, so config/recurring.yml exists for the purge.
|
|
214
|
+
# Installs the maquina:rack_attack rules too.
|
|
215
|
+
generate "maquina:security", "--prefix", options[:prefix], "--quiet"
|
|
207
216
|
end
|
|
208
217
|
|
|
209
218
|
# 13. Restore custom layouts overwritten by gem installers
|
|
@@ -255,7 +264,45 @@ module Maquina
|
|
|
255
264
|
end
|
|
256
265
|
end
|
|
257
266
|
|
|
258
|
-
# 18.
|
|
267
|
+
# 18. Restore database.yml from its example in bin/setup. config/database.yml
|
|
268
|
+
# is gitignored (it differs per environment), so a fresh clone has only
|
|
269
|
+
# the committed example — bin/setup must copy it before db:prepare.
|
|
270
|
+
def configure_bin_setup
|
|
271
|
+
setup_file = File.join(destination_root, "bin/setup")
|
|
272
|
+
return unless File.exist?(setup_file)
|
|
273
|
+
return if File.read(setup_file).include?("config/database.yml.example")
|
|
274
|
+
|
|
275
|
+
inject_into_file "bin/setup",
|
|
276
|
+
after: %r{system\("bundle check"\) \|\| system!\("bundle install"\)\n} do
|
|
277
|
+
<<~RUBY.indent(2)
|
|
278
|
+
|
|
279
|
+
# config/database.yml is gitignored; restore it from the committed example
|
|
280
|
+
unless File.exist?("config/database.yml")
|
|
281
|
+
puts "\\n== Copying config/database.yml =="
|
|
282
|
+
FileUtils.cp "config/database.yml.example", "config/database.yml"
|
|
283
|
+
end
|
|
284
|
+
RUBY
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# 19. Restore database.yml from its example in CI. Without it, the test job
|
|
289
|
+
# boots with no database.yml and every Rails task fails. Only touched
|
|
290
|
+
# when the host app already has a GitHub Actions workflow.
|
|
291
|
+
def configure_ci
|
|
292
|
+
ci_file = File.join(destination_root, ".github/workflows/ci.yml")
|
|
293
|
+
return unless File.exist?(ci_file)
|
|
294
|
+
return if File.read(ci_file).include?("config/database.yml.example")
|
|
295
|
+
|
|
296
|
+
inject_into_file ".github/workflows/ci.yml",
|
|
297
|
+
before: /^ - name: Run tests$/ do
|
|
298
|
+
<<~YAML.indent(6)
|
|
299
|
+
- name: Prepare database config
|
|
300
|
+
run: cp config/database.yml.example config/database.yml
|
|
301
|
+
YAML
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
# 20. Run all migrations
|
|
259
306
|
def run_migrations
|
|
260
307
|
return unless rails_app?
|
|
261
308
|
|
|
@@ -264,7 +311,7 @@ module Maquina
|
|
|
264
311
|
end
|
|
265
312
|
end
|
|
266
313
|
|
|
267
|
-
#
|
|
314
|
+
# 21. Post-install message
|
|
268
315
|
def show_post_install
|
|
269
316
|
say ""
|
|
270
317
|
say "=" * 60, :green
|
|
@@ -279,10 +326,12 @@ module Maquina
|
|
|
279
326
|
say " 2. Start the app: bin/dev"
|
|
280
327
|
say ""
|
|
281
328
|
say "Installed components:", :yellow
|
|
282
|
-
say " - Rack::Attack:
|
|
329
|
+
say " - Rack::Attack: scanner and flood bans, throttles (config/initializers/rack_attack.rb)"
|
|
330
|
+
say " - Security: Rack::Attack refusals at #{options[:prefix]}/security"
|
|
283
331
|
say " - Solid Queue: background jobs (config/solid_queue.yml)"
|
|
284
|
-
say " - Mission Control Jobs: job dashboard at #{options[:prefix]}/
|
|
285
|
-
say " - Solid Errors: error tracking at #{options[:prefix]}/
|
|
332
|
+
say " - Mission Control Jobs: job dashboard at #{options[:prefix]}/jobs"
|
|
333
|
+
say " - Solid Errors: error tracking at #{options[:prefix]}/errors"
|
|
334
|
+
say " - Admin overview: #{options[:prefix]}"
|
|
286
335
|
say " - Maquina Components: UI components"
|
|
287
336
|
if options[:auth] != "none"
|
|
288
337
|
say ""
|
|
@@ -298,6 +347,13 @@ module Maquina
|
|
|
298
347
|
|
|
299
348
|
private
|
|
300
349
|
|
|
350
|
+
def gem_line(name, version, group: nil)
|
|
351
|
+
line = "gem \"#{name}\""
|
|
352
|
+
line << ", \"#{version}\"" if version
|
|
353
|
+
line << ", group: :#{group}" if group
|
|
354
|
+
line
|
|
355
|
+
end
|
|
356
|
+
|
|
301
357
|
def rails_app?
|
|
302
358
|
File.exist?(File.join(destination_root, "bin/rails"))
|
|
303
359
|
end
|
|
@@ -1,3 +1,9 @@
|
|
|
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.
|
|
1
7
|
require:
|
|
2
8
|
- standard
|
|
3
9
|
|
|
@@ -34,15 +34,17 @@ Your app will be running at **http://localhost:<%= options[:port] %>**.
|
|
|
34
34
|
| **WebSockets** | Solid Cable |
|
|
35
35
|
| **Rich Text** | Action Text |
|
|
36
36
|
| **File Storage** | Active Storage (S3-ready in production) |
|
|
37
|
-
| **Multi-database** | Separate databases for queue, cache, cable, and
|
|
37
|
+
| **Multi-database** | Separate databases for queue, cache, cable, errors, and security |
|
|
38
38
|
|
|
39
39
|
## Development URLs
|
|
40
40
|
|
|
41
41
|
| URL | Description |
|
|
42
42
|
|-----|-------------|
|
|
43
43
|
| http://localhost:<%= options[:port] %> | Application |
|
|
44
|
-
| http://localhost:<%= options[:port] %><%= options[:prefix]
|
|
45
|
-
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/
|
|
44
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %> | Admin overview |
|
|
45
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/errors | Errors dashboard |
|
|
46
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/jobs | Jobs dashboard |
|
|
47
|
+
| http://localhost:<%= options[:port] %><%= options[:prefix] %>/security | Security dashboard |
|
|
46
48
|
<% if options[:auth] != "none" -%>
|
|
47
49
|
| http://localhost:<%= options[:port] %>/session/new | Sign in |
|
|
48
50
|
<% if options[:auth] == "registration" -%>
|
|
@@ -65,7 +67,7 @@ Edit credentials with:
|
|
|
65
67
|
bin/rails credentials:edit
|
|
66
68
|
```
|
|
67
69
|
|
|
68
|
-
Add backstage credentials for the
|
|
70
|
+
Add backstage credentials for the admin dashboards (overview, errors, jobs, security):
|
|
69
71
|
|
|
70
72
|
```yaml
|
|
71
73
|
backstage:
|
|
@@ -32,17 +32,17 @@
|
|
|
32
32
|
<%% if authenticated? %>
|
|
33
33
|
<section class="max-w-5xl mx-auto px-5 py-12">
|
|
34
34
|
<p class="text-sm font-semibold uppercase tracking-wider text-primary mb-4">Admin Tools</p>
|
|
35
|
-
<div class="grid grid-cols-1 md:grid-cols-
|
|
35
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
36
36
|
<%%= render "components/card" do %>
|
|
37
37
|
<%%= render "components/card/header" do %>
|
|
38
38
|
<div class="flex items-center gap-2">
|
|
39
|
-
<%%= icon_for(:
|
|
39
|
+
<%%= icon_for(:triangle_alert, class: "size-5 text-primary") %>
|
|
40
40
|
<%%= render "components/card/title", text: "Solid Errors" %>
|
|
41
41
|
</div>
|
|
42
42
|
<%%= render "components/card/description", text: "Browse, resolve, and clean up application exceptions in real time." %>
|
|
43
43
|
<%% end %>
|
|
44
44
|
<%%= render "components/card/footer" do %>
|
|
45
|
-
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/
|
|
45
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/errors", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
46
46
|
<%% end %>
|
|
47
47
|
<%% end %>
|
|
48
48
|
|
|
@@ -55,7 +55,20 @@
|
|
|
55
55
|
<%%= render "components/card/description", text: "Monitor queues, retry failed jobs, and manage background processing." %>
|
|
56
56
|
<%% end %>
|
|
57
57
|
<%%= render "components/card/footer" do %>
|
|
58
|
-
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/
|
|
58
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/jobs", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
59
|
+
<%% end %>
|
|
60
|
+
<%% end %>
|
|
61
|
+
|
|
62
|
+
<%%= render "components/card" do %>
|
|
63
|
+
<%%= render "components/card/header" do %>
|
|
64
|
+
<div class="flex items-center gap-2">
|
|
65
|
+
<%%= icon_for(:slash, class: "size-5 text-primary") %>
|
|
66
|
+
<%%= render "components/card/title", text: "Security" %>
|
|
67
|
+
</div>
|
|
68
|
+
<%%= render "components/card/description", text: "See what Rack::Attack throttled, blocked, and banned this week." %>
|
|
69
|
+
<%% end %>
|
|
70
|
+
<%%= render "components/card/footer" do %>
|
|
71
|
+
<%%= link_to "Open Dashboard", "<%= options[:prefix] %>/security", data: { component: "button", variant: "outline", size: "sm" } %>
|
|
59
72
|
<%% end %>
|
|
60
73
|
<%% end %>
|
|
61
74
|
</div>
|
|
@@ -109,7 +122,7 @@
|
|
|
109
122
|
<%%= render "components/card/title", text: "Rate Limiting" %>
|
|
110
123
|
<%% end %>
|
|
111
124
|
<%%= render "components/card/content" do %>
|
|
112
|
-
<p class="text-sm text-muted-foreground">Rack::Attack
|
|
125
|
+
<p class="text-sm text-muted-foreground">Rack::Attack bans scanners and floods and throttles sign-in. Every refusal shows on the Security dashboard.</p>
|
|
113
126
|
<%% end %>
|
|
114
127
|
<%% end %>
|
|
115
128
|
|
|
@@ -29,6 +29,10 @@ development:
|
|
|
29
29
|
<<: *default
|
|
30
30
|
database: storage/development_errors.sqlite3
|
|
31
31
|
migrations_paths: db/errors_migrate
|
|
32
|
+
security:
|
|
33
|
+
<<: *default
|
|
34
|
+
database: storage/development_security.sqlite3
|
|
35
|
+
migrations_paths: db/security_migrate
|
|
32
36
|
|
|
33
37
|
# Warning: The database defined as "test" will be erased and
|
|
34
38
|
# re-generated from your development database when you run "rake".
|
|
@@ -53,6 +57,10 @@ test:
|
|
|
53
57
|
<<: *default
|
|
54
58
|
database: storage/test_errors.sqlite3
|
|
55
59
|
migrations_paths: db/errors_migrate
|
|
60
|
+
security:
|
|
61
|
+
<<: *default
|
|
62
|
+
database: storage/test_security.sqlite3
|
|
63
|
+
migrations_paths: db/security_migrate
|
|
56
64
|
|
|
57
65
|
# Store production database in the storage/ directory, which by default
|
|
58
66
|
# is mounted as a persistent Docker volume in config/deploy.yml.
|
|
@@ -76,3 +84,7 @@ production:
|
|
|
76
84
|
<<: *default
|
|
77
85
|
database: storage/production_errors.sqlite3
|
|
78
86
|
migrations_paths: db/errors_migrate
|
|
87
|
+
security:
|
|
88
|
+
<<: *default
|
|
89
|
+
database: storage/production_security.sqlite3
|
|
90
|
+
migrations_paths: db/security_migrate
|
|
@@ -49,7 +49,7 @@ module Maquina
|
|
|
49
49
|
|
|
50
50
|
# 5. Routes
|
|
51
51
|
def add_route
|
|
52
|
-
mount_path = "#{options[:prefix]}/
|
|
52
|
+
mount_path = "#{options[:prefix]}/jobs"
|
|
53
53
|
|
|
54
54
|
route "mount MissionControl::Jobs::Engine, at: \"#{mount_path}\""
|
|
55
55
|
end
|
|
@@ -67,6 +67,13 @@ module Maquina
|
|
|
67
67
|
layout_files.each { |f| copy_file f, f }
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# 7b. Backstage dashboard — admin overview at the prefix root, linking to
|
|
71
|
+
# the installed engines. Shared with the solid_errors generator, so
|
|
72
|
+
# guard every step for idempotency when both run.
|
|
73
|
+
def create_backstage_dashboard
|
|
74
|
+
install_backstage_dashboard
|
|
75
|
+
end
|
|
76
|
+
|
|
70
77
|
# 8. Custom views
|
|
71
78
|
def copy_views
|
|
72
79
|
return unless options[:copy_views]
|
|
@@ -105,6 +112,30 @@ module Maquina
|
|
|
105
112
|
File.exist?(File.join(destination_root, "bin/rails"))
|
|
106
113
|
end
|
|
107
114
|
|
|
115
|
+
# Idempotent install of the shared backstage dashboard (controller, layout,
|
|
116
|
+
# prefix-aware view, and the prefix-root route). Safe to run from both this
|
|
117
|
+
# generator and solid_errors.
|
|
118
|
+
def install_backstage_dashboard
|
|
119
|
+
unless File.exist?(File.join(destination_root, "app/controllers/backstage_dashboard_controller.rb"))
|
|
120
|
+
copy_file "app/controllers/backstage_dashboard_controller.rb",
|
|
121
|
+
"app/controllers/backstage_dashboard_controller.rb"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
unless File.exist?(File.join(destination_root, "app/views/layouts/admin.html.erb"))
|
|
125
|
+
copy_file "app/views/layouts/admin.html.erb", "app/views/layouts/admin.html.erb"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
unless File.exist?(File.join(destination_root, "app/views/backstage_dashboard/index.html.erb"))
|
|
129
|
+
template "app/views/backstage_dashboard/index.html.erb.tt",
|
|
130
|
+
"app/views/backstage_dashboard/index.html.erb"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
routes_path = File.join(destination_root, "config/routes.rb")
|
|
134
|
+
if File.exist?(routes_path) && !File.read(routes_path).include?("backstage_dashboard#index")
|
|
135
|
+
route %(get "#{options[:prefix]}" => "backstage_dashboard#index", as: :backstage_dashboard)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
108
139
|
def layout_files
|
|
109
140
|
layouts_dir = File.join(self.class.source_root, "app/views/layouts/mission_control")
|
|
110
141
|
Dir.glob("**/*.erb", base: layouts_dir).map do |file|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Admin overview — a backstage surface alongside the SolidErrors and Mission
|
|
4
|
+
# Control Jobs engines, under the same backstage Basic auth.
|
|
5
|
+
#
|
|
6
|
+
# Out of the box it shows the installed engine entry points and an empty metrics
|
|
7
|
+
# area. Populate @metrics in #index with aggregate, non-sensitive counts for
|
|
8
|
+
# your app — counts, not names or per-record detail.
|
|
9
|
+
class BackstageDashboardController < BackstageController
|
|
10
|
+
layout "admin"
|
|
11
|
+
|
|
12
|
+
before_action :require_backstage_credentials
|
|
13
|
+
before_action :authenticate_backstage
|
|
14
|
+
|
|
15
|
+
def index
|
|
16
|
+
@generated_at = Time.current
|
|
17
|
+
|
|
18
|
+
# Add overview metrics here. Each entry renders as a card:
|
|
19
|
+
#
|
|
20
|
+
# @metrics = [
|
|
21
|
+
# {label: "Accounts", icon: :folder, value: Account.count, hint: "tenants"},
|
|
22
|
+
# {label: "Users", icon: :user, value: User.count, hint: "members"}
|
|
23
|
+
# ]
|
|
24
|
+
#
|
|
25
|
+
# Keep them aggregate-only. :total and :hint are optional.
|
|
26
|
+
@metrics = []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# Closed (503) until credentials.backstage is set. Without this an unset
|
|
32
|
+
# username and password are both "", and an empty Basic header matches them.
|
|
33
|
+
def require_backstage_credentials
|
|
34
|
+
backstage = Rails.application.credentials.backstage || {}
|
|
35
|
+
head :service_unavailable if backstage[:username].blank? || backstage[:password].blank?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Same backstage credentials the engines use.
|
|
39
|
+
def authenticate_backstage
|
|
40
|
+
authenticate_or_request_with_http_basic("Admin") do |username, password|
|
|
41
|
+
backstage = Rails.application.credentials.backstage || {}
|
|
42
|
+
|
|
43
|
+
ActiveSupport::SecurityUtils.secure_compare(username.to_s, backstage[:username].to_s) &
|
|
44
|
+
ActiveSupport::SecurityUtils.secure_compare(password.to_s, backstage[:password].to_s)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|