maquina-generators 0.6.0 → 0.7.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 +31 -0
- data/lib/generators/maquina/clave/clave_generator.rb +48 -0
- data/lib/generators/maquina/clave/templates/app/controllers/registration/verifications_controller.rb.tt +10 -1
- data/lib/generators/maquina/clave/templates/app/controllers/session/verifications_controller.rb.tt +10 -1
- data/lib/generators/maquina/clave/templates/app/models/email_verification.rb.tt +18 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.en.yml +2 -0
- data/lib/generators/maquina/clave/templates/config/locales/clave.es.yml +2 -0
- data/lib/generators/maquina/csp_reports/USAGE +18 -0
- data/lib/generators/maquina/csp_reports/csp_reports_generator.rb +231 -0
- data/lib/generators/maquina/csp_reports/templates/app/controllers/csp_reports_controller.rb +24 -0
- data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation.rb +89 -0
- data/lib/generators/maquina/csp_reports/templates/app/models/security/csp_violation_report.rb +30 -0
- data/lib/generators/maquina/csp_reports/templates/app/views/backstage/security/_csp_violations.html.erb +42 -0
- data/lib/generators/maquina/csp_reports/templates/db/create_csp_violations.rb.tt +23 -0
- data/lib/maquina_generators/version.rb +1 -1
- data/test/generators/maquina/clave_generator_test.rb +74 -0
- data/test/generators/maquina/csp_reports_generator_test.rb +151 -0
- data/test/tmp/.gitignore +5 -0
- data/test/tmp/.rubocop.yml +11 -0
- data/test/tmp/.standard.yml +1 -0
- data/test/tmp/Gemfile +13 -1
- data/test/tmp/Procfile.dev +3 -0
- data/test/tmp/README.md +89 -0
- data/test/tmp/app/controllers/home_controller.rb +6 -0
- data/test/tmp/app/javascript/application.js +1 -0
- data/test/tmp/app/views/home/index.html.erb +204 -0
- data/test/tmp/app/views/layouts/application.html.erb +10 -0
- data/test/tmp/config/application.rb +15 -0
- data/test/tmp/config/database.yml +90 -0
- data/test/tmp/config/database.yml.example +90 -0
- data/test/tmp/config/environments/development.rb +8 -0
- data/test/tmp/config/environments/production.rb +6 -0
- data/test/tmp/config/initializers/generators.rb +4 -0
- data/test/tmp/config/routes.rb +1 -2
- metadata +27 -14
- data/test/tmp/app/controllers/application_controller.rb +0 -2
- data/test/tmp/app/controllers/registrations_controller.rb +0 -37
- data/test/tmp/app/models/account.rb +0 -5
- data/test/tmp/app/models/current.rb +0 -5
- data/test/tmp/app/models/user.rb +0 -12
- data/test/tmp/app/views/registrations/new.html.erb +0 -72
- data/test/tmp/app/views/sessions/new.html.erb +0 -46
- data/test/tmp/config/locales/registration.en.yml +0 -44
- data/test/tmp/config/locales/registration.es.yml +0 -44
- data/test/tmp/db/migrate/20260927024163_create_accounts.rb +0 -9
- data/test/tmp/db/migrate/20260927024164_add_account_fields_to_users.rb +0 -7
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<%# locals: (report:) %>
|
|
2
|
+
<%# Content Security Policy violations browsers reported to /csp-reports
|
|
3
|
+
(maquina:csp_reports), grouped so each row is one thing to fix. %>
|
|
4
|
+
<%= render "components/card" do %>
|
|
5
|
+
<%= render "components/card/header" do %>
|
|
6
|
+
<%= render "components/card/title", text: "Content Security Policy violations" %>
|
|
7
|
+
<%= render "components/card/description",
|
|
8
|
+
text: "#{number_with_delimiter(report.total)} reported in the last #{report.window.inspect}, kept for #{Security::CspViolation::RETENTION.inspect}. Most frequent first." %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= render "components/card/content", spacing: :full do %>
|
|
11
|
+
<% if report.empty? %>
|
|
12
|
+
<%= empty_state(title: "No violation was reported.", icon: :check_circle, size: :compact) %>
|
|
13
|
+
<% else %>
|
|
14
|
+
<div class="overflow-x-auto">
|
|
15
|
+
<%= render "components/table", container: false, "aria-label": "Content Security Policy violations" do %>
|
|
16
|
+
<%= render "components/table/header" do %>
|
|
17
|
+
<%= render "components/table/row" do %>
|
|
18
|
+
<%= render "components/table/head", text: "Directive" %>
|
|
19
|
+
<%= render "components/table/head", text: "Blocked" %>
|
|
20
|
+
<%= render "components/table/head", text: "Page" %>
|
|
21
|
+
<%= render "components/table/head", text: "Reports", css_classes: "text-right" %>
|
|
22
|
+
<%= render "components/table/head", text: "Last seen" %>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<%= render "components/table/body" do %>
|
|
26
|
+
<% report.top.each do |group| %>
|
|
27
|
+
<%= render "components/table/row" do %>
|
|
28
|
+
<%= render "components/table/cell", css_classes: "font-mono", text: group.effective_directive %>
|
|
29
|
+
<%= render "components/table/cell", css_classes: "font-mono whitespace-normal break-all", text: group.blocked_uri.presence || "—" %>
|
|
30
|
+
<%= render "components/table/cell", css_classes: "font-mono whitespace-normal break-all", text: group.document_uri.presence || "—" %>
|
|
31
|
+
<%= render "components/table/cell", css_classes: "text-right tabular-nums", text: number_with_delimiter(group.count) %>
|
|
32
|
+
<%= render "components/table/cell" do %>
|
|
33
|
+
<%= render "backstage/security/utc_time", time: group.last_seen %>
|
|
34
|
+
<% end %>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
<% end %>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Content Security Policy violation reports (maquina:csp_reports). Lives in the
|
|
2
|
+
# `security` database next to abuse_events; db/security_schema.rb carries the same
|
|
3
|
+
# table for a fresh database, this migration adds it to an existing one.
|
|
4
|
+
class CreateCspViolations < ActiveRecord::Migration<%= migration_version %>
|
|
5
|
+
def change
|
|
6
|
+
create_table :csp_violations do |t|
|
|
7
|
+
t.string :effective_directive, null: false
|
|
8
|
+
t.string :blocked_uri
|
|
9
|
+
t.string :document_uri
|
|
10
|
+
t.string :source_file
|
|
11
|
+
t.integer :line_number
|
|
12
|
+
t.integer :column_number
|
|
13
|
+
t.string :disposition
|
|
14
|
+
t.string :sample
|
|
15
|
+
t.string :ip
|
|
16
|
+
t.string :user_agent
|
|
17
|
+
t.datetime :created_at, null: false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :csp_violations, :created_at
|
|
21
|
+
add_index :csp_violations, [:effective_directive, :created_at]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -181,9 +181,83 @@ class Maquina::Generators::ClaveGeneratorTest < Rails::Generators::TestCase
|
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
|
|
184
|
+
test "the sign-in code is encrypted at rest and compared in constant time" do
|
|
185
|
+
run_generator
|
|
186
|
+
|
|
187
|
+
assert_file "app/models/email_verification.rb" do |content|
|
|
188
|
+
assert_match(/encrypts :code/, content)
|
|
189
|
+
assert_match(/def code_matches\?\(candidate\)/, content)
|
|
190
|
+
assert_match(/ActiveSupport::SecurityUtils\.secure_compare/, content)
|
|
191
|
+
end
|
|
192
|
+
%w[session registration].each do |flow|
|
|
193
|
+
assert_file "app/controllers/#{flow}/verifications_controller.rb" do |content|
|
|
194
|
+
assert_match(/unless @verification\.code_matches\?\(params\[:code\]\)/, content)
|
|
195
|
+
assert_no_match(/@verification\.code !=/, content)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
test "a code dies after five wrong guesses in both verification flows" do
|
|
201
|
+
run_generator
|
|
202
|
+
|
|
203
|
+
assert_file "app/models/email_verification.rb" do |content|
|
|
204
|
+
assert_match(/MAX_ATTEMPTS = 5/, content)
|
|
205
|
+
assert_match(/def locked\?\n attempts >= MAX_ATTEMPTS/, content)
|
|
206
|
+
end
|
|
207
|
+
{"session" => "sessions", "registration" => "registrations"}.each do |flow, scope|
|
|
208
|
+
assert_file "app/controllers/#{flow}/verifications_controller.rb" do |content|
|
|
209
|
+
assert_match(/if @verification\.locked\?\n @verification\.update!\(expires_at: Time\.current\)/, content)
|
|
210
|
+
assert_match(/t\("flash\.#{scope}\.code_locked"\)/, content)
|
|
211
|
+
assert content.index("@verification.locked?") < content.index("code_matches?"),
|
|
212
|
+
"the lock is checked before the guess, so a correct sixth guess is refused too"
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
%w[en es].each do |locale|
|
|
216
|
+
assert_file "config/locales/clave.#{locale}.yml" do |content|
|
|
217
|
+
assert_equal 2, content.scan(/^ code_locked: /).size
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
test "writes Active Record encryption keys into the credentials when they are missing" do
|
|
223
|
+
credentials = create_credentials("secret_key_base: abc\n")
|
|
224
|
+
|
|
225
|
+
output = run_generator
|
|
226
|
+
|
|
227
|
+
content = credentials.read
|
|
228
|
+
assert_match(/\Asecret_key_base: abc\n\nactive_record_encryption:\n primary_key: \w{32}\n deterministic_key: \w{32}\n key_derivation_salt: \w{32}\n\z/, content)
|
|
229
|
+
assert_match(/Added Active Record encryption keys/, output)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
test "leaves existing encryption keys alone" do
|
|
233
|
+
existing = "active_record_encryption:\n primary_key: keep\n"
|
|
234
|
+
credentials = create_credentials(existing)
|
|
235
|
+
|
|
236
|
+
run_generator
|
|
237
|
+
|
|
238
|
+
assert_equal existing, credentials.read
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
test "without a master key it explains how to add the encryption keys" do
|
|
242
|
+
output = run_generator
|
|
243
|
+
|
|
244
|
+
assert_no_file "config/credentials.yml.enc"
|
|
245
|
+
assert_match(/bin\/rails db:encryption:init/, output)
|
|
246
|
+
end
|
|
247
|
+
|
|
184
248
|
private
|
|
185
249
|
|
|
186
250
|
def mkdir_p(path)
|
|
187
251
|
FileUtils.mkdir_p(File.join(destination_root, path))
|
|
188
252
|
end
|
|
253
|
+
|
|
254
|
+
def create_credentials(content)
|
|
255
|
+
key = ActiveSupport::EncryptedConfiguration.generate_key
|
|
256
|
+
File.write(File.join(destination_root, "config/master.key"), key)
|
|
257
|
+
ActiveSupport::EncryptedConfiguration.new(
|
|
258
|
+
config_path: File.join(destination_root, "config/credentials.yml.enc"),
|
|
259
|
+
key_path: File.join(destination_root, "config/master.key"),
|
|
260
|
+
env_key: "RAILS_MASTER_KEY_UNUSED", raise_if_missing_key: true
|
|
261
|
+
).tap { |credentials| credentials.write(content) }
|
|
262
|
+
end
|
|
189
263
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "generators/maquina/csp_reports/csp_reports_generator"
|
|
3
|
+
|
|
4
|
+
class Maquina::Generators::CspReportsGeneratorTest < Rails::Generators::TestCase
|
|
5
|
+
tests Maquina::Generators::CspReportsGenerator
|
|
6
|
+
destination File.expand_path("../../tmp", __dir__)
|
|
7
|
+
|
|
8
|
+
SECURITY_SCHEMA = <<~RUBY
|
|
9
|
+
# The `security` database.
|
|
10
|
+
|
|
11
|
+
ActiveRecord::Schema[8.1].define(version: 1) do
|
|
12
|
+
create_table "abuse_events", force: :cascade do |t|
|
|
13
|
+
t.string "ip", null: false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
RUBY
|
|
17
|
+
|
|
18
|
+
CSP_INITIALIZER = <<~RUBY
|
|
19
|
+
Rails.application.configure do
|
|
20
|
+
config.content_security_policy do |policy|
|
|
21
|
+
policy.default_src :self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
config.content_security_policy_nonce_generator = ->(request) { request.session.id.to_s }
|
|
25
|
+
config.content_security_policy_report_only = true
|
|
26
|
+
end
|
|
27
|
+
RUBY
|
|
28
|
+
|
|
29
|
+
RACK_ATTACK = <<~RUBY
|
|
30
|
+
class Rack::Attack
|
|
31
|
+
def self.counted_ip(req)
|
|
32
|
+
req.ip unless req.path.start_with?("/assets", "/rails/active_storage")
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
RUBY
|
|
36
|
+
|
|
37
|
+
SECURITY_PAGE = <<~ERB
|
|
38
|
+
<div class="py-8 space-y-8">
|
|
39
|
+
<% if @report.empty? %>
|
|
40
|
+
empty
|
|
41
|
+
<% end %>
|
|
42
|
+
</div>
|
|
43
|
+
ERB
|
|
44
|
+
|
|
45
|
+
setup do
|
|
46
|
+
prepare_destination
|
|
47
|
+
write "config/routes.rb", "Rails.application.routes.draw do\nend\n"
|
|
48
|
+
write "db/security_schema.rb", SECURITY_SCHEMA
|
|
49
|
+
write "config/initializers/content_security_policy.rb", CSP_INITIALIZER
|
|
50
|
+
write "config/initializers/rack_attack.rb", RACK_ATTACK
|
|
51
|
+
write "config/recurring.yml", "production:\n purge_abuse_events:\n command: \"x\"\n"
|
|
52
|
+
write "app/views/backstage/security/show.html.erb", SECURITY_PAGE
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
test "refuses to run without the security database" do
|
|
56
|
+
File.delete(File.join(destination_root, "db/security_schema.rb"))
|
|
57
|
+
|
|
58
|
+
output = capture(:stderr) { run_generator }
|
|
59
|
+
|
|
60
|
+
assert_match(/Run `rails g maquina:security --prefix \/admin` first/, output)
|
|
61
|
+
assert_no_file "app/controllers/csp_reports_controller.rb"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
test "installs the endpoint, the models and the route" do
|
|
65
|
+
run_generator
|
|
66
|
+
|
|
67
|
+
assert_file "app/controllers/csp_reports_controller.rb", /class CspReportsController < ActionController::API/
|
|
68
|
+
assert_file "app/models/security/csp_violation.rb", /class Security::CspViolation < Security::Record/
|
|
69
|
+
assert_file "app/models/security/csp_violation_report.rb", /class Security::CspViolationReport/
|
|
70
|
+
assert_file "config/routes.rb", %r{post "/csp-reports", to: "csp_reports#create", as: :csp_reports}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
test "adds the table as a security migration and to the schema" do
|
|
74
|
+
run_generator
|
|
75
|
+
|
|
76
|
+
assert_migration "db/security_migrate/create_csp_violations.rb", /create_table :csp_violations/
|
|
77
|
+
migration = Dir[File.join(destination_root, "db/security_migrate/*_create_csp_violations.rb")].first
|
|
78
|
+
version = File.basename(migration).split("_").first
|
|
79
|
+
assert_file "db/security_schema.rb" do |content|
|
|
80
|
+
assert_match(/define\(version: #{version.sub(/\A(\d{4})(\d{2})(\d{2})(\d{6})\z/, '\1_\2_\3_\4')}\)/, content)
|
|
81
|
+
assert_match(/create_table "abuse_events".*create_table "csp_violations"/m, content)
|
|
82
|
+
assert_match(/\n end\nend\n\z/, content)
|
|
83
|
+
assert_no_match(/[ \t]+$/, content, "no trailing whitespace")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
test "points the policy at the endpoint and gives anonymous requests a nonce" do
|
|
88
|
+
run_generator %w[--path /security/csp]
|
|
89
|
+
|
|
90
|
+
assert_file "config/initializers/content_security_policy.rb" do |content|
|
|
91
|
+
assert_match(%r{config\.content_security_policy do \|policy\|\n.*policy\.report_uri "/security/csp"}m, content)
|
|
92
|
+
assert_no_match(/report_to/, content.gsub(/#.*$/, ""))
|
|
93
|
+
assert_match(/request\.session\.id\.to_s\.presence \|\| SecureRandom\.base64\(16\)/, content)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
test "takes the endpoint out of the general throttle and caps it on its own" do
|
|
98
|
+
run_generator
|
|
99
|
+
|
|
100
|
+
assert_file "config/initializers/rack_attack.rb" do |content|
|
|
101
|
+
assert_match(%r{start_with\?\("/assets", "/rails/active_storage", "/csp-reports"\)}, content)
|
|
102
|
+
assert_match(/Rack::Attack\.throttle\("csp\/ip", limit: 100, period: 5\.minutes\)/, content)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
test "extends a STATIC_FILE_PREFIXES list when the rules use one" do
|
|
107
|
+
write "config/initializers/rack_attack.rb", "class Rack::Attack\n STATIC_FILE_PREFIXES = %w[/assets /u/].freeze\nend\n"
|
|
108
|
+
|
|
109
|
+
run_generator
|
|
110
|
+
|
|
111
|
+
assert_file "config/initializers/rack_attack.rb", %r{STATIC_FILE_PREFIXES = %w\[/assets /u/ /csp-reports\]}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
test "purges daily and shows the section on the Security page" do
|
|
115
|
+
run_generator
|
|
116
|
+
|
|
117
|
+
assert_file "config/recurring.yml", /purge_csp_violations:\n command: "Security::CspViolation\.expired\.delete_all"/
|
|
118
|
+
assert_file "app/views/backstage/security/_csp_violations.html.erb", /Content Security Policy violations/
|
|
119
|
+
assert_file "app/views/backstage/security/show.html.erb",
|
|
120
|
+
%r{<% end %>\n\n <%= render "backstage/security/csp_violations", report: Security::CspViolationReport\.new %>\n</div>\n\z}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
test "running twice changes nothing the second time" do
|
|
124
|
+
run_generator
|
|
125
|
+
snapshot = %w[db/security_schema.rb config/initializers/rack_attack.rb config/initializers/content_security_policy.rb
|
|
126
|
+
config/recurring.yml app/views/backstage/security/show.html.erb config/routes.rb].to_h { |f| [f, File.read(File.join(destination_root, f))] }
|
|
127
|
+
|
|
128
|
+
run_generator
|
|
129
|
+
|
|
130
|
+
snapshot.each { |file, content| assert_equal content, File.read(File.join(destination_root, file)), file }
|
|
131
|
+
assert_equal 1, Dir[File.join(destination_root, "db/security_migrate/*_create_csp_violations.rb")].size
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
test "lists what it could not place instead of forcing it" do
|
|
135
|
+
write "config/initializers/content_security_policy.rb", "# Rails.application.configure do\n# end\n"
|
|
136
|
+
write "config/initializers/rack_attack.rb", "class Rack::Attack\nend\n"
|
|
137
|
+
|
|
138
|
+
output = run_generator
|
|
139
|
+
|
|
140
|
+
assert_match(%r{policy\.report_uri "/csp-reports"}, output)
|
|
141
|
+
assert_match(/Exempt \/csp-reports from the general throttle/, output)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def write(path, content)
|
|
147
|
+
full = File.join(destination_root, path)
|
|
148
|
+
FileUtils.mkdir_p(File.dirname(full))
|
|
149
|
+
File.write(full, content)
|
|
150
|
+
end
|
|
151
|
+
end
|
data/test/tmp/.gitignore
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# standard.rb's runner bridge, not custom RuboCop rules.
|
|
2
|
+
#
|
|
3
|
+
# This is the entry point RuboCop (and editors/LSP) need to run the Standard
|
|
4
|
+
# ruleset — all style comes from the `standard` gem via config/base.yml below.
|
|
5
|
+
# Keep this file even though general guidance says "no .rubocop.yml": for
|
|
6
|
+
# standard.rb this IS the runner config, not a custom rule set.
|
|
7
|
+
require:
|
|
8
|
+
- standard
|
|
9
|
+
|
|
10
|
+
inherit_gem:
|
|
11
|
+
standard: config/base.yml
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ruby_version: 3.2
|
data/test/tmp/Gemfile
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
1
|
source "https://rubygems.org"
|
|
2
2
|
|
|
3
|
-
gem "
|
|
3
|
+
gem "brakeman", group: :development
|
|
4
|
+
|
|
5
|
+
gem "bundle-audit", group: :development
|
|
6
|
+
|
|
7
|
+
gem "letter_opener", group: :development
|
|
8
|
+
|
|
9
|
+
gem "standard", ">= 1.54", group: :development
|
|
10
|
+
|
|
11
|
+
gem "rails-i18n"
|
|
12
|
+
|
|
13
|
+
gem "maquina-components"
|
|
14
|
+
|
|
15
|
+
gem "aws-sdk-s3", group: :production
|
data/test/tmp/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Tmp
|
|
2
|
+
|
|
3
|
+
Built with [maquina.app](https://maquina.app) — production-tested tools for developers who ship alone.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
### Prerequisites
|
|
8
|
+
|
|
9
|
+
- Ruby 3.2+
|
|
10
|
+
- Rails 8+
|
|
11
|
+
- PostgreSQL (or SQLite for development)
|
|
12
|
+
|
|
13
|
+
### Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bin/setup # Install dependencies and prepare databases
|
|
17
|
+
bin/dev # Start all processes (web, css, solid_queue)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Your app will be running at **http://localhost:3000**.
|
|
21
|
+
|
|
22
|
+
## What's Included
|
|
23
|
+
|
|
24
|
+
| Feature | Description |
|
|
25
|
+
|---------|-------------|
|
|
26
|
+
| **Background Jobs** | Solid Queue with Mission Control Jobs dashboard |
|
|
27
|
+
| **Error Tracking** | Solid Errors with custom UI |
|
|
28
|
+
| **Rate Limiting** | Rack::Attack with sensible defaults |
|
|
29
|
+
| **UI Components** | Maquina Components (ERB partials + Tailwind + Stimulus) |
|
|
30
|
+
| **Caching** | Solid Cache |
|
|
31
|
+
| **WebSockets** | Solid Cable |
|
|
32
|
+
| **Rich Text** | Action Text |
|
|
33
|
+
| **File Storage** | Active Storage (S3-ready in production) |
|
|
34
|
+
| **Multi-database** | Separate databases for queue, cache, cable, errors, and security |
|
|
35
|
+
|
|
36
|
+
## Development URLs
|
|
37
|
+
|
|
38
|
+
| URL | Description |
|
|
39
|
+
|-----|-------------|
|
|
40
|
+
| http://localhost:3000 | Application |
|
|
41
|
+
| http://localhost:3000/admin | Admin overview |
|
|
42
|
+
| http://localhost:3000/admin/errors | Errors dashboard |
|
|
43
|
+
| http://localhost:3000/admin/jobs | Jobs dashboard |
|
|
44
|
+
| http://localhost:3000/admin/security | Security dashboard |
|
|
45
|
+
|
|
46
|
+
## Scripts
|
|
47
|
+
|
|
48
|
+
- `bin/setup` — Install dependencies and prepare databases
|
|
49
|
+
- `bin/dev` — Start all processes (web, css, solid_queue)
|
|
50
|
+
|
|
51
|
+
## Credentials
|
|
52
|
+
|
|
53
|
+
Edit credentials with:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
bin/rails credentials:edit
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Add backstage credentials for the admin dashboards (overview, errors, jobs, security):
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
backstage:
|
|
63
|
+
username: your_user
|
|
64
|
+
password: your_password
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Maquina Ecosystem
|
|
68
|
+
|
|
69
|
+
### UI Components
|
|
70
|
+
|
|
71
|
+
[Maquina Components](https://maquina.app/documentation/components/) — a library of ERB partials, Tailwind CSS styles, and Stimulus controllers for building consistent, accessible UIs in Rails.
|
|
72
|
+
|
|
73
|
+
### AI Tools
|
|
74
|
+
|
|
75
|
+
Maquina provides a suite of AI-powered tools to accelerate your Rails development:
|
|
76
|
+
|
|
77
|
+
- **Rails MCP Server** — Model Context Protocol server for Rails applications
|
|
78
|
+
- **Rails Simplifier** — simplify and refine Rails code following 37signals patterns
|
|
79
|
+
- **Rails Upgrade Assistant** — comprehensive Rails upgrade reports and migration guides
|
|
80
|
+
- **Maquina UI Standards** — build consistent UIs using maquina_components
|
|
81
|
+
- **Neovim MCP Server** — MCP server for Neovim integration
|
|
82
|
+
|
|
83
|
+
Learn more at [maquina.app/documentation/ai-tools](https://maquina.app/documentation/ai-tools/).
|
|
84
|
+
|
|
85
|
+
## Learn More
|
|
86
|
+
|
|
87
|
+
Visit [maquina.app](https://maquina.app) for full documentation.
|
|
88
|
+
|
|
89
|
+
Maquina is built for the one-person framework philosophy: production-first, standard Rails, designed to scale with a small team.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Entry point for the build script
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
|
|
2
|
+
<section class="bg-primary/5 border-b border-border">
|
|
3
|
+
<div class="max-w-5xl mx-auto px-5 py-20 sm:py-28">
|
|
4
|
+
<h1 class="text-4xl sm:text-5xl font-bold tracking-tight text-foreground mb-5">
|
|
5
|
+
Tools for Rails developers<br class="hidden sm:block"> who ship alone
|
|
6
|
+
</h1>
|
|
7
|
+
<p class="text-lg text-muted-foreground leading-relaxed max-w-2xl mb-8">
|
|
8
|
+
Maquina is a growing collection of open-source tools extracted from production Rails applications.
|
|
9
|
+
UI components, background jobs, error tracking, and authentication — configured and ready to go.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
</section>
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
<section class="max-w-5xl mx-auto px-5 py-12">
|
|
20
|
+
<p class="text-sm font-semibold uppercase tracking-wider text-primary mb-6">What's Included</p>
|
|
21
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
<%= render "components/card" do %>
|
|
26
|
+
<%= render "components/card/header" do %>
|
|
27
|
+
<%= render "components/card/title", text: "Background Jobs" %>
|
|
28
|
+
<% end %>
|
|
29
|
+
<%= render "components/card/content" do %>
|
|
30
|
+
<p class="text-sm text-muted-foreground">Solid Queue backed by SQLite. Includes Mission Control dashboard for monitoring and managing jobs.</p>
|
|
31
|
+
<% end %>
|
|
32
|
+
<% end %>
|
|
33
|
+
|
|
34
|
+
<%= render "components/card" do %>
|
|
35
|
+
<%= render "components/card/header" do %>
|
|
36
|
+
<%= render "components/card/title", text: "Error Tracking" %>
|
|
37
|
+
<% end %>
|
|
38
|
+
<%= render "components/card/content" do %>
|
|
39
|
+
<p class="text-sm text-muted-foreground">Solid Errors captures exceptions automatically. Browse, resolve, and clean up — no external service needed.</p>
|
|
40
|
+
<% end %>
|
|
41
|
+
<% end %>
|
|
42
|
+
|
|
43
|
+
<%= render "components/card" do %>
|
|
44
|
+
<%= render "components/card/header" do %>
|
|
45
|
+
<%= render "components/card/title", text: "Rate Limiting" %>
|
|
46
|
+
<% end %>
|
|
47
|
+
<%= render "components/card/content" do %>
|
|
48
|
+
<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>
|
|
49
|
+
<% end %>
|
|
50
|
+
<% end %>
|
|
51
|
+
|
|
52
|
+
<%= render "components/card" do %>
|
|
53
|
+
<%= render "components/card/header" do %>
|
|
54
|
+
<%= render "components/card/title", text: "Caching" %>
|
|
55
|
+
<% end %>
|
|
56
|
+
<%= render "components/card/content" do %>
|
|
57
|
+
<p class="text-sm text-muted-foreground">Solid Cache with SQLite backend. No Redis or Memcached to manage.</p>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% end %>
|
|
60
|
+
|
|
61
|
+
<%= render "components/card" do %>
|
|
62
|
+
<%= render "components/card/header" do %>
|
|
63
|
+
<%= render "components/card/title", text: "Real-time" %>
|
|
64
|
+
<% end %>
|
|
65
|
+
<%= render "components/card/content" do %>
|
|
66
|
+
<p class="text-sm text-muted-foreground">Solid Cable for WebSocket support. Action Cable backed by SQLite — zero external dependencies.</p>
|
|
67
|
+
<% end %>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<%= render "components/card" do %>
|
|
71
|
+
<%= render "components/card/header" do %>
|
|
72
|
+
<%= render "components/card/title", text: "UI Components" %>
|
|
73
|
+
<% end %>
|
|
74
|
+
<%= render "components/card/content" do %>
|
|
75
|
+
<p class="text-sm text-muted-foreground">maquina_components — ERB partials styled with Tailwind CSS and Stimulus controllers. No React, no build step.</p>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% end %>
|
|
78
|
+
|
|
79
|
+
<%= render "components/card" do %>
|
|
80
|
+
<%= render "components/card/header" do %>
|
|
81
|
+
<%= render "components/card/title", text: "Standard Rails" %>
|
|
82
|
+
<% end %>
|
|
83
|
+
<%= render "components/card/content" do %>
|
|
84
|
+
<p class="text-sm text-muted-foreground">Importmaps, Turbo, Stimulus. No webpack, no bundler, no extra frameworks to learn.</p>
|
|
85
|
+
<% end %>
|
|
86
|
+
<% end %>
|
|
87
|
+
|
|
88
|
+
</div>
|
|
89
|
+
</section>
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
<section class="max-w-5xl mx-auto px-5 pb-12">
|
|
93
|
+
<%= render "components/card" do %>
|
|
94
|
+
<%= render "components/card/header" do %>
|
|
95
|
+
<%= render "components/card/title", text: "Maquina Components" %>
|
|
96
|
+
<%= render "components/card/description", text: "UI components for Rails — ERB partials styled with Tailwind CSS 4.0 and Stimulus controllers. Inspired by shadcn/ui, built for the Rails way." %>
|
|
97
|
+
<% end %>
|
|
98
|
+
<%= render "components/card/content" do %>
|
|
99
|
+
<div class="flex flex-wrap gap-2">
|
|
100
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Card<% end %>
|
|
101
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Badge<% end %>
|
|
102
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Table<% end %>
|
|
103
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Sidebar<% end %>
|
|
104
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Calendar<% end %>
|
|
105
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Combobox<% end %>
|
|
106
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Date Picker<% end %>
|
|
107
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Toast<% end %>
|
|
108
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Dropdown Menu<% end %>
|
|
109
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Toggle Group<% end %>
|
|
110
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Pagination<% end %>
|
|
111
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Breadcrumbs<% end %>
|
|
112
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Alert<% end %>
|
|
113
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Empty State<% end %>
|
|
114
|
+
<%= render "components/badge", variant: :outline, size: :sm do %>Forms<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
<% end %>
|
|
117
|
+
<%= render "components/card/footer" do %>
|
|
118
|
+
<%= link_to "Browse Components", "https://maquina.app/documentation/components/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "primary" } %>
|
|
119
|
+
<% end %>
|
|
120
|
+
<% end %>
|
|
121
|
+
</section>
|
|
122
|
+
|
|
123
|
+
<div class="max-w-5xl mx-auto px-5">
|
|
124
|
+
<%= render "components/separator" %>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
<section class="max-w-5xl mx-auto px-5 py-12">
|
|
129
|
+
<p class="text-sm font-semibold uppercase tracking-wider text-primary mb-6">Ecosystem</p>
|
|
130
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
131
|
+
|
|
132
|
+
<%= render "components/card" do %>
|
|
133
|
+
<%= render "components/card/header" do %>
|
|
134
|
+
<%= render "components/card/title", text: "Rails MCP Server" %>
|
|
135
|
+
<% end %>
|
|
136
|
+
<%= render "components/card/content" do %>
|
|
137
|
+
<p class="text-sm text-muted-foreground">A Model Context Protocol server for Rails projects. Analyze models, routes, schemas, and execute read-only Ruby code in your Rails context.</p>
|
|
138
|
+
<% end %>
|
|
139
|
+
<% end %>
|
|
140
|
+
|
|
141
|
+
<%= render "components/card" do %>
|
|
142
|
+
<%= render "components/card/header" do %>
|
|
143
|
+
<%= render "components/card/title", text: "Neovim MCP Server" %>
|
|
144
|
+
<% end %>
|
|
145
|
+
<%= render "components/card/content" do %>
|
|
146
|
+
<p class="text-sm text-muted-foreground">MCP server for Neovim integration. Read and update buffers, coordinate file changes across your editor and AI assistants.</p>
|
|
147
|
+
<% end %>
|
|
148
|
+
<% end %>
|
|
149
|
+
|
|
150
|
+
<%= render "components/card" do %>
|
|
151
|
+
<%= render "components/card/header" do %>
|
|
152
|
+
<%= render "components/card/title", text: "Rails Upgrade Assistant" %>
|
|
153
|
+
<% end %>
|
|
154
|
+
<%= render "components/card/content" do %>
|
|
155
|
+
<p class="text-sm text-muted-foreground">A Claude Skill that analyzes Rails applications and generates upgrade guides. Covers Rails 7.0 through 8.1 with breaking changes and migration steps.</p>
|
|
156
|
+
<% end %>
|
|
157
|
+
<% end %>
|
|
158
|
+
|
|
159
|
+
<%= render "components/card" do %>
|
|
160
|
+
<%= render "components/card/header" do %>
|
|
161
|
+
<%= render "components/card/title", text: "Rails Simplifier" %>
|
|
162
|
+
<% end %>
|
|
163
|
+
<%= render "components/card/content" do %>
|
|
164
|
+
<p class="text-sm text-muted-foreground">Code simplification following 37signals patterns and the One Person Framework philosophy.</p>
|
|
165
|
+
<% end %>
|
|
166
|
+
<% end %>
|
|
167
|
+
|
|
168
|
+
</div>
|
|
169
|
+
<div class="mt-6 text-center">
|
|
170
|
+
<%= link_to "Explore All Tools", "https://maquina.app/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "outline" } %>
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
|
|
174
|
+
<div class="max-w-5xl mx-auto px-5">
|
|
175
|
+
<%= render "components/separator" %>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
<section class="max-w-5xl mx-auto px-5 py-12">
|
|
180
|
+
<p class="text-sm font-semibold uppercase tracking-wider text-primary mb-8 text-center">Philosophy</p>
|
|
181
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-10">
|
|
182
|
+
<div>
|
|
183
|
+
<h3 class="font-bold text-lg text-foreground mb-2">Production First</h3>
|
|
184
|
+
<p class="text-sm text-muted-foreground leading-relaxed">Every tool starts in a real application. No theoretical exercises — only code that has shipped.</p>
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<h3 class="font-bold text-lg text-foreground mb-2">Standard Rails</h3>
|
|
188
|
+
<p class="text-sm text-muted-foreground leading-relaxed">ERB, Tailwind, Stimulus. No new paradigms to learn, no complex build pipelines.</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<h3 class="font-bold text-lg text-foreground mb-2">One Person Scale</h3>
|
|
192
|
+
<p class="text-sm text-muted-foreground leading-relaxed">Built for developers who ship alone. Simple tools that stay out of your way.</p>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</section>
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
<section class="bg-primary/5 border-t border-border">
|
|
199
|
+
<div class="max-w-5xl mx-auto px-5 py-16 text-center">
|
|
200
|
+
<h2 class="text-2xl font-bold text-foreground mb-4">Start building today</h2>
|
|
201
|
+
<p class="text-muted-foreground mb-6">Visit maquina.app to explore the full ecosystem.</p>
|
|
202
|
+
<%= link_to "Visit maquina.app", "https://maquina.app/", target: "_blank", rel: "noopener noreferrer", data: { component: "button", variant: "primary" } %>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|